From a203eb5d92d781df35d93a2181677640a1e61246 Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Sun, 2 Aug 2026 02:15:57 +0300 Subject: [PATCH 01/14] Add DailyTaskAPI module sources Add the standalone daily quest module, task planner, native quest sources, combat helper, and PET NPC locator. --- .../berke/dailytaskapi/DailyNpcCombat.java | 90 ++ .../dailytaskapi/DailyNpcPetLocator.java | 95 ++ .../berke/dailytaskapi/DailyTaskAPI.java | 1220 +++++++++++++++++ .../berke/dailytaskapi/DailyTaskConfig.java | 26 + .../berke/dailytaskapi/DailyTaskPlanner.java | 228 +++ .../berke/dailytaskapi/QuestGiverSource.java | 73 + .../berke/dailytaskapi/QuestMenuSource.java | 239 ++++ 7 files changed, 1971 insertions(+) create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/QuestGiverSource.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java new file mode 100644 index 00000000..2fb63406 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java @@ -0,0 +1,90 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.PluginAPI; +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.game.entities.Npc; +import eu.darkbot.api.managers.ConfigAPI; +import eu.darkbot.shared.modules.LootModule; + +import java.util.Comparator; + +/** + * DarkBot's standard NPC combat engine with target selection restricted to + * the NPC required by the currently displayed daily quest. + * + *

The inherited implementation supplies the saved NPC radius, circle and + * run-configuration behaviour, ammunition selection, target validation and + * SafetyFinder logic used by NPC Kill and Collect. DailyTaskAPI remains the + * owner of quest and map selection.

+ */ +final class DailyNpcCombat extends LootModule { + private String targetDescription; + + DailyNpcCombat(PluginAPI api) { + super(api); + } + + void tick(String description) { + targetDescription = description; + ConfigSetting petEnabled = api.requireAPI(ConfigAPI.class).requireConfig("pet.enabled"); + if (!Boolean.TRUE.equals(petEnabled.getValue())) petEnabled.setValue(true); + super.onTickModule(); + } + + void stopCombat() { + attack.stopAttack(); + attack.setTarget(null); + targetDescription = null; + if (pet.isEnabled()) pet.setEnabled(false); + } + + @Override + protected boolean checkMap() { + // DailyTaskAPI resolves and navigates to the quest's required map. + // Do not let the saved working-map setting override that choice. + return true; + } + + @Override + protected boolean findTarget() { + Npc current = attack.getTargetAs(Npc.class); + if (isUsableTarget(current)) return true; + + if (current != null) { + attack.stopAttack(); + attack.setTarget(null); + } + + Npc selected = npcs.stream() + .filter(this::isUsableTarget) + .min(Comparator.comparingDouble(hero::distanceTo)) + .orElse(null); + attack.setTarget(selected); + // MovementAPI.moveRandom follows the saved preferred-zone route for + // this map and falls back to a random map point when no route exists. + if (selected == null && !movement.isMoving()) { + ConfigSetting keepPoint = api.requireAPI(ConfigAPI.class) + .requireConfig("general.roaming.keep"); + if (Boolean.TRUE.equals(keepPoint.getValue())) keepPoint.setValue(false); + movement.moveRandom(); + } + return selected != null; + } + + private boolean isUsableTarget(Npc npc) { + if (npc == null || targetDescription == null || + !npc.isValid() || !npc.isSelectable() || npc.isBlacklisted()) { + return false; + } + String name = npc.getInfo() == null ? null : npc.getInfo().getName(); + if (name == null || movement.getClosestDistance(npc) >= 590d) return false; + + String target = DailyTaskPlanner.normalizeNpcName(targetDescription); + String candidate = DailyTaskPlanner.normalizeNpcName(name); + boolean wantsBoss = target.contains("boss"); + boolean wantsUber = target.contains("uber"); + return !candidate.isBlank() && target.contains(candidate) && + wantsBoss == candidate.startsWith("boss ") && + wantsUber == candidate.startsWith("uber"); + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java new file mode 100644 index 00000000..4c63e243 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java @@ -0,0 +1,95 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.PluginAPI; +import eu.darkbot.api.extensions.Feature; +import eu.darkbot.api.extensions.Module; +import eu.darkbot.api.extensions.selectors.GearSelector; +import eu.darkbot.api.extensions.selectors.PetGearSupplier; +import eu.darkbot.api.extensions.selectors.PrioritizedSupplier; +import eu.darkbot.api.game.enums.PetGear; +import eu.darkbot.api.managers.BotAPI; +import eu.darkbot.api.managers.PetAPI; + +import java.util.Collection; + +/** Selects the quest NPC in P.E.T. Enemy Locator while DailyTaskAPI hunts. */ +@Feature( + name = "DailyTaskAPI PET Locator", + description = "Uses Enemy Locator for the active daily NPC", + enabledByDefault = true) +public final class DailyNpcPetLocator implements GearSelector, PetGearSupplier { + private final BotAPI bot; + private final PetAPI pet; + + public DailyNpcPetLocator(PluginAPI api) { + this.bot = api.requireAPI(BotAPI.class); + this.pet = api.requireAPI(PetAPI.class); + } + + @Override + public PetGearSupplier getGearSupplier() { + return this; + } + + @Override + public PrioritizedSupplier.Priority getPriority() { + return activeDaily() == null + ? PrioritizedSupplier.Priority.LOWEST + : PrioritizedSupplier.Priority.HIGHEST; + } + + @Override + public PetGear get() { + if (activeDaily() != null && pet.hasGear(PetGear.ENEMY_LOCATOR)) { + return PetGear.ENEMY_LOCATOR; + } + PetGear current = pet.getGear(); + return current == null ? PetGear.PASSIVE : current; + } + + @Override + public Boolean enablePet() { + return activeDaily() == null ? null : Boolean.TRUE; + } + + @Override + public PetAPI.LocatorPick getNpcLocatorPick( + Collection available) { + DailyTaskAPI daily = activeDaily(); + if (daily == null) return PetGearSupplier.super.getNpcLocatorPick(available); + String description = daily.getNpcLocatorTargetDescription(); + return available.stream() + .filter(pick -> matches(description, pick)) + .findFirst() + .orElse(null); + } + + @Override + public Integer getNpcPickPriority(PetAPI.LocatorPick pick) { + DailyTaskAPI daily = activeDaily(); + if (daily == null) return PetGearSupplier.super.getNpcPickPriority(pick); + return matches(daily.getNpcLocatorTargetDescription(), pick) ? 0 : null; + } + + private DailyTaskAPI activeDaily() { + if (!bot.isRunning()) return null; + Module module = bot.getNonTemporalModule(); + if (!(module instanceof DailyTaskAPI)) module = bot.getModule(); + if (module instanceof DailyTaskAPI) { + DailyTaskAPI daily = (DailyTaskAPI) module; + if (daily.getNpcLocatorTargetDescription() != null) return daily; + } + return null; + } + + private boolean matches(String description, PetAPI.LocatorPick pick) { + if (description == null || pick == null || pick.getName() == null) return false; + String target = DailyTaskPlanner.normalizeNpcName(description); + String candidate = DailyTaskPlanner.normalizeNpcName(pick.getName()); + boolean wantsBoss = target.contains("boss"); + boolean wantsUber = target.contains("uber"); + return !candidate.isBlank() && target.contains(candidate) && + wantsBoss == candidate.startsWith("boss ") && + wantsUber == candidate.startsWith("uber"); + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java new file mode 100644 index 00000000..da2c1e85 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -0,0 +1,1220 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.PluginAPI; +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.extensions.Configurable; +import eu.darkbot.api.extensions.Feature; +import eu.darkbot.api.extensions.Module; +import eu.darkbot.api.game.entities.Box; +import eu.darkbot.api.game.entities.Portal; +import eu.darkbot.api.game.entities.Station; +import eu.darkbot.api.game.other.GameMap; +import eu.darkbot.api.game.other.Gui; +import eu.darkbot.api.managers.AttackAPI; +import eu.darkbot.api.managers.BotAPI; +import eu.darkbot.api.managers.EntitiesAPI; +import eu.darkbot.api.managers.GameScreenAPI; +import eu.darkbot.api.managers.HeroAPI; +import eu.darkbot.api.managers.MovementAPI; +import eu.darkbot.api.managers.OreAPI; +import eu.darkbot.api.managers.QuestAPI; +import eu.darkbot.api.managers.StarSystemAPI; + +import java.util.Comparator; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +/** + * Standalone daily-quest module. + * + *

This class deliberately does not extend LootCollectorModule and never + * reads or writes the user's working-map, NPC-selection, box-selection or + * ore-selling settings. Selecting another DarkBot module therefore leaves + * that module's saved settings untouched.

+ */ +@Feature(name = "DailyTaskAPI", description = "Yalnızca 24 saatlik günlük görevleri tamamlar") +public final class DailyTaskAPI implements Module, Configurable { + private static final String QUEST_BASE_MAP = "1-8"; + private static final double QUEST_STATION_DISTANCE = 300d; + private static final Set SPECIAL_DAILY_QUEST_IDS = Set.of(318001); + + private enum State { + STARTING, DISCOVERING, WAITING_FOR_DISCOVERY, + SELECTING, WAITING_FOR_SELECTION, RUNNING, + OPENING_GIVER, WAITING_GIVER_OPEN, + SELECTING_DAILY_TAB, WAITING_DAILY_TAB, + SELECTING_GIVER_ROW, WAITING_GIVER_ROW, + ACCEPTING_GIVER_QUEST, WAITING_GIVER_ACCEPT, + CLOSING_GIVER, PAUSED, DONE + } + + private static final class QuestChoice { + private final int id; + private final String title; + private final QuestMenuSource.Selector selector; + + private QuestChoice(int id, String title, QuestMenuSource.Selector selector) { + this.id = id; + this.title = title; + this.selector = selector; + } + + private int id() { + return id; + } + + private String title() { + return title; + } + + private QuestMenuSource.Selector selector() { + return selector; + } + } + + private final QuestAPI quests; + private final BotAPI bot; + private final GameScreenAPI gameScreen; + private final StarSystemAPI starSystem; + private final OreAPI ores; + private final EntitiesAPI entities; + private final MovementAPI movement; + private final AttackAPI attack; + private final HeroAPI hero; + private final Gui questGui; + private final QuestMenuSource questMenuSource; + private final QuestGiverSource questGiverSource; + private final DailyNpcCombat npcCombat; + + private DailyTaskConfig settings = new DailyTaskConfig(); + private State state = State.STARTING; + private int selectionRetries; + private int menuScanPass; + private int selectorIndex; + private int questIdBeforeSelection = -1; + private int currentQuestId = -1; + private String currentQuestTitle = ""; + private long nextActionAt; + private long nextRoamAt; + private String status = "Başlatılıyor"; + private String targetNpcDescription; + private String targetMapName; + private OreAPI.Ore oreToSell; + private boolean collectBonus; + private boolean collectCargo; + private final Set completedQuestIds = new HashSet<>(); + private final Set knownDailyQuestIds = new HashSet<>(); + private final Set scannedQuestIds = new LinkedHashSet<>(); + private final Map dailyChoices = new LinkedHashMap<>(); + private List selectors = List.of(); + private QuestChoice pendingChoice; + private boolean offerScanNeeded = true; + private int giverOpenAttempts; + private int dailyTabRetries; + private int giverScrollPage; + private boolean giverPageHadNewId; + private int giverRowIndex = 1; + private int giverRowRetries; + private int selectedOfferId = -1; + private String selectedOfferTitle = ""; + private int acceptRetries; + private final Set reviewedOfferIds = new LinkedHashSet<>(); + private final Set seenGiverOfferIds = new LinkedHashSet<>(); + private final Set acceptedOfferIds = new LinkedHashSet<>(); + private final Set skippedTetrathrinOfferIds = new LinkedHashSet<>(); + + public DailyTaskAPI(PluginAPI api) { + this.quests = api.requireAPI(QuestAPI.class); + this.bot = api.requireAPI(BotAPI.class); + this.gameScreen = api.requireAPI(GameScreenAPI.class); + this.starSystem = api.requireAPI(StarSystemAPI.class); + this.ores = api.requireAPI(OreAPI.class); + this.entities = api.requireAPI(EntitiesAPI.class); + this.movement = api.requireAPI(MovementAPI.class); + this.attack = api.requireAPI(AttackAPI.class); + this.hero = api.requireAPI(HeroAPI.class); + this.questGui = gameScreen.getGui("quests"); + this.questMenuSource = new QuestMenuSource(questGui); + this.questGiverSource = new QuestGiverSource(gameScreen); + this.npcCombat = new DailyNpcCombat(api); + } + + @Override + public void setConfig(ConfigSetting setting) { + DailyTaskConfig configured = setting.getValue(); + if (configured != null) settings = configured; + setting.addListener(updated -> { + if (updated != null) settings = updated; + }); + } + + @Override + public void onTickModule() { + long now = System.currentTimeMillis(); + if (state == State.DONE) { + // Keep the completed state for the rest of the local day. Pressing + // Start again must not scan the same finished quests repeatedly. + // After midnight a deliberate new start begins a fresh daily run. + if (nextActionAt > now) { + npcCombat.stopCombat(); + boolean petStillActive = true; + try { + java.lang.reflect.Field petField = null; + for (Class type = npcCombat.getClass(); type != null; type = type.getSuperclass()) { + try { + petField = type.getDeclaredField("pet"); + break; + } catch (NoSuchFieldException ignored) { + // Continue through the exact runtime class hierarchy. + } + } + if (petField != null) { + petField.setAccessible(true); + petStillActive = ((eu.darkbot.api.managers.PetAPI) petField.get(npcCombat)).isActive(); + } + } catch (Throwable ignored) { + // Do not stop before PET shutdown can be confirmed. + } + if (petStillActive || now < nextRoamAt) { + status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + + " | PET kapatılıyor | Bot PET kapanınca duracak"; + return; + } + status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + + " | Tamamlanan: " + completedQuestIds.size() + + " | PET kapalı | Bot durduruldu"; + if (bot.isRunning()) bot.setRunning(false); + return; + } + state = State.STARTING; + } + if (state == State.STARTING) { + resetSession(); + if (quests.isQuestGiverOpen()) { + questGiverSource.close(); + nextActionAt = now + settings.questSwitchDelayMs; + } + state = State.DISCOVERING; + if (nextActionAt < now) nextActionAt = now; + } + + // Active quests can be discovered and selected from the quest menu on + // any map. Travel to the 1-8 station only after no active daily quest + // remains and the quest-giver offers must be inspected. + if (isGiverState(state)) { + if (!ensureQuestStation()) return; + if (now >= nextActionAt) tickQuestGiver(); + return; + } + + if (state == State.DISCOVERING) { + if (now >= nextActionAt) discoverOrScanNext(); + return; + } + if (state == State.WAITING_FOR_DISCOVERY) { + if (now >= nextActionAt) inspectDiscoveredSelector(); + return; + } + if (state == State.SELECTING) { + if (now >= nextActionAt) selectPendingDaily(); + return; + } + if (state == State.WAITING_FOR_SELECTION) { + if (now >= nextActionAt) inspectPendingSelection(); + return; + } + if (state == State.PAUSED) return; + + QuestAPI.Quest quest = quests.getDisplayedQuest(); + if (quest == null) { + // Completing a quest (and slow server responses) can leave the + // displayed quest empty for a few ticks. Wait for the next source + // snapshot instead of entering a permanent paused state. + attack.stopAttack(); + movement.stop(false); + hero.setRoamMode(); + status = "Görev verisi yenileniyor; mevcut plan korunuyor"; + return; + } + + if (currentQuestId >= 0 && quest.getId() != currentQuestId) { + completedQuestIds.add(currentQuestId); + stopCurrentAction(); + currentQuestId = -1; + currentQuestTitle = "__PARK_AFTER_DAILY__"; + prepareNewScan(); + nextActionAt = now + settings.questSwitchDelayMs; + status = "Görev değişti; görev menüsü kaynaktan yeniden taranıyor"; + return; + } + + if (!isVerifiedDaily(quest)) { + stopCurrentAction(); + prepareNewScan(); + nextActionAt = now + settings.questSwitchDelayMs; + status = "Görev günlük değil; kaynak menü yeniden taranıyor"; + return; + } + + double progress = DailyTaskPlanner.progress(quest); + if (quest.isCompleted() || progress >= 0.9999d) { + completedQuestIds.add(quest.getId()); + status = "Tamamlandı: " + safeTitle(quest); + stopCurrentAction(); + currentQuestId = -1; + currentQuestTitle = "__PARK_AFTER_DAILY__"; + prepareNewScan(); + nextActionAt = now + settings.questSwitchDelayMs; + return; + } + + buildPlanFromSource(quest); + boolean standardNpcCombatOwnsSafety = targetNpcDescription != null && + (targetMapName == null || isOnMap(targetMapName)); + if (!standardNpcCombatOwnsSafety && runLocalSafety()) return; + executePlan(quest, progress); + } + + private void resetSession() { + selectionRetries = 0; + menuScanPass = 0; + selectorIndex = 0; + questIdBeforeSelection = -1; + currentQuestId = -1; + currentQuestTitle = ""; + pendingChoice = null; + completedQuestIds.clear(); + knownDailyQuestIds.clear(); + scannedQuestIds.clear(); + dailyChoices.clear(); + selectors = List.of(); + offerScanNeeded = true; + giverOpenAttempts = 0; + dailyTabRetries = 0; + giverScrollPage = 0; + giverPageHadNewId = false; + giverRowIndex = 1; + giverRowRetries = 0; + selectedOfferId = -1; + selectedOfferTitle = ""; + acceptRetries = 0; + reviewedOfferIds.clear(); + seenGiverOfferIds.clear(); + acceptedOfferIds.clear(); + skippedTetrathrinOfferIds.clear(); + clearPlan(); + status = "Görev menüsü kaynaktan taranıyor"; + } + + private void discoverOrScanNext() { + if ("__PARK_AFTER_DAILY__".equals(currentQuestTitle)) { + Optional nearestPortal = entities.getPortals().stream() + .filter(Portal::isValid) + .min(Comparator.comparingDouble(hero::distanceTo)); + if (nearestPortal.isEmpty()) { + attack.stopAttack(); + status = "Görev sonu için en yakın portal verisi bekleniyor"; + nextActionAt = System.currentTimeMillis() + 1_000L; + return; + } + double portalDistance = hero.distanceTo(nearestPortal.get()); + if (portalDistance > 300d) { + attack.stopAttack(); + hero.setRunMode(); + movement.moveTo(nearestPortal.get()); + status = "Görev tamamlandı; en yakın portala gidiliyor: " + + (int) Math.round(portalDistance); + nextActionAt = System.currentTimeMillis() + 1_000L; + return; + } + movement.stop(false); + hero.setRoamMode(); + currentQuestTitle = ""; + status = "Görev tamamlandı; en yakın portalda güvenli konum alındı"; + } + + if (questGui == null) { + pauseSafely("DarkBot görev penceresi nesnesi bulunamadı"); + return; + } + if (!questGui.isVisible()) { + if (!settings.autoOpenQuestWindow) { + status = "Görev penceresini aç"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + questGui.setVisible(true); + status = "Görev penceresi açılıyor"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + + if (selectors.isEmpty()) { + refreshDailyCatalog(); + selectors = questMenuSource.discoverSelectors(); + if (selectors.isEmpty()) { + retryMenuScan(questMenuSource.getLastError()); + return; + } + menuScanPass++; + selectorIndex = 0; + selectionRetries = 0; + if (menuScanPass == 1) { + scannedQuestIds.clear(); + dailyChoices.clear(); + } + QuestAPI.Quest displayed = quests.getDisplayedQuest(); + recordQuest(displayed, null); + if (displayed != null && dailyChoices.containsKey(displayed.getId()) && !isFinished(displayed)) { + startDaily(displayed); + return; + } + } + + if (selectorIndex >= selectors.size()) { + finishMenuScan(); + return; + } + + QuestMenuSource.Selector selector = selectors.get(selectorIndex); + QuestAPI.Quest before = quests.getDisplayedQuest(); + questIdBeforeSelection = before == null ? -1 : before.getId(); + questGui.click(selector.x(), selector.y()); + state = State.WAITING_FOR_DISCOVERY; + status = "Görev menüsü taranıyor: " + (selectorIndex + 1) + "/" + selectors.size(); + nextActionAt = System.currentTimeMillis() + 500L; + } + + private void inspectDiscoveredSelector() { + QuestAPI.Quest quest = quests.getDisplayedQuest(); + if (quest == null) { + retryDiscoveredSelector("Sunucudan görev verisi bekleniyor"); + return; + } + + QuestMenuSource.Selector selector = selectors.get(selectorIndex); + if (quest.getId() == questIdBeforeSelection) { + selectorIndex++; + selectionRetries = 0; + state = State.DISCOVERING; + status = "Görev satırı yanıt vermedi; tek taramada sonraki satıra geçiliyor"; + nextActionAt = System.currentTimeMillis() + 250L; + return; + } + + recordQuest(quest, selector); + if (dailyChoices.containsKey(quest.getId()) && !isFinished(quest)) { + startDaily(quest); + return; + } + selectorIndex++; + selectionRetries = 0; + state = State.DISCOVERING; + nextActionAt = System.currentTimeMillis() + 250L; + } + + private void finishMenuScan() { + QuestAPI.Quest displayed = quests.getDisplayedQuest(); + recordQuest(displayed, selectorIndex > 0 && selectorIndex <= selectors.size() + ? selectors.get(selectorIndex - 1) : null); + + QuestChoice current = displayed == null ? null : dailyChoices.get(displayed.getId()); + if (current != null && !isFinished(displayed)) { + startDaily(displayed); + return; + } + + pendingChoice = dailyChoices.values().stream() + .filter(choice -> !completedQuestIds.contains(choice.id())) + .filter(choice -> choice.selector() != null) + .findFirst() + .orElse(null); + if (pendingChoice != null) { + selectionRetries = 0; + state = State.SELECTING; + nextActionAt = System.currentTimeMillis() + 250L; + status = scanSummary() + " | Günlük seçiliyor: " + pendingChoice.title(); + return; + } + + if (offerScanNeeded) { + beginQuestOfferScan(); + return; + } + finishAll(); + } + + private boolean isGiverState(State value) { + return value == State.OPENING_GIVER || value == State.WAITING_GIVER_OPEN || + value == State.SELECTING_DAILY_TAB || value == State.WAITING_DAILY_TAB || + value == State.SELECTING_GIVER_ROW || value == State.WAITING_GIVER_ROW || + value == State.ACCEPTING_GIVER_QUEST || value == State.WAITING_GIVER_ACCEPT || + value == State.CLOSING_GIVER; + } + + private void beginQuestOfferScan() { + stopCurrentAction(); + if (questGui != null && questGui.isVisible()) questGui.setVisible(false); + // This set represents offers accepted by the current station scan. + // Keeping ids from an earlier scan made CLOSING_GIVER believe that new + // quests had been accepted forever, causing an active-menu scan loop. + acceptedOfferIds.clear(); + giverOpenAttempts = 0; + dailyTabRetries = 0; + giverScrollPage = 0; + giverPageHadNewId = false; + giverRowIndex = 1; + giverRowRetries = 0; + selectedOfferId = -1; + selectedOfferTitle = ""; + acceptRetries = 0; + seenGiverOfferIds.clear(); + state = State.OPENING_GIVER; + nextActionAt = System.currentTimeMillis() + 500L; + status = "İstasyondaki günlük teklifler kaynak verisiyle taranacak"; + } + + private void tickQuestGiver() { + if (state == State.CLOSING_GIVER) { + if (quests.isQuestGiverOpen()) { + questGiverSource.close(); + status = "Görev verici kapatılıyor"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + if (acceptedOfferIds.isEmpty()) { + finishAll(); + return; + } + prepareNewScan(); + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + status = "Kabul edilen günlükler aktif menüden doğrulanıyor"; + return; + } + + if (state == State.OPENING_GIVER || state == State.WAITING_GIVER_OPEN) { + if (quests.isQuestGiverOpen()) { + giverOpenAttempts = 0; + state = State.SELECTING_DAILY_TAB; + nextActionAt = System.currentTimeMillis() + 300L; + status = "Günlük görev sekmesi açılıyor"; + return; + } + Optional station = findQuestStation(); + if (station.isPresent() && hero.distanceTo(station.get()) <= QUEST_STATION_DISTANCE) { + station.get().trySelect(false); + } + giverOpenAttempts++; + state = State.WAITING_GIVER_OPEN; + status = "Görev vericinin sunucu yanıtı bekleniyor (" + giverOpenAttempts + ")"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + + if (!quests.isQuestGiverOpen()) { + state = State.OPENING_GIVER; + nextActionAt = System.currentTimeMillis() + 500L; + status = "Görev verici kapandı; yeniden açılıyor"; + return; + } + + if (state == State.SELECTING_DAILY_TAB) { + questGiverSource.selectDailyTab(); + state = State.WAITING_DAILY_TAB; + status = "Günlük sekmesinin kaynak verisi bekleniyor"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + if (state == State.WAITING_DAILY_TAB) { + giverRowIndex = 1; + giverRowRetries = 0; + state = State.SELECTING_GIVER_ROW; + nextActionAt = System.currentTimeMillis() + 250L; + return; + } + if (state == State.SELECTING_GIVER_ROW) { + if (giverRowIndex > QuestGiverSource.MAX_ROWS) { + if ((giverScrollPage > 0 && !giverPageHadNewId) || + giverScrollPage >= QuestGiverSource.MAX_SCROLL_PAGES) { + finishQuestOfferScan(); + return; + } + giverScrollPage++; + giverPageHadNewId = false; + giverRowIndex = 1; + giverRowRetries = 0; + questGiverSource.scrollDailyListDown(); + state = State.WAITING_DAILY_TAB; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + questGiverSource.selectRow(giverRowIndex); + state = State.WAITING_GIVER_ROW; + status = "Günlük teklif okunuyor: satır " + giverRowIndex; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + if (state == State.WAITING_GIVER_ROW) { + inspectGiverRow(); + return; + } + if (state == State.ACCEPTING_GIVER_QUEST) { + acceptSelectedOffer(); + return; + } + if (state == State.WAITING_GIVER_ACCEPT) verifyAcceptedOffer(); + } + + private void inspectGiverRow() { + QuestAPI.QuestListItem info = quests.getSelectedQuestInfo(); + QuestAPI.Quest selected = quests.getSelectedQuest(); + boolean matchingSource = info != null && selected != null && info.getId() == selected.getId(); + if (!matchingSource) { + retryOrAdvanceGiverRow("Teklif ayrıntısı sunucudan bekleniyor"); + return; + } + if (seenGiverOfferIds.add(info.getId())) giverPageHadNewId = true; + boolean dailyOffer = DailyTaskPlanner.isDailyType(info.getType()) || DailyTaskPlanner.isDaily(selected); + if (!dailyOffer) { + advanceGiverRow(); + return; + } + + boolean protegitKill = false; + for (QuestAPI.Requirement requirement : DailyTaskPlanner.actionable(selected)) { + if (isNpcRequirement(requirement) && + DailyTaskPlanner.normalizeNpcName(requirement.getDescription()).contains("protegit")) { + protegitKill = true; + break; + } + } + if (protegitKill) { + reviewedOfferIds.add(info.getId()); + status = "Protegit öldürme görevi pas geçildi: " + safeTitle(selected); + advanceGiverRow(); + return; + } + + int id = info.getId(); + if (reviewedOfferIds.contains(id) || info.isCompleted() || !info.isActivable()) { + reviewedOfferIds.add(id); + advanceGiverRow(); + return; + } + if (selected.getRewards() == null || selected.getRewards().isEmpty()) { + retryOrAdvanceGiverRow("Teklif ödülleri kaynaktan bekleniyor"); + return; + } + if (!DailyTaskPlanner.hasUridiumReward(selected.getRewards())) { + reviewedOfferIds.add(id); + status = "Uridium ödülü olmayan günlük pas geçildi: " + safeTitle(selected); + advanceGiverRow(); + return; + } + + selectedOfferId = id; + selectedOfferTitle = safeTitle(selected); + acceptRetries = 0; + state = State.ACCEPTING_GIVER_QUEST; + nextActionAt = System.currentTimeMillis() + 250L; + status = "Kaynak verisi doğrulandı; günlük kabul edilecek: " + selectedOfferTitle; + } + + private void retryOrAdvanceGiverRow(String reason) { + giverRowRetries++; + if (giverRowRetries < Math.max(2, settings.maxSelectionRetries)) { + state = State.SELECTING_GIVER_ROW; + status = reason + " (" + giverRowRetries + ")"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + advanceGiverRow(); + } + + private void advanceGiverRow() { + giverRowIndex++; + giverRowRetries = 0; + state = State.SELECTING_GIVER_ROW; + nextActionAt = System.currentTimeMillis() + 300L; + } + + private void acceptSelectedOffer() { + QuestAPI.QuestListItem info = quests.getSelectedQuestInfo(); + QuestAPI.Quest selected = quests.getSelectedQuest(); + boolean protegitKill = false; + if (selected != null) { + for (QuestAPI.Requirement requirement : DailyTaskPlanner.actionable(selected)) { + if (isNpcRequirement(requirement) && + DailyTaskPlanner.normalizeNpcName(requirement.getDescription()).contains("protegit")) { + protegitKill = true; + break; + } + } + } + boolean safe = info != null && selected != null && info.getId() == selectedOfferId && + selected.getId() == selectedOfferId && + (DailyTaskPlanner.isDailyType(info.getType()) || DailyTaskPlanner.isDaily(selected)) && + !protegitKill && + info.isActivable() && !info.isCompleted() && + selected.getRewards() != null && !selected.getRewards().isEmpty() && + DailyTaskPlanner.hasUridiumReward(selected.getRewards()); + if (!safe) { + int skippedId = selectedOfferId; + String skippedTitle = selectedOfferTitle; + if (skippedId > 0) reviewedOfferIds.add(skippedId); + selectedOfferId = -1; + selectedOfferTitle = ""; + status = info != null && info.getId() == skippedId && !info.isActivable() + ? "Günlük artık alınabilir değil; pas geçildi: " + skippedTitle + : "Günlük kaynak doğrulaması değişti; güvenle pas geçildi: " + skippedTitle; + advanceGiverRow(); + return; + } + questGiverSource.acceptSelected(); + state = State.WAITING_GIVER_ACCEPT; + status = "Günlük kabulü sunucudan doğrulanıyor: " + selectedOfferTitle; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void verifyAcceptedOffer() { + QuestAPI.QuestListItem catalog = findQuestItem(selectedOfferId).orElse(null); + boolean accepted = catalog != null && !catalog.isActivable(); + if (accepted) { + acceptedOfferIds.add(selectedOfferId); + reviewedOfferIds.add(selectedOfferId); + status = "Günlük kabul edildi: " + selectedOfferTitle; + selectedOfferId = -1; + selectedOfferTitle = ""; + giverRowIndex = 1; + giverRowRetries = 0; + dailyTabRetries = 0; + giverPageHadNewId = false; + state = quests.isQuestGiverOpen() ? State.SELECTING_GIVER_ROW : State.OPENING_GIVER; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + if (acceptRetries++ < Math.max(2, settings.maxSelectionRetries)) { + state = State.ACCEPTING_GIVER_QUEST; + status = "Günlük kabul yanıtı gecikti; kaynak yeniden doğrulanacak"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + pauseSafely("Günlük kabulü sunucudan doğrulanamadı: " + selectedOfferTitle); + } + + private Optional findQuestItem(int id) { + try { + return quests.getCurrestQuests().stream() + .filter(item -> item != null && item.getId() == id) + .findFirst() + .map(item -> (QuestAPI.QuestListItem) item); + } catch (Throwable ignored) { + return Optional.empty(); + } + } + + private void finishQuestOfferScan() { + offerScanNeeded = false; + state = State.CLOSING_GIVER; + status = "İstasyon taraması tamamlandı | Kabul: " + acceptedOfferIds.size() + + " | Tetrathrin pas: " + skippedTetrathrinOfferIds.size(); + nextActionAt = System.currentTimeMillis() + 300L; + } + + private void selectPendingDaily() { + if (pendingChoice == null || pendingChoice.selector() == null) { + prepareNewScan(); + return; + } + QuestAPI.Quest displayed = quests.getDisplayedQuest(); + if (displayed != null && displayed.getId() == pendingChoice.id() && isVerifiedDaily(displayed)) { + startDaily(displayed); + return; + } + questIdBeforeSelection = displayed == null ? -1 : displayed.getId(); + questGui.click(pendingChoice.selector().x(), pendingChoice.selector().y()); + state = State.WAITING_FOR_SELECTION; + status = "Günlük görev kaynaktan seçiliyor: " + pendingChoice.title(); + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void inspectPendingSelection() { + QuestAPI.Quest quest = quests.getDisplayedQuest(); + if (quest != null && pendingChoice != null && quest.getId() == pendingChoice.id() && isVerifiedDaily(quest)) { + startDaily(quest); + return; + } + + selectionRetries++; + if (selectionRetries >= Math.max(1, settings.maxSelectionRetries)) { + prepareNewScan(); + status = "Günlük seçim doğrulanamadı; görev menüsü yeniden taranıyor"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + state = State.SELECTING; + status = "Sunucudan günlük görev seçimi bekleniyor (" + selectionRetries + "/" + + settings.maxSelectionRetries + ")"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void startDaily(QuestAPI.Quest quest) { + currentQuestId = quest.getId(); + currentQuestTitle = safeTitle(quest); + selectionRetries = 0; + state = State.RUNNING; + buildPlanFromSource(quest); + status = "Günlük görev doğrulandı: " + currentQuestTitle; + } + + private void retryDiscoveredSelector(String reason) { + selectionRetries++; + if (selectionRetries >= Math.max(1, settings.maxSelectionRetries)) { + selectorIndex++; + selectionRetries = 0; + state = State.DISCOVERING; + status = reason + "; sıradaki kaynak seçici deneniyor"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return; + } + state = State.DISCOVERING; + status = reason + " (" + selectionRetries + "/" + settings.maxSelectionRetries + ")"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void retryMenuScan(String reason) { + status = reason + "; tek tarama tamamlandı"; + if (offerScanNeeded) beginQuestOfferScan(); + else finishAll(); + } + + private void prepareNewScan() { + state = State.DISCOVERING; + menuScanPass = 0; + selectorIndex = 0; + selectionRetries = 0; + questIdBeforeSelection = -1; + pendingChoice = null; + selectors = List.of(); + scannedQuestIds.clear(); + dailyChoices.clear(); + } + + private void refreshDailyCatalog() { + try { + for (QuestAPI.QuestListItem item : quests.getCurrestQuests()) { + if (item != null && DailyTaskPlanner.isDailyType(item.getType())) { + knownDailyQuestIds.add(item.getId()); + } + } + } catch (Throwable ignored) { + // The quest-giver catalog can be unavailable when its window has + // never been opened. The 24-hour requirement remains authoritative. + } + } + + private void recordQuest(QuestAPI.Quest quest, QuestMenuSource.Selector selector) { + if (quest == null || quest.getId() <= 0) return; + scannedQuestIds.add(quest.getId()); + if (!isVerifiedDaily(quest)) return; + if (isFinished(quest)) { + completedQuestIds.add(quest.getId()); + return; + } + boolean supported = false; + for (QuestAPI.Requirement requirement : DailyTaskPlanner.actionable(quest)) { + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + if (isNpcRequirement(requirement) || + DailyTaskPlanner.findMap(requirement.getDescription()).isPresent() || + type == QuestAPI.Requirement.RequirementType.SELL_ORE || + type == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX || + type == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX_TYPE || + type == QuestAPI.Requirement.RequirementType.SALVAGE || + type == QuestAPI.Requirement.RequirementType.COLLECT_LOOT || + type == QuestAPI.Requirement.RequirementType.CARGO || + type == QuestAPI.Requirement.RequirementType.COLLECT) { + supported = true; + break; + } + } + if (!supported) return; + QuestChoice choice = new QuestChoice(quest.getId(), safeTitle(quest), selector); + QuestChoice existing = dailyChoices.get(quest.getId()); + if (existing == null || (existing.selector() == null && selector != null)) { + dailyChoices.put(quest.getId(), choice); + } + } + + private boolean isVerifiedDaily(QuestAPI.Quest quest) { + return quest != null && (SPECIAL_DAILY_QUEST_IDS.contains(quest.getId()) || + acceptedOfferIds.contains(quest.getId()) || knownDailyQuestIds.contains(quest.getId()) || + DailyTaskPlanner.isDaily(quest)); + } + + private boolean isFinished(QuestAPI.Quest quest) { + return quest == null || quest.isCompleted() || DailyTaskPlanner.progress(quest) >= 0.9999d; + } + + private String scanSummary() { + return "Menü: " + scannedQuestIds.size() + " görev, " + dailyChoices.size() + " günlük"; + } + + private void buildPlanFromSource(QuestAPI.Quest quest) { + clearPlan(); + List requirements = DailyTaskPlanner.actionable(quest); + targetMapName = requirements.stream() + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::findMap) + .flatMap(Optional::stream) + .findFirst() + .orElse(null); + + targetNpcDescription = requirements.stream() + .filter(this::isNpcRequirement) + .map(QuestAPI.Requirement::getDescription) + .findFirst() + .orElse(null); + + if (targetMapName == null && targetNpcDescription != null) { + targetMapName = DailyTaskPlanner.preferredMapForNpc(targetNpcDescription, "1").orElse(null); + } + + oreToSell = requirements.stream() + .filter(r -> r.getRequirementType() == QuestAPI.Requirement.RequirementType.SELL_ORE) + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::findOre) + .flatMap(Optional::stream) + .findFirst() + .orElse(null); + + collectBonus = requirements.stream().anyMatch(r -> + r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX || + r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX_TYPE); + collectCargo = requirements.stream().anyMatch(r -> + r.getRequirementType() == QuestAPI.Requirement.RequirementType.SALVAGE || + r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT_LOOT || + r.getRequirementType() == QuestAPI.Requirement.RequirementType.CARGO || + r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT); + + if (oreToSell != null && !ores.canSellOres() && targetMapName == null) { + targetMapName = homeBaseMapName(); + } + } + + private void executePlan(QuestAPI.Quest quest, double progress) { + if (targetMapName != null && !isOnMap(targetMapName)) { + navigateTo(targetMapName); + updateProgressStatus(quest, progress, "Haritaya gidiliyor: " + targetMapName); + return; + } + + if (oreToSell != null) { + if (ores.canSellOres() && ores.getAmount(oreToSell) > 0) { + ores.sellOre(oreToSell); + updateProgressStatus(quest, progress, "Maden satılıyor: " + oreToSell.getName()); + return; + } + if (!ores.canSellOres()) { + String base = homeBaseMapName(); + if (!isOnMap(base)) navigateTo(base); + updateProgressStatus(quest, progress, "Maden satışı için üsse gidiliyor"); + return; + } + // When the requested raw ore is absent, collect cargo from a small + // NPC without changing LootCollector's saved NPC/box settings. + if (ores.getAmount(oreToSell) <= 0 && targetNpcDescription == null) { + targetNpcDescription = "Streuner"; + collectCargo = true; + } + } + + if ((collectBonus || collectCargo) && tryCollectRequiredBox()) { + updateProgressStatus(quest, progress, collectBonus ? "Bonus kutusu toplanıyor" : "NPC yükü toplanıyor"); + return; + } + + if (targetNpcDescription != null) { + npcCombat.tick(targetNpcDescription); + // Shared combat safety can occasionally skip its internal target + // scan for a tick. Never leave a daily hunter parked after it has + // reached the last route point: MovementAPI follows configured + // preferred zones, or chooses a random point when none exist. + if (attack.getTarget() == null && !movement.isMoving()) movement.moveRandom(); + updateProgressStatus(quest, progress, "NPC Kill and Collect: " + npcCombat.getStatus() + + " | Hedef: " + cleanedRequirement(targetNpcDescription)); + return; + } + + if (collectBonus || collectCargo) { + roamIfNeeded(); + updateProgressStatus(quest, progress, "Toplanacak kutu aranıyor"); + return; + } + + if (targetMapName != null && isOnMap(targetMapName)) { + updateProgressStatus(quest, progress, "Hedef haritaya ulaşıldı"); + return; + } + + pauseSafely("Desteklenmeyen günlük hedef: " + firstRequirement(quest)); + } + + private boolean runLocalSafety() { + double hp = hero.getHealth().hpPercent(); + if (hp >= settings.minimumHpPercent) return false; + attack.stopAttack(); + hero.setRunMode(); + entities.getPortals().stream() + .filter(Portal::isValid) + .min(Comparator.comparingDouble(hero::distanceTo)) + .ifPresent(portal -> movement.moveTo(portal)); + status = "Yerel güvenlik: HP %" + (int) Math.round(hp * 100d) + "; portal yanında bekleniyor"; + return true; + } + + private boolean ensureQuestStation() { + if (!isOnMap(QUEST_BASE_MAP)) { + if (!selectors.isEmpty() || state != State.DISCOVERING) prepareNewScan(); + if (questGui != null && questGui.isVisible()) questGui.setVisible(false); + navigateToQuestBase(); + return false; + } + + Optional station = findQuestStation(); + if (station.isEmpty()) { + attack.stopAttack(); + hero.setRoamMode(); + status = QUEST_BASE_MAP + " ana istasyon verisi bekleniyor"; + return false; + } + + double distance = hero.distanceTo(station.get()); + if (distance > QUEST_STATION_DISTANCE) { + attack.stopAttack(); + hero.setRoamMode(); + movement.moveTo(station.get()); + status = QUEST_BASE_MAP + " ana görev istasyonuna yaklaşılıyor: " + (int) Math.round(distance); + return false; + } + + if (movement.isMoving()) movement.stop(false); + return true; + } + + private Optional findQuestStation() { + Optional questGiver = entities.getStations().stream() + .filter(Station::isValid) + .filter(station -> station instanceof Station.QuestGiver) + .min(Comparator.comparingDouble(hero::distanceTo)); + if (questGiver.isPresent()) return questGiver; + + Optional homeBase = entities.getStations().stream() + .filter(Station::isValid) + .filter(station -> station instanceof Station.HomeBase) + .min(Comparator.comparingDouble(hero::distanceTo)); + if (homeBase.isPresent()) return homeBase; + + return entities.getStations().stream() + .filter(Station::isValid) + .min(Comparator.comparingDouble(hero::distanceTo)); + } + + private void navigateToQuestBase() { + Optional destination = starSystem.findMap(QUEST_BASE_MAP); + if (destination.isEmpty() || !starSystem.isAccessible(destination.get())) { + attack.stopAttack(); + status = "Görev merkezi harita verisi bekleniyor: " + QUEST_BASE_MAP; + return; + } + + Portal portal = starSystem.findNext(destination.get()); + if (portal == null || !portal.isValid()) { + // Map changes briefly clear the portal collection. This is a + // normal loading state, so wait for the next tick instead of + // entering a permanent safe pause. + attack.stopAttack(); + status = "Görev merkezi rotası yükleniyor: " + QUEST_BASE_MAP; + return; + } + + attack.stopAttack(); + hero.setRoamMode(); + if (hero.distanceTo(portal) > 220d) movement.moveTo(portal); + else movement.jumpPortal(portal); + status = "Görev merkezi için " + QUEST_BASE_MAP + " haritasına gidiliyor"; + } + + private void navigateTo(String mapName) { + Optional destination = starSystem.findMap(mapName); + if (destination.isEmpty() || !starSystem.isAccessible(destination.get())) { + // Map/route data can be temporarily unavailable while a portal jump is + // loading. Keep the daily plan intact and retry on the next tick. + attack.stopAttack(); + status = "Harita rotası yükleniyor: " + mapName; + return; + } + Portal portal = starSystem.findNext(destination.get()); + if (portal == null || !portal.isValid()) { + // The portal collection is briefly empty during map transitions. This + // is not a permanent navigation failure and must not clear the plan. + attack.stopAttack(); + status = "Sonraki portal bekleniyor: " + mapName; + return; + } + attack.stopAttack(); + hero.setRoamMode(); + if (hero.distanceTo(portal) > 220d) movement.moveTo(portal); + else movement.jumpPortal(portal); + } + + private boolean tryCollectRequiredBox() { + Optional nearest = entities.getBoxes().stream() + .filter(Box::isValid) + .filter(box -> !box.isCollected()) + .filter(this::matchesRequiredBox) + .min(Comparator.comparingDouble(hero::distanceTo)); + if (nearest.isEmpty()) return false; + Box box = nearest.get(); + attack.stopAttack(); + hero.setRoamMode(); + if (hero.distanceTo(box) > 180d) movement.moveTo(box); + else box.tryCollect(); + return true; + } + + private boolean matchesRequiredBox(Box box) { + String type = ((box.getTypeName() == null ? "" : box.getTypeName()) + " " + + (box.getHash() == null ? "" : box.getHash())).toUpperCase(Locale.ROOT); + if (collectBonus && type.contains("BONUS")) return true; + return collectCargo && (type.contains("FROM_SHIP") || type.contains("CARGO")); + } + + private void roamIfNeeded() { + long now = System.currentTimeMillis(); + if (movement.isMoving() && now < nextRoamAt) return; + movement.moveRandom(); + nextRoamAt = now + 2500L; + } + + private boolean isNpcRequirement(QuestAPI.Requirement requirement) { + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.KILL_NPC || + type == QuestAPI.Requirement.RequirementType.KILL_NPCS || + type == QuestAPI.Requirement.RequirementType.DAMAGE_NPCS || + type == QuestAPI.Requirement.RequirementType.DAMAGE; + } + + private boolean isOnMap(String mapName) { + GameMap current = starSystem.getCurrentMap(); + return current != null && current.getName().equalsIgnoreCase(mapName); + } + + private String homeBaseMapName() { + GameMap current = starSystem.getCurrentMap(); + String prefix = current == null ? "1" : current.getName().split("-")[0]; + if (!prefix.matches("[1-3]")) prefix = "1"; + return prefix + "-1"; + } + + private void updateProgressStatus(QuestAPI.Quest quest, double progress, String action) { + int percent = (int) Math.round(progress * 100d); + status = "Günlük görev | Tamamlanan: " + completedQuestIds.size() + + " | " + safeTitle(quest) + " | %" + percent + " | " + action; + } + + private void pauseSafely(String reason) { + stopCurrentAction(); + state = State.PAUSED; + status = "Güvenli duraklama: " + reason; + } + + private void finishAll() { + stopCurrentAction(); + state = State.DONE; + nextActionAt = java.time.LocalDate.now().plusDays(1) + .atStartOfDay(java.time.ZoneId.systemDefault()) + .toInstant().toEpochMilli(); + // Keep DarkBot ticking briefly so PetManager can send and observe the + // actual PET deactivation before the bot's running flag is cleared. + nextRoamAt = System.currentTimeMillis() + 5_000L; + status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + + " | Tamamlanan: " + completedQuestIds.size() + + " | PET kapatılıyor | Bot PET kapanınca duracak"; + } + + private void stopCurrentAction() { + npcCombat.stopCombat(); + attack.stopAttack(); + movement.stop(false); + hero.setRoamMode(); + clearPlan(); + } + + private void clearPlan() { + targetNpcDescription = null; + targetMapName = null; + oreToSell = null; + collectBonus = false; + collectCargo = false; + } + + String getNpcLocatorTargetDescription() { + if (state != State.RUNNING || targetNpcDescription == null) return null; + if (targetMapName != null && !isOnMap(targetMapName)) return null; + return targetNpcDescription; + } + + private String safeTitle(QuestAPI.Quest quest) { + String title = quest == null ? null : quest.getTitle(); + if (title == null || title.isBlank() || title.equalsIgnoreCase("ERROR")) { + return quest == null ? "Görev" : "Görev #" + quest.getId(); + } + return title; + } + + private String firstRequirement(QuestAPI.Quest quest) { + return DailyTaskPlanner.actionable(quest).stream() + .map(QuestAPI.Requirement::getDescription) + .findFirst() + .orElse("etkin hedef yok"); + } + + private String cleanedRequirement(String value) { + return value == null ? "" : value.replaceAll("\\s+", " ").trim(); + } + + @Override + public void onTickStopped() { + // DarkBot also invokes this callback during short disconnects and map-load + // gaps. Stop active movement/attack, but retain the selected quest and its + // plan so a slow server does not force another trip to the quest station. + npcCombat.stopCombat(); + attack.stopAttack(); + movement.stop(false); + hero.setRoamMode(); + if (state != State.PAUSED && state != State.DONE) { + status = "DarkBot beklemede; günlük görev planı korundu"; + } + } + + @Override + public String getStatus() { + return "DailyTaskAPI | " + status; + } + + @Override + public String getStoppedStatus() { + return "DailyTaskAPI durduruldu | " + status; + } + + @Override + public boolean canRefresh() { + return true; + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java new file mode 100644 index 00000000..5eae9a09 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java @@ -0,0 +1,26 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.config.annotations.Configuration; +import eu.darkbot.api.config.annotations.Number; + +@Configuration("dailytaskapi.config") +public final class DailyTaskConfig { + public boolean autoOpenQuestWindow = true; + + @Number(min = 250, max = 20000, step = 250) + public int questSwitchDelayMs = 500; + + @Number(min = 1, max = 10, step = 1) + public int maxSelectionRetries = 1; + + @Number(min = 1, max = 5, step = 1) + public int maxMenuScanPasses = 1; + + @Number(min = 300, max = 750, step = 25) + public int attackRadius = 500; + + @Number(min = 0.10, max = 0.90, step = 0.05) + public double minimumHpPercent = 0.30; + + public boolean stopWhenAllDailyTasksAreDone = true; +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java new file mode 100644 index 00000000..6a07b8d1 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -0,0 +1,228 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.managers.OreAPI; +import eu.darkbot.api.managers.QuestAPI; + +import java.text.Normalizer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +final class DailyTaskPlanner { + private static final Pattern MAP_PATTERN = Pattern.compile("(?i)([1-3]-BL|[1-5]-[1-8]|4-[1-5]|5-[1-3])"); + private static final double DAILY_DURATION_MS = 86_400_000d; + + private DailyTaskPlanner() { + } + + static boolean isDaily(QuestAPI.Quest quest) { + if (quest == null) return false; + for (QuestAPI.Requirement requirement : flatten(quest.getRequirements())) { + if (requirement.getRequirementType() == QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE) { + double goal = requirement.getGoal(); + return goal >= DAILY_DURATION_MS - 120_000d && goal <= DAILY_DURATION_MS + 120_000d; + } + } + return false; + } + + static boolean isDailyType(String type) { + if (type == null) return false; + String normalized = type.trim().toLowerCase(Locale.ROOT); + return normalized.startsWith("questtype_daily") || normalized.equals("daily"); + } + + static boolean isOnlyTetrathrinReward(Collection rewards) { + if (rewards == null || rewards.isEmpty()) return false; + boolean hasMaterialReward = false; + for (QuestAPI.Reward reward : rewards) { + if (reward == null || reward.getAmount() <= 0) continue; + String type = normalize(reward.getType()); + // Uridium is always valuable for this account. Accept the quest even + // when its only material reward is Tetrathrin. + if (type.contains("uridium")) return false; + if (type.startsWith("currency ")) continue; + hasMaterialReward = true; + if (!type.contains("tetrathrin")) return false; + } + return hasMaterialReward; + } + + static boolean hasUridiumReward(Collection rewards) { + if (rewards == null || rewards.isEmpty()) return false; + for (QuestAPI.Reward reward : rewards) { + if (reward != null && reward.getAmount() > 0 && normalize(reward.getType()).contains("uridium")) { + return true; + } + } + return false; + } + + static List actionable(QuestAPI.Quest quest) { + List result = new ArrayList<>(); + if (quest == null) return result; + for (QuestAPI.Requirement requirement : flatten(quest.getRequirements())) { + if (!requirement.isEnabled() || requirement.isCompleted()) continue; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + if (type == QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE || + type == QuestAPI.Requirement.RequirementType.HASTE || + type == QuestAPI.Requirement.RequirementType.TIMER || + type == QuestAPI.Requirement.RequirementType.COUNTDOWN) continue; + result.add(requirement); + } + return result; + } + + static List flatten(Collection roots) { + List result = new ArrayList<>(); + if (roots == null) return result; + for (QuestAPI.Requirement root : roots) flattenInto(root, result); + return result; + } + + private static void flattenInto(QuestAPI.Requirement requirement, List output) { + if (requirement == null) return; + // A requirement may be both actionable itself and contain structural + // children. DarkBot models quests such as "kill Mordon on 1-4" as a + // KILL_NPC parent with a MAP child, so retaining leaves only loses the + // NPC target and prevents both combat and the PET enemy locator. + output.add(requirement); + List children = requirement.getRequirements(); + if (children != null) { + for (QuestAPI.Requirement child : children) flattenInto(child, output); + } + } + + static Optional findNpc(String description, Collection npcNames) { + if (description == null || npcNames == null) return Optional.empty(); + String text = normalize(description); + boolean wantsBoss = text.contains("boss"); + boolean wantsUber = text.contains("uber"); + return npcNames.stream() + .map(name -> new NpcCandidate(name, normalizeNpcName(name))) + .filter(candidate -> !candidate.clean.isBlank() && text.contains(candidate.clean)) + .filter(candidate -> wantsBoss == candidate.clean.startsWith("boss ")) + .filter(candidate -> wantsUber == candidate.clean.startsWith("uber")) + .max(Comparator.comparingInt(candidate -> candidate.clean.length())) + .map(candidate -> candidate.original); + } + + static Optional findMap(String description) { + if (description == null) return Optional.empty(); + Matcher matcher = MAP_PATTERN.matcher(description); + return matcher.find() ? Optional.of(matcher.group(1).toUpperCase(Locale.ROOT)) : Optional.empty(); + } + + static Optional preferredMapForNpc(String description, String companyPrefix) { + if (description == null) return Optional.empty(); + String prefix = companyPrefix != null && companyPrefix.matches("[1-3]") ? companyPrefix : "1"; + String npc = normalizeNpcName(description); + + if (npc.contains("uber")) return Optional.of("4-5"); + if (description.contains("StreuneR")) return Optional.of(prefix + "-8"); + if (npc.contains("protecgit") || npc.contains("protegit") || npc.contains("cubikon")) { + return Optional.of(prefix + "-6"); + } + if (npc.contains("boss kristallon")) return Optional.of(prefix + "-7"); + if (npc.contains("boss lordakium")) return Optional.of(prefix + "-5"); + if (npc.contains("kristallin") || npc.contains("kristallon")) return Optional.of(prefix + "-6"); + // The account owner's requested Lordakium farming route is X-6. + if (npc.contains("lordakium")) return Optional.of(prefix + "-6"); + if (npc.contains("sibelonit")) return Optional.of(prefix + "-5"); + if (npc.contains("sibelon")) return Optional.of(prefix + "-4"); + if (npc.contains("saimon") || npc.contains("mordon") || npc.contains("devolarium")) { + return Optional.of(prefix + "-3"); + } + if (npc.contains("lordakia") || npc.contains("streuner")) return Optional.of(prefix + "-2"); + if (npc.contains("battleray")) return Optional.of("5-3"); + if (npc.contains("interceptor") || npc.contains("barracuda") || + npc.contains("saboteur") || npc.contains("annihilator")) return Optional.of("5-2"); + return Optional.empty(); + } + + static Optional findOre(String description) { + String text = normalize(description); + if (text.contains("prometium")) return Optional.of(OreAPI.Ore.PROMETIUM); + if (text.contains("endurium")) return Optional.of(OreAPI.Ore.ENDURIUM); + if (text.contains("terbium")) return Optional.of(OreAPI.Ore.TERBIUM); + if (text.contains("prometid")) return Optional.of(OreAPI.Ore.PROMETID); + if (text.contains("duranium")) return Optional.of(OreAPI.Ore.DURANIUM); + if (text.contains("promerium")) return Optional.of(OreAPI.Ore.PROMERIUM); + if (text.contains("seprom")) return Optional.of(OreAPI.Ore.SEPROM); + if (text.contains("palladium")) return Optional.of(OreAPI.Ore.PALLADIUM); + if (text.contains("osmium")) return Optional.of(OreAPI.Ore.OSMIUM); + if (text.contains("xenomit")) return Optional.of(OreAPI.Ore.XENOMIT); + return Optional.empty(); + } + + static String normalizeNpcName(String value) { + String normalized = normalize(value) + .replace('-', ' ') + .replaceAll("\\b(delta|alpha|beta|gamma)\\s*\\d+\\b", " ") + .replaceAll("\\b[a-z]\\d+\\b", " ") + .replaceAll("\\s+", " ") + .trim(); + return normalized; + } + + static String normalize(String value) { + if (value == null) return ""; + String ascii = Normalizer.normalize(value, Normalizer.Form.NFD) + .replaceAll("\\p{M}+", "") + .toLowerCase(Locale.ROOT) + .replace('ı', 'i') + .replaceAll("[^a-z0-9-]+", " ") + .replaceAll("\\s+", " ") + .trim(); + return ascii; + } + + static double progress(QuestAPI.Quest quest) { + List requirements = actionableIncludingCompleted(quest); + if (requirements.isEmpty()) return 0d; + double sum = 0d; + for (QuestAPI.Requirement requirement : requirements) { + double goal = requirement.getGoal(); + sum += goal <= 0d ? (requirement.isCompleted() ? 1d : 0d) + : Math.max(0d, Math.min(1d, requirement.getProgress() / goal)); + } + return sum / requirements.size(); + } + + private static List actionableIncludingCompleted(QuestAPI.Quest quest) { + List result = new ArrayList<>(); + if (quest == null) return result; + for (QuestAPI.Requirement requirement : flatten(quest.getRequirements())) { + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + if (type == QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE || + type == QuestAPI.Requirement.RequirementType.HASTE || + type == QuestAPI.Requirement.RequirementType.TIMER || + type == QuestAPI.Requirement.RequirementType.COUNTDOWN) continue; + result.add(requirement); + } + return result; + } + + private static final class NpcCandidate { + private final String original; + private final String clean; + + private NpcCandidate(String original, String clean) { + this.original = original; + this.clean = clean; + } + + private String original() { + return original; + } + + private String clean() { + return clean; + } + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/QuestGiverSource.java b/src/main/java/dev/shared/berke/dailytaskapi/QuestGiverSource.java new file mode 100644 index 00000000..2b857047 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/QuestGiverSource.java @@ -0,0 +1,73 @@ +package dev.shared.berke.dailytaskapi; + +import com.github.manolo8.darkbot.Main; +import eu.darkbot.api.game.other.Area; +import eu.darkbot.api.managers.GameScreenAPI; +import eu.darkbot.api.utils.NativeAction; + +/** Source-verified controls for DarkOrbit's quest-giver modal. */ +final class QuestGiverSource { + static final int MAX_ROWS = 6; + static final int MAX_SCROLL_PAGES = 10; + + private static final double WINDOW_WIDTH = 840d; + private static final double WINDOW_HEIGHT = 550d; + private static final double TABS_X = 8d; + private static final double TABS_Y = 35d; + private static final double TABS_WIDTH = 813d; + private static final double TABS_HEIGHT = 21d; + private static final int TAB_COUNT = 5; + private static final int DAILY_TAB_POSITION = 3; + private static final double LIST_X = 8d; + private static final double LIST_Y = 305d; + private static final double LIST_WIDTH = 238d; + private static final double LIST_HEIGHT = 206d; + private static final double ACCEPT_X = 668d; + private static final double ACCEPT_Y = 494d; + private static final double ACCEPT_WIDTH = 162d; + private static final double ACCEPT_HEIGHT = 21d; + + private final GameScreenAPI screen; + + QuestGiverSource(GameScreenAPI screen) { + this.screen = screen; + } + + void selectDailyTab() { + double tabWidth = TABS_WIDTH / TAB_COUNT; + click(TABS_X + tabWidth * DAILY_TAB_POSITION - tabWidth / 2d, + TABS_Y + TABS_HEIGHT / 2d); + } + + void scrollDailyListDown() { + Area.Rectangle viewport = screen.getViewBounds(); + double windowX = viewport.getWidth() / 2d - WINDOW_WIDTH / 2d; + double windowY = viewport.getHeight() / 2d - WINDOW_HEIGHT / 2d; + int x = (int) Math.round(windowX + LIST_X + LIST_WIDTH / 2d); + int y = (int) Math.round(windowY + LIST_Y + LIST_HEIGHT / 2d); + Main.API.postActions(NativeAction.MouseWheel.down(x, y)); + } + + void selectRow(int oneBasedRow) { + int row = Math.max(1, Math.min(MAX_ROWS, oneBasedRow)); + double rowHeight = LIST_HEIGHT / MAX_ROWS; + click(LIST_X + LIST_WIDTH / 2d, + LIST_Y + rowHeight * row - rowHeight / 2d); + } + + void acceptSelected() { + click(ACCEPT_X + ACCEPT_WIDTH / 2d, ACCEPT_Y + ACCEPT_HEIGHT / 2d); + } + + void close() { + Main.API.keyClick(27); + } + + private void click(double localX, double localY) { + Area.Rectangle viewport = screen.getViewBounds(); + double windowX = viewport.getWidth() / 2d - WINDOW_WIDTH / 2d; + double windowY = viewport.getHeight() / 2d - WINDOW_HEIGHT / 2d; + Main.API.mouseClick((int) Math.round(windowX + localX), + (int) Math.round(windowY + localY)); + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java b/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java new file mode 100644 index 00000000..b2b14eb0 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java @@ -0,0 +1,239 @@ +package dev.shared.berke.dailytaskapi; + +import com.github.manolo8.darkbot.Main; +import com.github.manolo8.darkbot.core.IDarkBotAPI; +import eu.darkbot.api.game.other.Gui; +import eu.darkbot.api.utils.NativeAction; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Discovers the active-quest selector row from DarkBot's native quest GUI. + * + *

No screenshot, OCR, quest title, fixed slot number or screen resolution is + * used. The selector row is recognized from the native sprite source: it lives + * in the quest header and contains evenly-spaced selector sprites. All quest + * identity and daily/non-daily decisions remain the responsibility of + * QuestAPI.

+ */ +final class QuestMenuSource { + static final class Selector { + private final int x; + private final int y; + + Selector(int x, int y) { + this.x = x; + this.y = y; + } + + int x() { + return x; + } + + int y() { + return y; + } + } + + private static final class Sprite { + private final long address; + private final int x; + private final int y; + private final List children; + + private Sprite(long address, int x, int y, List children) { + this.address = address; + this.x = x; + this.y = y; + this.children = children; + } + + private long address() { + return address; + } + + private int x() { + return x; + } + + private int y() { + return y; + } + + private List children() { + return children; + } + } + + private static final class Row { + private final int x; + private final int y; + private final int step; + private final List childXs; + private final int score; + + private Row(int x, int y, int step, List childXs, int score) { + this.x = x; + this.y = y; + this.step = step; + this.childXs = childXs; + this.score = score; + } + + private int x() { + return x; + } + + private int y() { + return y; + } + + private int step() { + return step; + } + + private List childXs() { + return childXs; + } + + private int score() { + return score; + } + } + + private final Gui questGui; + private final com.github.manolo8.darkbot.core.objects.Gui nativeQuestGui; + private final IDarkBotAPI memory; + private String lastError = ""; + + QuestMenuSource(Gui questGui) { + this.questGui = questGui; + this.nativeQuestGui = questGui instanceof com.github.manolo8.darkbot.core.objects.Gui + ? (com.github.manolo8.darkbot.core.objects.Gui) questGui : null; + this.memory = Main.API; + } + + List discoverSelectors() { + lastError = ""; + if (questGui == null) return fail("Görev penceresi bulunamadı"); + if (memory == null || nativeQuestGui == null) { + return fail("DarkBot görev kaynak ağacı kullanılamıyor"); + } + + try { + long rootAddress = nativeQuestGui.getAddress(); + if (rootAddress == 0L) return fail("Görev kaynak adresi henüz hazır değil"); + Sprite root = readSprite(rootAddress, 0, new HashSet<>()); + if (root == null) return fail("Görev kaynak ağacı okunamadı"); + + List rows = new ArrayList<>(); + // The root coordinate is the quest window's screen position. + // Remove it so candidates are evaluated in GUI-local space; + // window position, animation and resolution cannot affect discovery. + findRows(root, -root.x(), -root.y(), 0, rows); + Row best = rows.stream().max(Comparator.comparingInt(Row::score)).orElse(null); + if (best == null) return fail("Görev seçici satırı kaynak ağacında bulunamadı"); + + List selectors = new ArrayList<>(); + for (int childX : best.childXs()) { + // These compact quest tabs only expose a hitbox near their + // top-left edge. The visual centre is outside the clickable + // area in the current client, so use a one-pixel inset. + selectors.add(new Selector(best.x() + childX + 1, best.y() + 1)); + } + return List.copyOf(selectors); + } catch (Throwable error) { + return fail("Görev kaynak ağacı hatası: " + error.getClass().getSimpleName()); + } + } + + String getLastError() { + return lastError; + } + + void scrollSelectorsDown() { + List current = discoverSelectors(); + if (current.isEmpty()) return; + Selector middle = current.get(current.size() / 2); + int screenX = (int) Math.round(questGui.getX() + middle.x()); + int screenY = (int) Math.round(questGui.getY() + middle.y()); + Main.API.postActions(NativeAction.MouseWheel.down(screenX, screenY)); + } + + private Sprite readSprite(long address, int depth, Set visited) { + if (address == 0L || depth > 7 || visited.size() > 600 || !visited.add(address)) return null; + List childAddresses = new ArrayList<>(); + try { + nativeQuestGui.forEachSpriteChild(address, childAddresses::add); + } catch (Throwable error) { + lastError = "Görev kaynak çocuğu okunamadı: " + describe(error); + } + + List children = new ArrayList<>(); + for (long childAddress : childAddresses) { + Sprite child = readSprite(childAddress, depth + 1, visited); + if (child != null) children.add(child); + } + return new Sprite(address, coordinate(address, 88L), coordinate(address, 92L), List.copyOf(children)); + } + + private int coordinate(long address, long offset) { + try { + long matrix = memory.readLong(address + 72L); + int raw = memory.readInt(matrix + offset); + return Math.round(raw * 0.05f); + } catch (Throwable ignored) { + return 0; + } + } + + private void findRows(Sprite sprite, int parentX, int parentY, int depth, List rows) { + int absoluteX = parentX + sprite.x(); + int absoluteY = parentY + sprite.y(); + Row candidate = rowCandidate(sprite, absoluteX, absoluteY, depth); + if (candidate != null) rows.add(candidate); + for (Sprite child : sprite.children()) findRows(child, absoluteX, absoluteY, depth + 1, rows); + } + + private Row rowCandidate(Sprite sprite, int absoluteX, int absoluteY, int depth) { + List children = sprite.children(); + if (children.size() < 2 || children.size() > 24 || depth < 2) return null; + if (absoluteY < 0 || absoluteY > 25 || absoluteX < 100) return null; + + List xs = children.stream().map(Sprite::x).sorted().collect(Collectors.toList()); + int minY = children.stream().mapToInt(Sprite::y).min().orElse(0); + int maxY = children.stream().mapToInt(Sprite::y).max().orElse(0); + if (maxY - minY > 2) return null; + + List differences = new ArrayList<>(); + for (int index = 1; index < xs.size(); index++) differences.add(xs.get(index) - xs.get(index - 1)); + int step = median(differences); + if (step < 8 || step > 14) return null; + if (differences.stream().anyMatch(value -> Math.abs(value - step) > 2)) return null; + + int score = children.size() * 100 + Math.min(absoluteX, 500) - absoluteY * 2; + return new Row(absoluteX, absoluteY, step, xs, score); + } + + private int median(List values) { + if (values.isEmpty()) return 0; + List sorted = values.stream().sorted().collect(Collectors.toList()); + return sorted.get(sorted.size() / 2); + } + + private List fail(String message) { + if (lastError == null || lastError.isBlank()) lastError = message; + return List.of(); + } + + private String describe(Throwable error) { + String message = error == null ? null : error.getMessage(); + return error == null ? "bilinmeyen hata" : error.getClass().getSimpleName() + + (message == null || message.isBlank() ? "" : ": " + message); + } +} From a9fe9cb33140e94c18275d3419bb69c2cc8f5477 Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Sun, 2 Aug 2026 02:16:45 +0300 Subject: [PATCH 02/14] Add DailyTaskAPI planner test Cover daily detection, nested requirements, NPC and map matching, reward filtering, and Uridium acceptance. --- .../dailytaskapi/DailyTaskPlannerTest.java | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java new file mode 100644 index 00000000..14e4bd40 --- /dev/null +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java @@ -0,0 +1,130 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.managers.OreAPI; +import eu.darkbot.api.managers.QuestAPI; + +import java.util.List; + +public final class DailyTaskPlannerTest { + public static void main(String[] args) { + QuestAPI.Requirement dailyTimer = requirement("Bu görevi 1 gün içinde bitir", "REAL_TIME_HASTE", + QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE, 10, 86_400_000, false); + QuestAPI.Requirement kill = requirement("Lordakium imha et.", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 2, 5, false); + QuestAPI.Quest daily = quest(1, true, false, "Galaksi Politikaları", List.of(kill, dailyTimer)); + require(DailyTaskPlanner.isDaily(daily), "24 saatlik görev günlük tanınmalı"); + + QuestAPI.Quest normal = quest(2, true, false, "Normal", List.of(kill)); + require(!DailyTaskPlanner.isDaily(normal), "süresiz görev günlük sayılmamalı"); + QuestAPI.Quest dailyOffer = quest(3, false, false, "Teklif", List.of(kill, dailyTimer)); + require(DailyTaskPlanner.isDaily(dailyOffer), + "henüz aktif olmayan istasyon teklifi 24 saat şartından günlük tanınmalı"); + + QuestAPI.Requirement mapChild = requirement("1-4 haritasında", "MAP", + QuestAPI.Requirement.RequirementType.MAP, 0, 0, false); + QuestAPI.Requirement nestedMordon = requirement("Mordon imha et.", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 8, false, List.of(mapChild)); + QuestAPI.Quest nestedNpcQuest = quest(4, true, false, "Hedef: Biliniyor", + List.of(nestedMordon, dailyTimer)); + require(DailyTaskPlanner.actionable(nestedNpcQuest).contains(nestedMordon), + "altında harita şartı bulunan NPC öldürme hedefi kaybolmamalı"); + require(DailyTaskPlanner.actionable(nestedNpcQuest).contains(mapChild), + "NPC hedefinin altındaki harita şartı da korunmalı"); + + require(DailyTaskPlanner.isDailyType("questType_daily1"), + "quest catalog daily type should be recognized"); + require(!DailyTaskPlanner.isDailyType("questType_kill"), + "normal quest catalog type must not be daily"); + + List names = List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..", "( UberLordakium )"); + require(DailyTaskPlanner.findNpc("Mordon imha et.", List.of("-=[ Mordon ]=-")).isPresent(), + "decorated Mordon name should match the quest target"); + require(DailyTaskPlanner.findNpc("Lordakium imha et.", names).orElseThrow().equals("-=[ Lordakium ]=-"), + "normal Lordakium seçilmeli"); + require(DailyTaskPlanner.findNpc("Boss Lordakium imha et.", names).orElseThrow().contains("Boss Lordakium"), + "Boss Lordakium seçilmeli"); + require(DailyTaskPlanner.findNpc("Boss Sibelonit imha et.", List.of("-=[ Sibelonit ]=-")).isEmpty(), + "Boss isteniyorsa normal NPC kesinlikle seçilmemeli"); + require(DailyTaskPlanner.findNpc("UberLordakium imha et.", + List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..")).isEmpty(), + "Uber isteniyorsa normal veya Boss NPC seçilmemeli"); + require(DailyTaskPlanner.findMap("1-6 haritasına git").orElseThrow().equals("1-6"), + "harita ayrıştırılmalı"); + require(DailyTaskPlanner.preferredMapForNpc("Saimon imha et.", "1").orElseThrow().equals("1-3"), + "Saimon MMO X-3 haritasına yönlendirilmeli"); + require(DailyTaskPlanner.preferredMapForNpc("Lordakium imha et.", "1").orElseThrow().equals("1-6"), + "hesap sahibinin Lordakium X-6 tercihi korunmalı"); + require(DailyTaskPlanner.preferredMapForNpc("Boss Lordakium imha et.", "1").orElseThrow().equals("1-5"), + "Boss Lordakium MMO X-5 haritasına yönlendirilmeli"); + require(DailyTaskPlanner.preferredMapForNpc("UberKristallin imha et.", "1").orElseThrow().equals("4-5"), + "Uber NPC 4-5 haritasına yönlendirilmeli"); + require(DailyTaskPlanner.preferredMapForNpc("StreuneR imha et.", "1").orElseThrow().equals("1-8"), + "StreuneR X-8 haritasına yönlendirilmeli"); + require(DailyTaskPlanner.findOre("Prometium sat.").orElseThrow() == OreAPI.Ore.PROMETIUM, + "Prometium ayrıştırılmalı"); + require(DailyTaskPlanner.isOnlyTetrathrinReward(List.of( + reward("currency_experience", 1000), reward("currency_credits", 1000), + reward("resource_tetrathrin", 10))), + "currency rewards plus only Tetrathrin must be skipped"); + require(!DailyTaskPlanner.isOnlyTetrathrinReward(List.of( + reward("resource_tetrathrin", 10), reward("ammunition_laser_ucb-100", 5000))), + "a quest with Tetrathrin and another special reward must be accepted"); + require(!DailyTaskPlanner.isOnlyTetrathrinReward(List.of( + reward("resource_tetrathrin", 10), reward("currency_uridium", 1500))), + "a quest containing Uridium must be accepted even with Tetrathrin"); + require(!DailyTaskPlanner.isOnlyTetrathrinReward(List.of(reward("currency_experience", 1000))), + "currency-only rewards are not a Tetrathrin variant"); + require(DailyTaskPlanner.hasUridiumReward(List.of( + reward("currency_experience", 1000), reward("currency_uridium", 1500))), + "Uridium reward must be detected from the source reward type"); + require(!DailyTaskPlanner.hasUridiumReward(List.of(reward("resource_tetrathrin", 10))), + "a reward list without Uridium must be rejected"); + System.out.println("DailyTaskPlannerTest: OK"); + } + + private static QuestAPI.Reward reward(String type, int amount) { + return new QuestAPI.Reward() { + public int getAmount() { return amount; } + public String getType() { return type; } + }; + } + + private static QuestAPI.Requirement requirement(String description, String type, + QuestAPI.Requirement.RequirementType requirementType, + double progress, double goal, boolean completed) { + return requirement(description, type, requirementType, progress, goal, completed, List.of()); + } + + private static QuestAPI.Requirement requirement(String description, String type, + QuestAPI.Requirement.RequirementType requirementType, + double progress, double goal, boolean completed, + List children) { + return new QuestAPI.Requirement() { + public String getDescription() { return description; } + public double getProgress() { return progress; } + public double getGoal() { return goal; } + public boolean isCompleted() { return completed; } + public String getType() { return type; } + public List getRequirements() { return children; } + public boolean isEnabled() { return true; } + public QuestAPI.Requirement.RequirementType getRequirementType() { return requirementType; } + }; + } + + private static QuestAPI.Quest quest(int id, boolean active, boolean completed, String title, + List requirements) { + return new QuestAPI.Quest() { + public int getId() { return id; } + public boolean isActive() { return active; } + public String getTitle() { return title; } + public String getDescription() { return ""; } + public boolean isCompleted() { return completed; } + public List getRequirements() { return requirements; } + public List getRewards() { return List.of(); } + }; + } + + private static void require(boolean condition, String message) { + if (!condition) throw new AssertionError(message); + } +} From 362ce3bb0a972de2dfa33a0f3ef0182125687591 Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Sun, 2 Aug 2026 02:18:23 +0300 Subject: [PATCH 03/14] Register DailyTaskAPI features --- src/main/resources/plugin.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index d25be11b..6a8e3352 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -1,7 +1,7 @@ { "name": "SharedPlugin", "author": "Several Devs", - "version": "0.12.20", + "version": "0.12.21", "minVersion": "1.131.6 beta 3", "supportedVersion": "1.132.0 alpha 2", "basePackage": "dev.shared", @@ -19,8 +19,10 @@ "dev.shared.orbithelper.behaviours.fast_travel.FastTravel", "dev.shared.do_gamer.module.simple_galaxy_gate.SimpleGalaxyGate", "dev.shared.do_gamer.task.autobuy.Autobuy", - "dev.shared.halizeur.log_overlay.LogOverlay" + "dev.shared.halizeur.log_overlay.LogOverlay", + "dev.shared.berke.dailytaskapi.DailyTaskAPI", + "dev.shared.berke.dailytaskapi.DailyNpcPetLocator" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar" -} \ No newline at end of file +} From d3d61c8c9ef0ae2e30728183e0d7bb4b2b51d198 Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Sun, 2 Aug 2026 02:20:28 +0300 Subject: [PATCH 04/14] Document DailyTaskAPI feature Describe daily quest automation, Uridium filtering, PET shutdown, and configuration isolation. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7759f47a..0bc63f61 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ We will seek to implement CI/CD in the repository so that developers have a good - Simple Galaxy Gate: Supports ABG, Delta, Epsilon, Zeta, Hades, Kuiper, LoW, Invasion, Mimesis, EBG, GoP, Treacherous, DSE, Trinity and Voyagers. By @do-gamer - Autobuy: Automatically buys boosters and special items from the shop at a configured interval. By @do-gamer - Log Overlay: Displays the latest in-game log messages on the canvas (top-center, auto-fade after 5s). Built-in keyword whitelist limits the displayed lines to gains and errors so the canvas does not get cluttered. By @Halizeur +- Daily Task API: Detects active daily quests from DarkBot's quest data, completes supported objectives, accepts eligible Uridium-reward dailies at the home station, disables the PET when finished, and stops the bot without changing another module's saved configuration. By @Berke ## Contributing From 3ac9fe452ecd438ae3f18d88aa3da61b1134f055 Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Tue, 11 Aug 2026 23:03:53 +0300 Subject: [PATCH 05/14] Update DailyTaskAPI and add RAM cleaner --- .../berke/dailytaskapi/DailyNpcCombat.java | 4 + .../DailyQuestConditionEngine.java | 230 ++++++++++++++++++ .../berke/dailytaskapi/DailyRamCleaner.java | 99 ++++++++ .../dailytaskapi/DailyRamCleanerConfig.java | 23 ++ .../dailytaskapi/DailyRamCleanerPolicy.java | 13 + .../berke/dailytaskapi/DailyTaskAPI.java | 147 ++++++----- .../berke/dailytaskapi/DailyTaskConfig.java | 3 + .../berke/dailytaskapi/DailyTaskPlanner.java | 8 - .../berke/dailytaskapi/QuestMenuSource.java | 6 +- 9 files changed, 445 insertions(+), 88 deletions(-) create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java index 2fb63406..4e409cd3 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java @@ -38,6 +38,10 @@ void stopCombat() { if (pet.isEnabled()) pet.setEnabled(false); } + boolean isPetActive() { + return pet.isActive(); + } + @Override protected boolean checkMap() { // DailyTaskAPI resolves and navigates to the quest's required map. diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java new file mode 100644 index 00000000..f2418431 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java @@ -0,0 +1,230 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.game.other.Locatable; +import eu.darkbot.api.managers.OreAPI; +import eu.darkbot.api.managers.QuestAPI; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Builds an executable daily-quest plan from DarkBot's typed QuestAPI source. + * + *

Quest discovery stays in {@link DailyTaskAPI}, while this class translates + * enabled, unfinished requirements into map, coordinate, NPC, collection and + * ore actions. It has no dependency on DmPlugin and does not alter another + * module's saved configuration.

+ */ +final class DailyQuestConditionEngine { + private static final Pattern LABELED_COORDINATES = Pattern.compile( + "(?i)(?:x|x-koordinat(?:i|ı)?)\\s*[:=]?\\s*(-?\\d{1,5})\\D{1,24}" + + "(?:y|y-koordinat(?:i|ı)?)\\s*[:=]?\\s*(-?\\d{1,5})"); + private static final Pattern PAIRED_COORDINATES = Pattern.compile( + "(? requirements = DailyTaskPlanner.actionable(quest); + + String targetMap = requirements.stream() + .filter(DailyQuestConditionEngine::isMapCondition) + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::findMap) + .flatMap(Optional::stream) + .findFirst() + .orElseGet(() -> requirements.stream() + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::findMap) + .flatMap(Optional::stream) + .findFirst() + .orElse(null)); + + String targetNpc = requirements.stream() + .filter(DailyQuestConditionEngine::isNpcCondition) + .map(QuestAPI.Requirement::getDescription) + .filter(description -> description != null && !description.isBlank()) + .findFirst() + .orElse(null); + if (targetMap == null && targetNpc != null) { + targetMap = DailyTaskPlanner.preferredMapForNpc(targetNpc, companyPrefix).orElse(null); + } + + OreAPI.Ore ore = requirements.stream() + .filter(requirement -> requirement.getRequirementType() == + QuestAPI.Requirement.RequirementType.SELL_ORE) + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::findOre) + .flatMap(Optional::stream) + .findFirst() + .orElse(null); + + boolean collectBonus = requirements.stream().anyMatch(DailyQuestConditionEngine::isBonusCondition); + boolean collectCargo = requirements.stream().anyMatch(DailyQuestConditionEngine::isCargoCondition); + + Locatable coordinates = requirements.stream() + .filter(DailyQuestConditionEngine::isCoordinateCondition) + .map(QuestAPI.Requirement::getDescription) + .map(DailyQuestConditionEngine::findCoordinates) + .flatMap(Optional::stream) + .findFirst() + .orElse(null); + + String primary = requirements.stream() + .filter(DailyQuestConditionEngine::isExecutableCondition) + .map(QuestAPI.Requirement::getDescription) + .filter(description -> description != null && !description.isBlank()) + .findFirst() + .orElseGet(() -> requirements.stream() + .map(QuestAPI.Requirement::getDescription) + .filter(description -> description != null && !description.isBlank()) + .findFirst() + .orElse("etkin hedef yok")); + + return new Plan(targetMap, targetNpc, ore, collectBonus, collectCargo, coordinates, primary); + } + + static boolean supports(QuestAPI.Quest quest) { + return build(quest, "1").executable(); + } + + static boolean isNpcCondition(QuestAPI.Requirement requirement) { + if (requirement == null) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.KILL_NPC || + type == QuestAPI.Requirement.RequirementType.KILL_NPCS || + type == QuestAPI.Requirement.RequirementType.DAMAGE_NPCS || + type == QuestAPI.Requirement.RequirementType.DAMAGE || + type == QuestAPI.Requirement.RequirementType.RESTRICT_AMMUNITION_KILL_NPC; + } + + private static boolean isMapCondition(QuestAPI.Requirement requirement) { + if (requirement == null) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.MAP || + type == QuestAPI.Requirement.RequirementType.MAP_DIVERSE || + type == QuestAPI.Requirement.RequirementType.TRAVEL || + type == QuestAPI.Requirement.RequirementType.VISIT_MAP || + type == QuestAPI.Requirement.RequirementType.VISIT_MULTIPLE_MAPS || + type == QuestAPI.Requirement.RequirementType.VISIT_JUMP_GATE_TO_MAP_TYPE || + type == QuestAPI.Requirement.RequirementType.VISIT_MAP_ASSET || + type == QuestAPI.Requirement.RequirementType.VISIT_DISRUPTION_ZONE; + } + + private static boolean isCoordinateCondition(QuestAPI.Requirement requirement) { + if (requirement == null) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.COORDINATES || + type == QuestAPI.Requirement.RequirementType.PROXIMITY || + type == QuestAPI.Requirement.RequirementType.DISTANCE; + } + + private static boolean isBonusCondition(QuestAPI.Requirement requirement) { + if (requirement == null) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX || + type == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX_TYPE; + } + + private static boolean isCargoCondition(QuestAPI.Requirement requirement) { + if (requirement == null) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.SALVAGE || + type == QuestAPI.Requirement.RequirementType.COLLECT_LOOT || + type == QuestAPI.Requirement.RequirementType.CARGO || + type == QuestAPI.Requirement.RequirementType.COLLECT; + } + + private static boolean isExecutableCondition(QuestAPI.Requirement requirement) { + return isNpcCondition(requirement) || isMapCondition(requirement) || + isCoordinateCondition(requirement) || isBonusCondition(requirement) || + isCargoCondition(requirement) || + (requirement != null && requirement.getRequirementType() == + QuestAPI.Requirement.RequirementType.SELL_ORE); + } + + static Optional findCoordinates(String description) { + if (description == null || description.isBlank()) return Optional.empty(); + String normalized = description.toLowerCase(Locale.ROOT); + Matcher labeled = LABELED_COORDINATES.matcher(normalized); + if (labeled.find()) return coordinates(labeled.group(1), labeled.group(2)); + Matcher paired = PAIRED_COORDINATES.matcher(normalized); + if (paired.find()) return coordinates(paired.group(1), paired.group(2)); + return Optional.empty(); + } + + private static Optional coordinates(String rawX, String rawY) { + try { + double x = Double.parseDouble(rawX); + double y = Double.parseDouble(rawY); + if (!Double.isFinite(x) || !Double.isFinite(y)) return Optional.empty(); + return Optional.of(Locatable.of(x, y)); + } catch (NumberFormatException ignored) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java new file mode 100644 index 00000000..684a176e --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java @@ -0,0 +1,99 @@ +package dev.shared.berke.dailytaskapi; + +import com.github.manolo8.darkbot.Main; +import com.github.manolo8.darkbot.core.IDarkBotAPI; +import com.github.manolo8.darkbot.core.api.Capability; +import eu.darkbot.api.PluginAPI; +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.extensions.Behavior; +import eu.darkbot.api.extensions.Configurable; +import eu.darkbot.api.extensions.Feature; +import eu.darkbot.api.managers.AttackAPI; +import eu.darkbot.api.managers.HeroAPI; + +/** + * Threshold-based game-client working-set cleaner. + * + *

The implementation uses the same RAM operation exposed by DarkBot's own + * API settings screen. It does not terminate processes, edit JVM arguments or + * delete files.

+ */ +@Feature(name = "BerkePlugin RAM Cleaner", + description = "Bellek eşiği aşıldığında DarkBot oyun istemcisinin RAM kullanımını azaltır", + enabledByDefault = true) +public final class DailyRamCleaner implements Behavior, Configurable { + private static final long MIN_RETRY_AFTER_COMBAT_MS = 5_000L; + + private final IDarkBotAPI darkbotApi; + private final HeroAPI hero; + private final AttackAPI attack; + + private DailyRamCleanerConfig config = new DailyRamCleanerConfig(); + private long nextCheckAt; + + public DailyRamCleaner(PluginAPI api) { + this.darkbotApi = Main.API; + this.hero = api.requireAPI(HeroAPI.class); + this.attack = api.requireAPI(AttackAPI.class); + } + + @Override + public void setConfig(ConfigSetting setting) { + DailyRamCleanerConfig configured = setting.getValue(); + if (configured != null) config = configured; + setting.addListener(updated -> { + if (updated != null) config = updated; + nextCheckAt = 0L; + }); + } + + @Override + public void onTickBehavior() { + DailyRamCleanerConfig current = config; + if (current == null || !current.enabled) { + nextCheckAt = 0L; + return; + } + + long now = System.currentTimeMillis(); + if (now < nextCheckAt) return; + + long checkIntervalMs = Math.max(15, current.checkIntervalSeconds) * 1_000L; + nextCheckAt = now + checkIntervalMs; + + long memoryUsageMb; + try { + memoryUsageMb = darkbotApi.getMemoryUsage(); + } catch (RuntimeException unavailable) { + return; + } + + boolean inCombat = attack.isAttacking() + || attack.hasTarget() + || hero.getLocalTarget() != null; + if (!DailyRamCleanerPolicy.shouldClean(memoryUsageMb, + current.ramThresholdMb, current.avoidCombat, inCombat)) { + if (current.avoidCombat && inCombat && memoryUsageMb >= current.ramThresholdMb) { + nextCheckAt = Math.min(nextCheckAt, now + MIN_RETRY_AFTER_COMBAT_MS); + } + return; + } + + try { + if (current.clearGameCache + && darkbotApi.hasCapability(Capability.HANDLER_CLEAR_CACHE)) { + darkbotApi.clearCache(".*"); + } + + System.gc(); + if (darkbotApi.hasCapability(Capability.HANDLER_CLEAR_RAM)) { + darkbotApi.emptyWorkingSet(); + } + System.out.println("[BerkePlugin RAM Cleaner] Temizleme tamamlandı: " + + memoryUsageMb + " MB"); + } catch (RuntimeException cleanupFailure) { + System.err.println("[BerkePlugin RAM Cleaner] Temizleme başarısız: " + + cleanupFailure.getMessage()); + } + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java new file mode 100644 index 00000000..37c8dd9c --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java @@ -0,0 +1,23 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.config.annotations.Configuration; +import eu.darkbot.api.config.annotations.Number; + +@Configuration("dailytaskapi.ram_cleaner") +public final class DailyRamCleanerConfig { + public boolean enabled = true; + + @Number(min = 512, max = 16384, step = 128) + public int ramThresholdMb = 1200; + + @Number(min = 15, max = 600, step = 15) + public int checkIntervalSeconds = 30; + + public boolean avoidCombat = true; + + /** + * Clearing the game cache can cause assets to be loaded again. Keep this + * disabled unless ordinary working-set trimming is insufficient. + */ + public boolean clearGameCache = false; +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java new file mode 100644 index 00000000..d9b913f6 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java @@ -0,0 +1,13 @@ +package dev.shared.berke.dailytaskapi; + +/** Pure decision logic kept separate so RAM cleanup rules can be unit tested. */ +final class DailyRamCleanerPolicy { + private DailyRamCleanerPolicy() { + } + + static boolean shouldClean(long memoryUsageMb, int thresholdMb, + boolean avoidCombat, boolean inCombat) { + if (memoryUsageMb < thresholdMb) return false; + return !avoidCombat || !inCombat; + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java index da2c1e85..6c11ae20 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -10,6 +10,7 @@ import eu.darkbot.api.game.entities.Station; import eu.darkbot.api.game.other.GameMap; import eu.darkbot.api.game.other.Gui; +import eu.darkbot.api.game.other.Locatable; import eu.darkbot.api.managers.AttackAPI; import eu.darkbot.api.managers.BotAPI; import eu.darkbot.api.managers.EntitiesAPI; @@ -108,6 +109,9 @@ private QuestMenuSource.Selector selector() { private OreAPI.Ore oreToSell; private boolean collectBonus; private boolean collectCargo; + private Locatable targetCoordinates; + private double lastTrackedProgress = -1d; + private long lastProgressAt; private final Set completedQuestIds = new HashSet<>(); private final Set knownDailyQuestIds = new HashSet<>(); private final Set scannedQuestIds = new LinkedHashSet<>(); @@ -163,25 +167,7 @@ public void onTickModule() { // After midnight a deliberate new start begins a fresh daily run. if (nextActionAt > now) { npcCombat.stopCombat(); - boolean petStillActive = true; - try { - java.lang.reflect.Field petField = null; - for (Class type = npcCombat.getClass(); type != null; type = type.getSuperclass()) { - try { - petField = type.getDeclaredField("pet"); - break; - } catch (NoSuchFieldException ignored) { - // Continue through the exact runtime class hierarchy. - } - } - if (petField != null) { - petField.setAccessible(true); - petStillActive = ((eu.darkbot.api.managers.PetAPI) petField.get(npcCombat)).isActive(); - } - } catch (Throwable ignored) { - // Do not stop before PET shutdown can be confirmed. - } - if (petStillActive || now < nextRoamAt) { + if (npcCombat.isPetActive() || now < nextRoamAt) { status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + " | PET kapatılıyor | Bot PET kapanınca duracak"; return; @@ -275,6 +261,7 @@ public void onTickModule() { } buildPlanFromSource(quest); + if (recoverStagnantPlan(quest, progress, now)) return; boolean standardNpcCombatOwnsSafety = targetNpcDescription != null && (targetMapName == null || isOnMap(targetMapName)); if (!standardNpcCombatOwnsSafety && runLocalSafety()) return; @@ -308,6 +295,7 @@ private void resetSession() { seenGiverOfferIds.clear(); acceptedOfferIds.clear(); skippedTetrathrinOfferIds.clear(); + resetProgressTracking(); clearPlan(); status = "Görev menüsü kaynaktan taranıyor"; } @@ -714,7 +702,7 @@ private Optional findQuestItem(int id) { .filter(item -> item != null && item.getId() == id) .findFirst() .map(item -> (QuestAPI.QuestListItem) item); - } catch (Throwable ignored) { + } catch (RuntimeException ignored) { return Optional.empty(); } } @@ -768,6 +756,7 @@ private void startDaily(QuestAPI.Quest quest) { currentQuestId = quest.getId(); currentQuestTitle = safeTitle(quest); selectionRetries = 0; + resetProgressTracking(); state = State.RUNNING; buildPlanFromSource(quest); status = "Günlük görev doğrulandı: " + currentQuestTitle; @@ -813,7 +802,7 @@ private void refreshDailyCatalog() { knownDailyQuestIds.add(item.getId()); } } - } catch (Throwable ignored) { + } catch (RuntimeException ignored) { // The quest-giver catalog can be unavailable when its window has // never been opened. The 24-hour requirement remains authoritative. } @@ -827,23 +816,7 @@ private void recordQuest(QuestAPI.Quest quest, QuestMenuSource.Selector selector completedQuestIds.add(quest.getId()); return; } - boolean supported = false; - for (QuestAPI.Requirement requirement : DailyTaskPlanner.actionable(quest)) { - QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); - if (isNpcRequirement(requirement) || - DailyTaskPlanner.findMap(requirement.getDescription()).isPresent() || - type == QuestAPI.Requirement.RequirementType.SELL_ORE || - type == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX || - type == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX_TYPE || - type == QuestAPI.Requirement.RequirementType.SALVAGE || - type == QuestAPI.Requirement.RequirementType.COLLECT_LOOT || - type == QuestAPI.Requirement.RequirementType.CARGO || - type == QuestAPI.Requirement.RequirementType.COLLECT) { - supported = true; - break; - } - } - if (!supported) return; + if (!DailyQuestConditionEngine.supports(quest)) return; QuestChoice choice = new QuestChoice(quest.getId(), safeTitle(quest), selector); QuestChoice existing = dailyChoices.get(quest.getId()); if (existing == null || (existing.selector() == null && selector != null)) { @@ -867,40 +840,13 @@ private String scanSummary() { private void buildPlanFromSource(QuestAPI.Quest quest) { clearPlan(); - List requirements = DailyTaskPlanner.actionable(quest); - targetMapName = requirements.stream() - .map(QuestAPI.Requirement::getDescription) - .map(DailyTaskPlanner::findMap) - .flatMap(Optional::stream) - .findFirst() - .orElse(null); - - targetNpcDescription = requirements.stream() - .filter(this::isNpcRequirement) - .map(QuestAPI.Requirement::getDescription) - .findFirst() - .orElse(null); - - if (targetMapName == null && targetNpcDescription != null) { - targetMapName = DailyTaskPlanner.preferredMapForNpc(targetNpcDescription, "1").orElse(null); - } - - oreToSell = requirements.stream() - .filter(r -> r.getRequirementType() == QuestAPI.Requirement.RequirementType.SELL_ORE) - .map(QuestAPI.Requirement::getDescription) - .map(DailyTaskPlanner::findOre) - .flatMap(Optional::stream) - .findFirst() - .orElse(null); - - collectBonus = requirements.stream().anyMatch(r -> - r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX || - r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT_BONUS_BOX_TYPE); - collectCargo = requirements.stream().anyMatch(r -> - r.getRequirementType() == QuestAPI.Requirement.RequirementType.SALVAGE || - r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT_LOOT || - r.getRequirementType() == QuestAPI.Requirement.RequirementType.CARGO || - r.getRequirementType() == QuestAPI.Requirement.RequirementType.COLLECT); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, companyPrefix()); + targetMapName = plan.targetMapName(); + targetNpcDescription = plan.targetNpcDescription(); + oreToSell = plan.oreToSell(); + collectBonus = plan.collectBonus(); + collectCargo = plan.collectCargo(); + targetCoordinates = plan.targetCoordinates(); if (oreToSell != null && !ores.canSellOres() && targetMapName == null) { targetMapName = homeBaseMapName(); @@ -914,6 +860,23 @@ private void executePlan(QuestAPI.Quest quest, double progress) { return; } + if (targetCoordinates != null && movement.getClosestDistance(targetCoordinates) > 120d) { + npcCombat.stopCombat(); + attack.stopAttack(); + hero.setRunMode(); + movement.moveTo(targetCoordinates); + updateProgressStatus(quest, progress, "Quest Engine koordinata gidiyor: " + + targetCoordinates.x() + "/" + targetCoordinates.y()); + return; + } + if (targetCoordinates != null && targetNpcDescription == null && oreToSell == null && + !collectBonus && !collectCargo) { + movement.stop(false); + hero.setRoamMode(); + updateProgressStatus(quest, progress, "Quest Engine hedef koordinatta"); + return; + } + if (oreToSell != null) { if (ores.canSellOres() && ores.getAmount(oreToSell) > 0) { ores.sellOre(oreToSell); @@ -1103,11 +1066,7 @@ private void roamIfNeeded() { } private boolean isNpcRequirement(QuestAPI.Requirement requirement) { - QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); - return type == QuestAPI.Requirement.RequirementType.KILL_NPC || - type == QuestAPI.Requirement.RequirementType.KILL_NPCS || - type == QuestAPI.Requirement.RequirementType.DAMAGE_NPCS || - type == QuestAPI.Requirement.RequirementType.DAMAGE; + return DailyQuestConditionEngine.isNpcCondition(requirement); } private boolean isOnMap(String mapName) { @@ -1122,6 +1081,39 @@ private String homeBaseMapName() { return prefix + "-1"; } + private String companyPrefix() { + GameMap current = starSystem.getCurrentMap(); + String prefix = current == null ? "1" : current.getName().split("-")[0]; + return prefix.matches("[1-3]") ? prefix : "1"; + } + + private boolean recoverStagnantPlan(QuestAPI.Quest quest, double progress, long now) { + if (lastProgressAt == 0L || lastTrackedProgress < 0d || progress > lastTrackedProgress + 0.000001d) { + lastTrackedProgress = progress; + lastProgressAt = now; + return false; + } + if (now - lastProgressAt < settings.stagnationTimeoutSeconds * 1_000L) return false; + + lastProgressAt = now; + npcCombat.stopCombat(); + attack.stopAttack(); + attack.setTarget(null); + if (targetMapName != null && !isOnMap(targetMapName)) { + navigateTo(targetMapName); + } else if (!movement.isMoving()) { + movement.moveRandom(); + } + updateProgressStatus(quest, progress, + "Quest Engine ilerleme yenilemesi: hedef ve rota yeniden değerlendirildi"); + return true; + } + + private void resetProgressTracking() { + lastTrackedProgress = -1d; + lastProgressAt = 0L; + } + private void updateProgressStatus(QuestAPI.Quest quest, double progress, String action) { int percent = (int) Math.round(progress * 100d); status = "Günlük görev | Tamamlanan: " + completedQuestIds.size() + @@ -1162,6 +1154,7 @@ private void clearPlan() { oreToSell = null; collectBonus = false; collectCargo = false; + targetCoordinates = null; } String getNpcLocatorTargetDescription() { diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java index 5eae9a09..1efa353b 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java @@ -22,5 +22,8 @@ public final class DailyTaskConfig { @Number(min = 0.10, max = 0.90, step = 0.05) public double minimumHpPercent = 0.30; + @Number(min = 60, max = 900, step = 30) + public int stagnationTimeoutSeconds = 300; + public boolean stopWhenAllDailyTasksAreDone = true; } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java index 6a07b8d1..bcc8fdfd 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -216,13 +216,5 @@ private NpcCandidate(String original, String clean) { this.original = original; this.clean = clean; } - - private String original() { - return original; - } - - private String clean() { - return clean; - } } } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java b/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java index b2b14eb0..61b0ad4e 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java @@ -147,7 +147,7 @@ List discoverSelectors() { selectors.add(new Selector(best.x() + childX + 1, best.y() + 1)); } return List.copyOf(selectors); - } catch (Throwable error) { + } catch (RuntimeException error) { return fail("Görev kaynak ağacı hatası: " + error.getClass().getSimpleName()); } } @@ -170,7 +170,7 @@ private Sprite readSprite(long address, int depth, Set visited) { List childAddresses = new ArrayList<>(); try { nativeQuestGui.forEachSpriteChild(address, childAddresses::add); - } catch (Throwable error) { + } catch (RuntimeException error) { lastError = "Görev kaynak çocuğu okunamadı: " + describe(error); } @@ -187,7 +187,7 @@ private int coordinate(long address, long offset) { long matrix = memory.readLong(address + 72L); int raw = memory.readInt(matrix + offset); return Math.round(raw * 0.05f); - } catch (Throwable ignored) { + } catch (RuntimeException ignored) { return 0; } } From a358785b631dcfd7676e8351ddac6c70e5996bac Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Tue, 11 Aug 2026 23:04:21 +0300 Subject: [PATCH 06/14] Add DailyTaskAPI policy tests --- .../DailyRamCleanerPolicyTest.java | 23 ++++++++++++++++++ .../dailytaskapi/DailyTaskPlannerTest.java | 24 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java new file mode 100644 index 00000000..efa1b0db --- /dev/null +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java @@ -0,0 +1,23 @@ +package dev.shared.berke.dailytaskapi; + +public final class DailyRamCleanerPolicyTest { + public static void main(String[] args) { + assertFalse(DailyRamCleanerPolicy.shouldClean(1199, 1200, true, false), + "below threshold"); + assertTrue(DailyRamCleanerPolicy.shouldClean(1200, 1200, true, false), + "at threshold while idle"); + assertFalse(DailyRamCleanerPolicy.shouldClean(1800, 1200, true, true), + "combat protection"); + assertTrue(DailyRamCleanerPolicy.shouldClean(1800, 1200, false, true), + "combat protection disabled"); + System.out.println("DailyRamCleanerPolicyTest: OK"); + } + + private static void assertTrue(boolean value, String message) { + if (!value) throw new AssertionError(message); + } + + private static void assertFalse(boolean value, String message) { + if (value) throw new AssertionError(message); + } +} diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java index 14e4bd40..e127f87a 100644 --- a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java @@ -31,6 +31,30 @@ public static void main(String[] args) { require(DailyTaskPlanner.actionable(nestedNpcQuest).contains(mapChild), "NPC hedefinin altındaki harita şartı da korunmalı"); + DailyQuestConditionEngine.Plan nestedPlan = DailyQuestConditionEngine.build(nestedNpcQuest, "1"); + require("1-4".equals(nestedPlan.targetMapName()), + "Quest Engine must include a nested MAP condition"); + require(nestedPlan.targetNpcDescription().contains("Mordon"), + "Quest Engine must include a nested NPC condition"); + + QuestAPI.Requirement coordinates = requirement("X: 12400 Y: 7600", "COORDINATES", + QuestAPI.Requirement.RequirementType.COORDINATES, 0, 1, false); + QuestAPI.Quest coordinateQuest = quest(5, true, false, "Coordinates", List.of(coordinates, dailyTimer)); + DailyQuestConditionEngine.Plan coordinatePlan = DailyQuestConditionEngine.build(coordinateQuest, "1"); + require(coordinatePlan.targetCoordinates() != null && + coordinatePlan.targetCoordinates().x() == 12400 && + coordinatePlan.targetCoordinates().y() == 7600, + "Quest Engine must parse coordinates from the source condition"); + require(coordinatePlan.coordinatesOnly(), "coordinate-only plans must be recognized"); + + QuestAPI.Requirement visitMap = requirement("Visit map 1-8", "VISIT_MAP", + QuestAPI.Requirement.RequirementType.VISIT_MAP, 0, 1, false); + QuestAPI.Quest visitQuest = quest(6, true, false, "Map visit", List.of(visitMap, dailyTimer)); + require("1-8".equals(DailyQuestConditionEngine.build(visitQuest, "1").targetMapName()), + "Quest Engine must support typed VISIT_MAP conditions"); + require(DailyQuestConditionEngine.supports(visitQuest), + "a map visit must be considered executable"); + require(DailyTaskPlanner.isDailyType("questType_daily1"), "quest catalog daily type should be recognized"); require(!DailyTaskPlanner.isDailyType("questType_kill"), From c3f524936f5eef7267ca3aa7d700e3e3fdb37508 Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Tue, 11 Aug 2026 23:04:42 +0300 Subject: [PATCH 07/14] Register DailyTaskAPI features --- src/main/resources/plugin.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index 6a8e3352..36a9295a 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -21,7 +21,8 @@ "dev.shared.do_gamer.task.autobuy.Autobuy", "dev.shared.halizeur.log_overlay.LogOverlay", "dev.shared.berke.dailytaskapi.DailyTaskAPI", - "dev.shared.berke.dailytaskapi.DailyNpcPetLocator" + "dev.shared.berke.dailytaskapi.DailyNpcPetLocator", + "dev.shared.berke.dailytaskapi.DailyRamCleaner" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar" From a3d0b728e9047c005c81d0cfae2638f96c721e8a Mon Sep 17 00:00:00 2001 From: zrqq31 Date: Tue, 11 Aug 2026 23:04:58 +0300 Subject: [PATCH 08/14] Document DailyTaskAPI and RAM cleaner --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0bc63f61..ba9f48a8 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,12 @@ We will seek to implement CI/CD in the repository so that developers have a good - GG Alert Closer: Closes alerts at Galaxy Gates (Hades, LoW and Kuiper). By @do-gamer & @orbithelper - Fast Travel: Fast travel between maps using Jump CPU (AJP-01). By @do-gamer & @orbithelper - Orbit Helper Quick Login: Adds a button to navigate to Orbit Helper from the menu. By @orbithelper -- Simple Galaxy Gate: Supports ABG, Delta, Epsilon, Zeta, Hades, Kuiper, LoW, Invasion, Mimesis, EBG, GoP, Treacherous, DSE, Trinity and Voyagers. By @do-gamer +- Simple Galaxy Gate: Supports ABG, Delta, Epsilon, Zeta, Hades, Kuiper, LoW, Invasion, Mimesis, EBG, GoP, Treacherous, DSE, Fiesta, Trinity and Voyagers. By @do-gamer - Autobuy: Automatically buys boosters and special items from the shop at a configured interval. By @do-gamer - Log Overlay: Displays the latest in-game log messages on the canvas (top-center, auto-fade after 5s). Built-in keyword whitelist limits the displayed lines to gains and errors so the canvas does not get cluttered. By @Halizeur -- Daily Task API: Detects active daily quests from DarkBot's quest data, completes supported objectives, accepts eligible Uridium-reward dailies at the home station, disables the PET when finished, and stops the bot without changing another module's saved configuration. By @Berke +- DailyTaskAPI: Detects supported daily quests from DarkBot's quest data, completes their NPC, map, collection and ore requirements, and accepts eligible Uridium-reward dailies. By Berke +- DailyTaskAPI PET Locator: Exposes the current quest NPC to DarkBot's PET Enemy Locator without changing another module's saved NPC selection. By Berke +- RAM Cleaner: Requests cleanup through DarkBot's supported memory handler when a configurable threshold is exceeded, with optional combat protection. By Berke ## Contributing From 792df44646b7bdf4bff42f26bbc0f86be1629d68 Mon Sep 17 00:00:00 2001 From: Berke Date: Wed, 12 Aug 2026 18:00:32 +0300 Subject: [PATCH 09/14] Fix review findings and separate Berke features --- build.gradle.kts | 8 +- .../berke/dailytaskapi/DailyNpcCombat.java | 41 +- .../berke/dailytaskapi/DailyTaskAPI.java | 769 ++++++++++-------- .../berke/dailytaskapi/DailyTaskConfig.java | 4 - .../berke/dailytaskapi/DailyTaskPlanner.java | 118 +-- .../berke/dailytaskapi/QuestMenuSource.java | 44 +- .../DailyNpcPetLocator.java | 22 +- .../DailyRamCleaner.java | 8 +- .../DailyRamCleanerConfig.java | 4 +- .../DailyRamCleanerPolicy.java | 2 +- src/main/resources/plugin.json | 4 +- .../DailyRamCleanerPolicyTest.java | 23 - .../dailytaskapi/DailyTaskPlannerTest.java | 207 ++--- .../ramcleaner/DailyRamCleanerPolicyTest.java | 20 + 14 files changed, 702 insertions(+), 572 deletions(-) rename src/main/java/dev/shared/berke/{dailytaskapi => petlocator}/DailyNpcPetLocator.java (74%) rename src/main/java/dev/shared/berke/{dailytaskapi => ramcleaner}/DailyRamCleaner.java (91%) rename src/main/java/dev/shared/berke/{dailytaskapi => ramcleaner}/DailyRamCleanerConfig.java (88%) rename src/main/java/dev/shared/berke/{dailytaskapi => ramcleaner}/DailyRamCleanerPolicy.java (91%) delete mode 100644 src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java create mode 100644 src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java diff --git a/build.gradle.kts b/build.gradle.kts index 67e36e84..3d102465 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -30,6 +30,12 @@ description = "SharedPlugin" dependencies { api("eu.darkbot.DarkBotAPI", "darkbot-impl", "0.9.8") api("eu.darkbot", "DarkBot", "dc48506543") + testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +tasks.test { + useJUnitPlatform() } @@ -40,4 +46,4 @@ tasks.named("jar") { tasks.register("signFile") { dependsOn("build") commandLine("cmd", "/c", "sign.bat") -} \ No newline at end of file +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java index 4e409cd3..32f811ed 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java @@ -18,16 +18,24 @@ * owner of quest and map selection.

*/ final class DailyNpcCombat extends LootModule { + private final ConfigSetting petEnabledSetting; + private final ConfigSetting keepRoamingPointSetting; private String targetDescription; + private Boolean originalPetEnabledSetting; + private Boolean originalKeepRoamingPointSetting; + private Boolean originalPetRuntimeEnabled; DailyNpcCombat(PluginAPI api) { super(api); + ConfigAPI config = api.requireAPI(ConfigAPI.class); + petEnabledSetting = config.requireConfig("pet.enabled"); + keepRoamingPointSetting = config.requireConfig("general.roaming.keep"); } void tick(String description) { targetDescription = description; - ConfigSetting petEnabled = api.requireAPI(ConfigAPI.class).requireConfig("pet.enabled"); - if (!Boolean.TRUE.equals(petEnabled.getValue())) petEnabled.setValue(true); + rememberUserSettings(); + if (!Boolean.TRUE.equals(petEnabledSetting.getValue())) petEnabledSetting.setValue(true); super.onTickModule(); } @@ -35,6 +43,11 @@ void stopCombat() { attack.stopAttack(); attack.setTarget(null); targetDescription = null; + restoreUserSettings(); + } + + void shutdownPet() { + stopCombat(); if (pet.isEnabled()) pet.setEnabled(false); } @@ -67,14 +80,32 @@ protected boolean findTarget() { // MovementAPI.moveRandom follows the saved preferred-zone route for // this map and falls back to a random map point when no route exists. if (selected == null && !movement.isMoving()) { - ConfigSetting keepPoint = api.requireAPI(ConfigAPI.class) - .requireConfig("general.roaming.keep"); - if (Boolean.TRUE.equals(keepPoint.getValue())) keepPoint.setValue(false); + rememberUserSettings(); + if (Boolean.TRUE.equals(keepRoamingPointSetting.getValue())) { + keepRoamingPointSetting.setValue(false); + } movement.moveRandom(); } return selected != null; } + private void rememberUserSettings() { + if (originalPetEnabledSetting != null) return; + originalPetEnabledSetting = petEnabledSetting.getValue(); + originalKeepRoamingPointSetting = keepRoamingPointSetting.getValue(); + originalPetRuntimeEnabled = pet.isEnabled(); + } + + private void restoreUserSettings() { + if (originalPetEnabledSetting == null) return; + petEnabledSetting.setValue(originalPetEnabledSetting); + keepRoamingPointSetting.setValue(originalKeepRoamingPointSetting); + pet.setEnabled(Boolean.TRUE.equals(originalPetRuntimeEnabled)); + originalPetEnabledSetting = null; + originalKeepRoamingPointSetting = null; + originalPetRuntimeEnabled = null; + } + private boolean isUsableTarget(Npc npc) { if (npc == null || targetDescription == null || !npc.isValid() || !npc.isSelectable() || npc.isBlacklisted()) { diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java index 6c11ae20..e8929c10 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -21,6 +21,8 @@ import eu.darkbot.api.managers.QuestAPI; import eu.darkbot.api.managers.StarSystemAPI; +import java.time.LocalDate; +import java.time.ZoneId; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedHashMap; @@ -39,9 +41,10 @@ * ore-selling settings. Selecting another DarkBot module therefore leaves * that module's saved settings untouched.

*/ -@Feature(name = "DailyTaskAPI", description = "Yalnızca 24 saatlik günlük görevleri tamamlar") +@Feature(name = "DailyTaskAPI", description = "Completes only verified 24-hour daily quests") public final class DailyTaskAPI implements Module, Configurable { private static final String QUEST_BASE_MAP = "1-8"; + private static final String PARK_AFTER_DAILY = "__PARK_AFTER_DAILY__"; private static final double QUEST_STATION_DISTANCE = 300d; private static final Set SPECIAL_DAILY_QUEST_IDS = Set.of(318001); @@ -103,7 +106,7 @@ private QuestMenuSource.Selector selector() { private String currentQuestTitle = ""; private long nextActionAt; private long nextRoamAt; - private String status = "Başlatılıyor"; + private String status = "Starting"; private String targetNpcDescription; private String targetMapName; private OreAPI.Ore oreToSell; @@ -161,105 +164,71 @@ public void setConfig(ConfigSetting setting) { @Override public void onTickModule() { long now = System.currentTimeMillis(); - if (state == State.DONE) { - // Keep the completed state for the rest of the local day. Pressing - // Start again must not scan the same finished quests repeatedly. - // After midnight a deliberate new start begins a fresh daily run. - if (nextActionAt > now) { - npcCombat.stopCombat(); - if (npcCombat.isPetActive() || now < nextRoamAt) { - status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + - " | PET kapatılıyor | Bot PET kapanınca duracak"; - return; - } - status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + - " | Tamamlanan: " + completedQuestIds.size() + - " | PET kapalı | Bot durduruldu"; - if (bot.isRunning()) bot.setRunning(false); - return; - } + if (handleCompletedState(now)) return; + initializeSession(now); + if (tickStateMachine(now)) return; + runDisplayedQuest(now); + } + + private boolean handleCompletedState(long now) { + if (state != State.DONE) return false; + if (nextActionAt <= now) { state = State.STARTING; + return false; } - if (state == State.STARTING) { - resetSession(); - if (quests.isQuestGiverOpen()) { - questGiverSource.close(); - nextActionAt = now + settings.questSwitchDelayMs; - } - state = State.DISCOVERING; - if (nextActionAt < now) nextActionAt = now; + npcCombat.shutdownPet(); + if (npcCombat.isPetActive() || now < nextRoamAt) { + status = "ALL DAILY QUESTS COMPLETED | PET is shutting down"; + return true; } + status = "ALL DAILY QUESTS COMPLETED | Completed: " + completedQuestIds.size() + + " | PET off | Bot stopped"; + if (bot.isRunning()) bot.setRunning(false); + return true; + } - // Active quests can be discovered and selected from the quest menu on - // any map. Travel to the 1-8 station only after no active daily quest - // remains and the quest-giver offers must be inspected. - if (isGiverState(state)) { - if (!ensureQuestStation()) return; - if (now >= nextActionAt) tickQuestGiver(); - return; + private void initializeSession(long now) { + if (state != State.STARTING) return; + resetSession(); + if (quests.isQuestGiverOpen()) { + questGiverSource.close(); + nextActionAt = now + settings.questSwitchDelayMs; } + state = State.DISCOVERING; + if (nextActionAt < now) nextActionAt = now; + } - if (state == State.DISCOVERING) { - if (now >= nextActionAt) discoverOrScanNext(); - return; - } - if (state == State.WAITING_FOR_DISCOVERY) { - if (now >= nextActionAt) inspectDiscoveredSelector(); - return; - } - if (state == State.SELECTING) { - if (now >= nextActionAt) selectPendingDaily(); - return; - } - if (state == State.WAITING_FOR_SELECTION) { - if (now >= nextActionAt) inspectPendingSelection(); - return; + private boolean tickStateMachine(long now) { + if (isGiverState(state)) { + if (ensureQuestStation() && now >= nextActionAt) tickQuestGiver(); + return true; } - if (state == State.PAUSED) return; + Runnable action = stateAction(); + if (action == null) return state == State.PAUSED; + if (now >= nextActionAt) action.run(); + return true; + } + private Runnable stateAction() { + if (state == State.DISCOVERING) return this::discoverOrScanNext; + if (state == State.WAITING_FOR_DISCOVERY) return this::inspectDiscoveredSelector; + if (state == State.SELECTING) return this::selectPendingDaily; + if (state == State.WAITING_FOR_SELECTION) return this::inspectPendingSelection; + return null; + } + + private void runDisplayedQuest(long now) { QuestAPI.Quest quest = quests.getDisplayedQuest(); if (quest == null) { - // Completing a quest (and slow server responses) can leave the - // displayed quest empty for a few ticks. Wait for the next source - // snapshot instead of entering a permanent paused state. - attack.stopAttack(); - movement.stop(false); - hero.setRoamMode(); - status = "Görev verisi yenileniyor; mevcut plan korunuyor"; + waitForQuestSnapshot(); return; } - - if (currentQuestId >= 0 && quest.getId() != currentQuestId) { - completedQuestIds.add(currentQuestId); - stopCurrentAction(); - currentQuestId = -1; - currentQuestTitle = "__PARK_AFTER_DAILY__"; - prepareNewScan(); - nextActionAt = now + settings.questSwitchDelayMs; - status = "Görev değişti; görev menüsü kaynaktan yeniden taranıyor"; - return; - } - - if (!isVerifiedDaily(quest)) { - stopCurrentAction(); - prepareNewScan(); - nextActionAt = now + settings.questSwitchDelayMs; - status = "Görev günlük değil; kaynak menü yeniden taranıyor"; - return; - } - + if (handleQuestChange(quest, now) || rejectNonDailyQuest(quest, now)) return; double progress = DailyTaskPlanner.progress(quest); if (quest.isCompleted() || progress >= 0.9999d) { - completedQuestIds.add(quest.getId()); - status = "Tamamlandı: " + safeTitle(quest); - stopCurrentAction(); - currentQuestId = -1; - currentQuestTitle = "__PARK_AFTER_DAILY__"; - prepareNewScan(); - nextActionAt = now + settings.questSwitchDelayMs; + completeQuest(quest, now); return; } - buildPlanFromSource(quest); if (recoverStagnantPlan(quest, progress, now)) return; boolean standardNpcCombatOwnsSafety = targetNpcDescription != null && @@ -268,6 +237,42 @@ public void onTickModule() { executePlan(quest, progress); } + private void waitForQuestSnapshot() { + attack.stopAttack(); + movement.stop(false); + hero.setRoamMode(); + status = "Waiting for refreshed quest data; current plan retained"; + } + + private boolean handleQuestChange(QuestAPI.Quest quest, long now) { + if (currentQuestId < 0 || quest.getId() == currentQuestId) return false; + completedQuestIds.add(currentQuestId); + scheduleQuestMenuScan(now, true); + status = "Displayed quest changed; rescanning the native quest menu"; + return true; + } + + private boolean rejectNonDailyQuest(QuestAPI.Quest quest, long now) { + if (isVerifiedDaily(quest)) return false; + scheduleQuestMenuScan(now, false); + status = "Displayed quest is not daily; rescanning the native quest menu"; + return true; + } + + private void completeQuest(QuestAPI.Quest quest, long now) { + completedQuestIds.add(quest.getId()); + status = "Completed: " + safeTitle(quest); + scheduleQuestMenuScan(now, true); + } + + private void scheduleQuestMenuScan(long now, boolean parkFirst) { + stopCurrentAction(); + currentQuestId = -1; + currentQuestTitle = parkFirst ? PARK_AFTER_DAILY : ""; + prepareNewScan(); + nextActionAt = now + settings.questSwitchDelayMs; + } + private void resetSession() { selectionRetries = 0; menuScanPass = 0; @@ -297,92 +302,110 @@ private void resetSession() { skippedTetrathrinOfferIds.clear(); resetProgressTracking(); clearPlan(); - status = "Görev menüsü kaynaktan taranıyor"; + status = "Scanning the native quest menu"; } private void discoverOrScanNext() { - if ("__PARK_AFTER_DAILY__".equals(currentQuestTitle)) { - Optional nearestPortal = entities.getPortals().stream() - .filter(Portal::isValid) - .min(Comparator.comparingDouble(hero::distanceTo)); - if (nearestPortal.isEmpty()) { - attack.stopAttack(); - status = "Görev sonu için en yakın portal verisi bekleniyor"; - nextActionAt = System.currentTimeMillis() + 1_000L; - return; - } - double portalDistance = hero.distanceTo(nearestPortal.get()); - if (portalDistance > 300d) { - attack.stopAttack(); - hero.setRunMode(); - movement.moveTo(nearestPortal.get()); - status = "Görev tamamlandı; en yakın portala gidiliyor: " + - (int) Math.round(portalDistance); - nextActionAt = System.currentTimeMillis() + 1_000L; - return; - } - movement.stop(false); - hero.setRoamMode(); - currentQuestTitle = ""; - status = "Görev tamamlandı; en yakın portalda güvenli konum alındı"; + if (parkAfterCompletedQuest()) return; + if (!ensureQuestMenuVisible()) return; + if (!ensureQuestSelectors()) return; + scanNextQuestSelector(); + } + + private boolean parkAfterCompletedQuest() { + if (!PARK_AFTER_DAILY.equals(currentQuestTitle)) return false; + Optional portal = nearestPortal(); + if (portal.isEmpty()) { + attack.stopAttack(); + status = "Waiting for the nearest portal after quest completion"; + nextActionAt = System.currentTimeMillis() + 1_000L; + return true; + } + double distance = hero.distanceTo(portal.get()); + if (distance > 300d) { + moveToCompletionPortal(portal.get(), distance); + return true; } + movement.stop(false); + hero.setRoamMode(); + currentQuestTitle = ""; + status = "Quest completed; parked safely at the nearest portal"; + return false; + } + + private Optional nearestPortal() { + return entities.getPortals().stream() + .filter(Portal::isValid) + .min(Comparator.comparingDouble(hero::distanceTo)); + } + + private void moveToCompletionPortal(Portal portal, double distance) { + attack.stopAttack(); + hero.setRunMode(); + movement.moveTo(portal); + status = "Quest completed; moving to the nearest portal: " + (int) Math.round(distance); + nextActionAt = System.currentTimeMillis() + 1_000L; + } + private boolean ensureQuestMenuVisible() { if (questGui == null) { - pauseSafely("DarkBot görev penceresi nesnesi bulunamadı"); - return; + pauseSafely("DarkBot quest window is unavailable"); + return false; } - if (!questGui.isVisible()) { - if (!settings.autoOpenQuestWindow) { - status = "Görev penceresini aç"; - nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; - return; - } - questGui.setVisible(true); - status = "Görev penceresi açılıyor"; + if (questGui.isVisible()) return true; + if (!settings.autoOpenQuestWindow) { + status = "Open the quest window"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; - return; + return false; } + questGui.setVisible(true); + status = "Opening the quest window"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + return false; + } + private boolean ensureQuestSelectors() { + if (!selectors.isEmpty()) return true; + refreshDailyCatalog(); + selectors = questMenuSource.discoverSelectors(); if (selectors.isEmpty()) { - refreshDailyCatalog(); - selectors = questMenuSource.discoverSelectors(); - if (selectors.isEmpty()) { - retryMenuScan(questMenuSource.getLastError()); - return; - } - menuScanPass++; - selectorIndex = 0; - selectionRetries = 0; - if (menuScanPass == 1) { - scannedQuestIds.clear(); - dailyChoices.clear(); - } - QuestAPI.Quest displayed = quests.getDisplayedQuest(); - recordQuest(displayed, null); - if (displayed != null && dailyChoices.containsKey(displayed.getId()) && !isFinished(displayed)) { - startDaily(displayed); - return; - } + retryMenuScan(questMenuSource.getLastError()); + return false; } + menuScanPass++; + selectorIndex = 0; + selectionRetries = 0; + scannedQuestIds.clear(); + dailyChoices.clear(); + QuestAPI.Quest displayed = quests.getDisplayedQuest(); + recordQuest(displayed, null); + if (!isRunnableDaily(displayed)) return true; + startDaily(displayed); + return false; + } + + private boolean isRunnableDaily(QuestAPI.Quest quest) { + return quest != null && dailyChoices.containsKey(quest.getId()) && !isFinished(quest); + } + private void scanNextQuestSelector() { if (selectorIndex >= selectors.size()) { finishMenuScan(); return; } - QuestMenuSource.Selector selector = selectors.get(selectorIndex); QuestAPI.Quest before = quests.getDisplayedQuest(); questIdBeforeSelection = before == null ? -1 : before.getId(); questGui.click(selector.x(), selector.y()); state = State.WAITING_FOR_DISCOVERY; - status = "Görev menüsü taranıyor: " + (selectorIndex + 1) + "/" + selectors.size(); + status = "Scanning quest menu: " + (selectorIndex + 1) + "/" + selectors.size(); nextActionAt = System.currentTimeMillis() + 500L; } private void inspectDiscoveredSelector() { QuestAPI.Quest quest = quests.getDisplayedQuest(); if (quest == null) { - retryDiscoveredSelector("Sunucudan görev verisi bekleniyor"); + retryDiscoveredSelector("Waiting for quest data from the server"); return; } @@ -391,7 +414,7 @@ private void inspectDiscoveredSelector() { selectorIndex++; selectionRetries = 0; state = State.DISCOVERING; - status = "Görev satırı yanıt vermedi; tek taramada sonraki satıra geçiliyor"; + status = "Quest row did not respond; continuing the single scan"; nextActionAt = System.currentTimeMillis() + 250L; return; } @@ -427,7 +450,7 @@ private void finishMenuScan() { selectionRetries = 0; state = State.SELECTING; nextActionAt = System.currentTimeMillis() + 250L; - status = scanSummary() + " | Günlük seçiliyor: " + pendingChoice.title(); + status = scanSummary() + " | Selecting daily: " + pendingChoice.title(); return; } @@ -465,98 +488,125 @@ private void beginQuestOfferScan() { seenGiverOfferIds.clear(); state = State.OPENING_GIVER; nextActionAt = System.currentTimeMillis() + 500L; - status = "İstasyondaki günlük teklifler kaynak verisiyle taranacak"; + status = "Scanning station daily offers from source data"; } private void tickQuestGiver() { - if (state == State.CLOSING_GIVER) { - if (quests.isQuestGiverOpen()) { - questGiverSource.close(); - status = "Görev verici kapatılıyor"; - nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; - return; - } - if (acceptedOfferIds.isEmpty()) { - finishAll(); - return; - } - prepareNewScan(); - nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; - status = "Kabul edilen günlükler aktif menüden doğrulanıyor"; - return; + switch (state) { + case CLOSING_GIVER: + tickClosingGiver(); + break; + case OPENING_GIVER: + case WAITING_GIVER_OPEN: + tickOpeningGiver(); + break; + default: + tickOpenGiverState(); + break; } + } - if (state == State.OPENING_GIVER || state == State.WAITING_GIVER_OPEN) { - if (quests.isQuestGiverOpen()) { - giverOpenAttempts = 0; - state = State.SELECTING_DAILY_TAB; - nextActionAt = System.currentTimeMillis() + 300L; - status = "Günlük görev sekmesi açılıyor"; - return; - } - Optional station = findQuestStation(); - if (station.isPresent() && hero.distanceTo(station.get()) <= QUEST_STATION_DISTANCE) { - station.get().trySelect(false); - } - giverOpenAttempts++; - state = State.WAITING_GIVER_OPEN; - status = "Görev vericinin sunucu yanıtı bekleniyor (" + giverOpenAttempts + ")"; + private void tickClosingGiver() { + if (quests.isQuestGiverOpen()) { + questGiverSource.close(); + status = "Closing quest giver"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; return; } - - if (!quests.isQuestGiverOpen()) { - state = State.OPENING_GIVER; - nextActionAt = System.currentTimeMillis() + 500L; - status = "Görev verici kapandı; yeniden açılıyor"; + if (acceptedOfferIds.isEmpty()) { + finishAll(); return; } + prepareNewScan(); + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + status = "Verifying accepted daily quests in the active quest menu"; + } - if (state == State.SELECTING_DAILY_TAB) { - questGiverSource.selectDailyTab(); - state = State.WAITING_DAILY_TAB; - status = "Günlük sekmesinin kaynak verisi bekleniyor"; - nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + private void tickOpeningGiver() { + if (quests.isQuestGiverOpen()) { + giverOpenAttempts = 0; + state = State.SELECTING_DAILY_TAB; + nextActionAt = System.currentTimeMillis() + 300L; + status = "Opening daily quest tab"; return; } - if (state == State.WAITING_DAILY_TAB) { - giverRowIndex = 1; - giverRowRetries = 0; - state = State.SELECTING_GIVER_ROW; - nextActionAt = System.currentTimeMillis() + 250L; + findQuestStation().filter(station -> hero.distanceTo(station) <= QUEST_STATION_DISTANCE) + .ifPresent(station -> station.trySelect(false)); + giverOpenAttempts++; + state = State.WAITING_GIVER_OPEN; + status = "Waiting for quest giver response (" + giverOpenAttempts + ")"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void tickOpenGiverState() { + if (!quests.isQuestGiverOpen()) { + state = State.OPENING_GIVER; + nextActionAt = System.currentTimeMillis() + 500L; + status = "Quest giver closed; reopening"; return; } - if (state == State.SELECTING_GIVER_ROW) { - if (giverRowIndex > QuestGiverSource.MAX_ROWS) { - if ((giverScrollPage > 0 && !giverPageHadNewId) || - giverScrollPage >= QuestGiverSource.MAX_SCROLL_PAGES) { - finishQuestOfferScan(); - return; - } - giverScrollPage++; - giverPageHadNewId = false; - giverRowIndex = 1; - giverRowRetries = 0; - questGiverSource.scrollDailyListDown(); - state = State.WAITING_DAILY_TAB; - nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; - return; - } - questGiverSource.selectRow(giverRowIndex); - state = State.WAITING_GIVER_ROW; - status = "Günlük teklif okunuyor: satır " + giverRowIndex; - nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; - return; + switch (state) { + case SELECTING_DAILY_TAB: + selectDailyTab(); + break; + case WAITING_DAILY_TAB: + beginGiverRows(); + break; + case SELECTING_GIVER_ROW: + selectGiverRow(); + break; + case WAITING_GIVER_ROW: + inspectGiverRow(); + break; + case ACCEPTING_GIVER_QUEST: + acceptSelectedOffer(); + break; + case WAITING_GIVER_ACCEPT: + verifyAcceptedOffer(); + break; + default: + break; } - if (state == State.WAITING_GIVER_ROW) { - inspectGiverRow(); + } + + private void selectDailyTab() { + questGiverSource.selectDailyTab(); + state = State.WAITING_DAILY_TAB; + status = "Waiting for daily tab source data"; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void beginGiverRows() { + giverRowIndex = 1; + giverRowRetries = 0; + state = State.SELECTING_GIVER_ROW; + nextActionAt = System.currentTimeMillis() + 250L; + } + + private void selectGiverRow() { + if (giverRowIndex > QuestGiverSource.MAX_ROWS) { + advanceGiverPage(); return; } - if (state == State.ACCEPTING_GIVER_QUEST) { - acceptSelectedOffer(); + questGiverSource.selectRow(giverRowIndex); + state = State.WAITING_GIVER_ROW; + status = "Reading daily offer row " + giverRowIndex; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; + } + + private void advanceGiverPage() { + boolean exhausted = giverScrollPage > 0 && !giverPageHadNewId; + if (exhausted || giverScrollPage >= QuestGiverSource.MAX_SCROLL_PAGES) { + finishQuestOfferScan(); return; } - if (state == State.WAITING_GIVER_ACCEPT) verifyAcceptedOffer(); + giverScrollPage++; + giverPageHadNewId = false; + giverRowIndex = 1; + giverRowRetries = 0; + questGiverSource.scrollDailyListDown(); + state = State.WAITING_DAILY_TAB; + nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; } private void inspectGiverRow() { @@ -564,7 +614,7 @@ private void inspectGiverRow() { QuestAPI.Quest selected = quests.getSelectedQuest(); boolean matchingSource = info != null && selected != null && info.getId() == selected.getId(); if (!matchingSource) { - retryOrAdvanceGiverRow("Teklif ayrıntısı sunucudan bekleniyor"); + retryOrAdvanceGiverRow("Waiting for offer details from the server"); return; } if (seenGiverOfferIds.add(info.getId())) giverPageHadNewId = true; @@ -574,17 +624,9 @@ private void inspectGiverRow() { return; } - boolean protegitKill = false; - for (QuestAPI.Requirement requirement : DailyTaskPlanner.actionable(selected)) { - if (isNpcRequirement(requirement) && - DailyTaskPlanner.normalizeNpcName(requirement.getDescription()).contains("protegit")) { - protegitKill = true; - break; - } - } - if (protegitKill) { + if (isProtegitQuest(selected)) { reviewedOfferIds.add(info.getId()); - status = "Protegit öldürme görevi pas geçildi: " + safeTitle(selected); + status = "Skipped Protegit kill quest: " + safeTitle(selected); advanceGiverRow(); return; } @@ -596,12 +638,15 @@ private void inspectGiverRow() { return; } if (selected.getRewards() == null || selected.getRewards().isEmpty()) { - retryOrAdvanceGiverRow("Teklif ödülleri kaynaktan bekleniyor"); + retryOrAdvanceGiverRow("Waiting for offer rewards from the source"); return; } if (!DailyTaskPlanner.hasUridiumReward(selected.getRewards())) { reviewedOfferIds.add(id); - status = "Uridium ödülü olmayan günlük pas geçildi: " + safeTitle(selected); + if (DailyTaskPlanner.isOnlyTetrathrinReward(selected.getRewards())) { + skippedTetrathrinOfferIds.add(id); + } + status = "Skipped daily quest without Uridium: " + safeTitle(selected); advanceGiverRow(); return; } @@ -611,7 +656,7 @@ private void inspectGiverRow() { acceptRetries = 0; state = State.ACCEPTING_GIVER_QUEST; nextActionAt = System.currentTimeMillis() + 250L; - status = "Kaynak verisi doğrulandı; günlük kabul edilecek: " + selectedOfferTitle; + status = "Source data verified; accepting daily: " + selectedOfferTitle; } private void retryOrAdvanceGiverRow(String reason) { @@ -635,48 +680,54 @@ private void advanceGiverRow() { private void acceptSelectedOffer() { QuestAPI.QuestListItem info = quests.getSelectedQuestInfo(); QuestAPI.Quest selected = quests.getSelectedQuest(); - boolean protegitKill = false; - if (selected != null) { - for (QuestAPI.Requirement requirement : DailyTaskPlanner.actionable(selected)) { - if (isNpcRequirement(requirement) && - DailyTaskPlanner.normalizeNpcName(requirement.getDescription()).contains("protegit")) { - protegitKill = true; - break; - } - } - } - boolean safe = info != null && selected != null && info.getId() == selectedOfferId && - selected.getId() == selectedOfferId && - (DailyTaskPlanner.isDailyType(info.getType()) || DailyTaskPlanner.isDaily(selected)) && - !protegitKill && - info.isActivable() && !info.isCompleted() && - selected.getRewards() != null && !selected.getRewards().isEmpty() && - DailyTaskPlanner.hasUridiumReward(selected.getRewards()); - if (!safe) { - int skippedId = selectedOfferId; - String skippedTitle = selectedOfferTitle; - if (skippedId > 0) reviewedOfferIds.add(skippedId); - selectedOfferId = -1; - selectedOfferTitle = ""; - status = info != null && info.getId() == skippedId && !info.isActivable() - ? "Günlük artık alınabilir değil; pas geçildi: " + skippedTitle - : "Günlük kaynak doğrulaması değişti; güvenle pas geçildi: " + skippedTitle; - advanceGiverRow(); + if (!isSafeOfferSelection(info, selected)) { + skipSelectedOffer(info); return; } questGiverSource.acceptSelected(); state = State.WAITING_GIVER_ACCEPT; - status = "Günlük kabulü sunucudan doğrulanıyor: " + selectedOfferTitle; + status = "Waiting for server confirmation: " + selectedOfferTitle; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; } + private boolean isSafeOfferSelection(QuestAPI.QuestListItem info, QuestAPI.Quest selected) { + if (info == null || selected == null) return false; + if (info.getId() != selectedOfferId || selected.getId() != selectedOfferId) return false; + if (!DailyTaskPlanner.isDailyType(info.getType()) && !DailyTaskPlanner.isDaily(selected)) return false; + if (isProtegitQuest(selected) || !info.isActivable() || info.isCompleted()) return false; + return selected.getRewards() != null && !selected.getRewards().isEmpty() && + DailyTaskPlanner.hasUridiumReward(selected.getRewards()); + } + + private void skipSelectedOffer(QuestAPI.QuestListItem info) { + int skippedId = selectedOfferId; + String skippedTitle = selectedOfferTitle; + if (skippedId > 0) reviewedOfferIds.add(skippedId); + selectedOfferId = -1; + selectedOfferTitle = ""; + if (info != null && info.getId() == skippedId && !info.isActivable()) { + status = "Offer is no longer activable; skipped: " + skippedTitle; + } else { + status = "Offer source changed during verification; skipped safely: " + skippedTitle; + } + advanceGiverRow(); + } + + private boolean isProtegitQuest(QuestAPI.Quest quest) { + return DailyTaskPlanner.actionable(quest).stream() + .filter(this::isNpcRequirement) + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::normalizeNpcName) + .anyMatch(description -> description.contains("protegit")); + } + private void verifyAcceptedOffer() { QuestAPI.QuestListItem catalog = findQuestItem(selectedOfferId).orElse(null); boolean accepted = catalog != null && !catalog.isActivable(); if (accepted) { acceptedOfferIds.add(selectedOfferId); reviewedOfferIds.add(selectedOfferId); - status = "Günlük kabul edildi: " + selectedOfferTitle; + status = "Daily quest accepted: " + selectedOfferTitle; selectedOfferId = -1; selectedOfferTitle = ""; giverRowIndex = 1; @@ -689,11 +740,11 @@ private void verifyAcceptedOffer() { } if (acceptRetries++ < Math.max(2, settings.maxSelectionRetries)) { state = State.ACCEPTING_GIVER_QUEST; - status = "Günlük kabul yanıtı gecikti; kaynak yeniden doğrulanacak"; + status = "Daily acceptance response delayed; reverifying source"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; return; } - pauseSafely("Günlük kabulü sunucudan doğrulanamadı: " + selectedOfferTitle); + pauseSafely("Could not verify daily acceptance: " + selectedOfferTitle); } private Optional findQuestItem(int id) { @@ -710,8 +761,8 @@ private Optional findQuestItem(int id) { private void finishQuestOfferScan() { offerScanNeeded = false; state = State.CLOSING_GIVER; - status = "İstasyon taraması tamamlandı | Kabul: " + acceptedOfferIds.size() + - " | Tetrathrin pas: " + skippedTetrathrinOfferIds.size(); + status = "Station scan completed | Accepted: " + acceptedOfferIds.size() + + " | Tetrathrin-only skipped: " + skippedTetrathrinOfferIds.size(); nextActionAt = System.currentTimeMillis() + 300L; } @@ -728,7 +779,7 @@ private void selectPendingDaily() { questIdBeforeSelection = displayed == null ? -1 : displayed.getId(); questGui.click(pendingChoice.selector().x(), pendingChoice.selector().y()); state = State.WAITING_FOR_SELECTION; - status = "Günlük görev kaynaktan seçiliyor: " + pendingChoice.title(); + status = "Selecting daily quest from source: " + pendingChoice.title(); nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; } @@ -742,12 +793,12 @@ private void inspectPendingSelection() { selectionRetries++; if (selectionRetries >= Math.max(1, settings.maxSelectionRetries)) { prepareNewScan(); - status = "Günlük seçim doğrulanamadı; görev menüsü yeniden taranıyor"; + status = "Daily selection not verified; rescanning quest menu"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; return; } state = State.SELECTING; - status = "Sunucudan günlük görev seçimi bekleniyor (" + selectionRetries + "/" + + status = "Waiting for daily quest selection (" + selectionRetries + "/" + settings.maxSelectionRetries + ")"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; } @@ -759,7 +810,7 @@ private void startDaily(QuestAPI.Quest quest) { resetProgressTracking(); state = State.RUNNING; buildPlanFromSource(quest); - status = "Günlük görev doğrulandı: " + currentQuestTitle; + status = "Daily quest verified: " + currentQuestTitle; } private void retryDiscoveredSelector(String reason) { @@ -768,7 +819,7 @@ private void retryDiscoveredSelector(String reason) { selectorIndex++; selectionRetries = 0; state = State.DISCOVERING; - status = reason + "; sıradaki kaynak seçici deneniyor"; + status = reason + "; trying the next source selector"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; return; } @@ -778,7 +829,7 @@ private void retryDiscoveredSelector(String reason) { } private void retryMenuScan(String reason) { - status = reason + "; tek tarama tamamlandı"; + status = reason + "; single scan completed"; if (offerScanNeeded) beginQuestOfferScan(); else finishAll(); } @@ -835,7 +886,7 @@ private boolean isFinished(QuestAPI.Quest quest) { } private String scanSummary() { - return "Menü: " + scannedQuestIds.size() + " görev, " + dailyChoices.size() + " günlük"; + return "Menu: " + scannedQuestIds.size() + " quests, " + dailyChoices.size() + " daily"; } private void buildPlanFromSource(QuestAPI.Quest quest) { @@ -854,78 +905,92 @@ private void buildPlanFromSource(QuestAPI.Quest quest) { } private void executePlan(QuestAPI.Quest quest, double progress) { - if (targetMapName != null && !isOnMap(targetMapName)) { - navigateTo(targetMapName); - updateProgressStatus(quest, progress, "Haritaya gidiliyor: " + targetMapName); - return; - } + if (travelToTargetMap(quest, progress)) return; + if (handleTargetCoordinates(quest, progress)) return; + if (handleOreSale(quest, progress)) return; + if (collectRequiredBox(quest, progress)) return; + if (huntRequiredNpc(quest, progress)) return; + if (searchForRequiredBox(quest, progress)) return; + if (reportMapArrival(quest, progress)) return; + pauseSafely("Unsupported daily objective: " + firstRequirement(quest)); + } + + private boolean travelToTargetMap(QuestAPI.Quest quest, double progress) { + if (targetMapName == null || isOnMap(targetMapName)) return false; + navigateTo(targetMapName); + updateProgressStatus(quest, progress, "Travelling to map " + targetMapName); + return true; + } - if (targetCoordinates != null && movement.getClosestDistance(targetCoordinates) > 120d) { + private boolean handleTargetCoordinates(QuestAPI.Quest quest, double progress) { + if (targetCoordinates == null) return false; + if (movement.getClosestDistance(targetCoordinates) > 120d) { npcCombat.stopCombat(); attack.stopAttack(); hero.setRunMode(); movement.moveTo(targetCoordinates); - updateProgressStatus(quest, progress, "Quest Engine koordinata gidiyor: " + + updateProgressStatus(quest, progress, "Travelling to coordinates " + targetCoordinates.x() + "/" + targetCoordinates.y()); - return; - } - if (targetCoordinates != null && targetNpcDescription == null && oreToSell == null && - !collectBonus && !collectCargo) { - movement.stop(false); - hero.setRoamMode(); - updateProgressStatus(quest, progress, "Quest Engine hedef koordinatta"); - return; + return true; } + if (!isCoordinateOnlyPlan()) return false; + movement.stop(false); + hero.setRoamMode(); + updateProgressStatus(quest, progress, "Reached target coordinates"); + return true; + } - if (oreToSell != null) { - if (ores.canSellOres() && ores.getAmount(oreToSell) > 0) { - ores.sellOre(oreToSell); - updateProgressStatus(quest, progress, "Maden satılıyor: " + oreToSell.getName()); - return; - } - if (!ores.canSellOres()) { - String base = homeBaseMapName(); - if (!isOnMap(base)) navigateTo(base); - updateProgressStatus(quest, progress, "Maden satışı için üsse gidiliyor"); - return; - } - // When the requested raw ore is absent, collect cargo from a small - // NPC without changing LootCollector's saved NPC/box settings. - if (ores.getAmount(oreToSell) <= 0 && targetNpcDescription == null) { - targetNpcDescription = "Streuner"; - collectCargo = true; - } - } + private boolean isCoordinateOnlyPlan() { + return targetNpcDescription == null && oreToSell == null && !collectBonus && !collectCargo; + } - if ((collectBonus || collectCargo) && tryCollectRequiredBox()) { - updateProgressStatus(quest, progress, collectBonus ? "Bonus kutusu toplanıyor" : "NPC yükü toplanıyor"); - return; + private boolean handleOreSale(QuestAPI.Quest quest, double progress) { + if (oreToSell == null) return false; + if (ores.canSellOres() && ores.getAmount(oreToSell) > 0) { + ores.sellOre(oreToSell); + updateProgressStatus(quest, progress, "Selling ore: " + oreToSell.getName()); + return true; } - - if (targetNpcDescription != null) { - npcCombat.tick(targetNpcDescription); - // Shared combat safety can occasionally skip its internal target - // scan for a tick. Never leave a daily hunter parked after it has - // reached the last route point: MovementAPI follows configured - // preferred zones, or chooses a random point when none exist. - if (attack.getTarget() == null && !movement.isMoving()) movement.moveRandom(); - updateProgressStatus(quest, progress, "NPC Kill and Collect: " + npcCombat.getStatus() + - " | Hedef: " + cleanedRequirement(targetNpcDescription)); - return; + if (!ores.canSellOres()) { + String base = homeBaseMapName(); + if (!isOnMap(base)) navigateTo(base); + updateProgressStatus(quest, progress, "Travelling to base to sell ore"); + return true; } - - if (collectBonus || collectCargo) { - roamIfNeeded(); - updateProgressStatus(quest, progress, "Toplanacak kutu aranıyor"); - return; + if (targetNpcDescription == null) { + targetNpcDescription = "Streuner"; + collectCargo = true; } + return false; + } - if (targetMapName != null && isOnMap(targetMapName)) { - updateProgressStatus(quest, progress, "Hedef haritaya ulaşıldı"); - return; - } + private boolean collectRequiredBox(QuestAPI.Quest quest, double progress) { + if ((!collectBonus && !collectCargo) || !tryCollectRequiredBox()) return false; + String action = collectBonus ? "Collecting bonus box" : "Collecting NPC cargo"; + updateProgressStatus(quest, progress, action); + return true; + } - pauseSafely("Desteklenmeyen günlük hedef: " + firstRequirement(quest)); + private boolean huntRequiredNpc(QuestAPI.Quest quest, double progress) { + if (targetNpcDescription == null) return false; + npcCombat.tick(targetNpcDescription); + if (attack.getTarget() == null && !movement.isMoving()) movement.moveRandom(); + updateProgressStatus(quest, progress, "NPC Kill and Collect: " + npcCombat.getStatus() + + " | Target: " + cleanedRequirement(targetNpcDescription)); + return true; + } + + private boolean searchForRequiredBox(QuestAPI.Quest quest, double progress) { + if (!collectBonus && !collectCargo) return false; + roamIfNeeded(); + updateProgressStatus(quest, progress, "Searching for required box"); + return true; + } + + private boolean reportMapArrival(QuestAPI.Quest quest, double progress) { + if (targetMapName == null || !isOnMap(targetMapName)) return false; + updateProgressStatus(quest, progress, "Reached target map"); + return true; } private boolean runLocalSafety() { @@ -937,7 +1002,7 @@ private boolean runLocalSafety() { .filter(Portal::isValid) .min(Comparator.comparingDouble(hero::distanceTo)) .ifPresent(portal -> movement.moveTo(portal)); - status = "Yerel güvenlik: HP %" + (int) Math.round(hp * 100d) + "; portal yanında bekleniyor"; + status = "Local safety: HP " + (int) Math.round(hp * 100d) + "% ; waiting near portal"; return true; } @@ -962,7 +1027,7 @@ private boolean ensureQuestStation() { attack.stopAttack(); hero.setRoamMode(); movement.moveTo(station.get()); - status = QUEST_BASE_MAP + " ana görev istasyonuna yaklaşılıyor: " + (int) Math.round(distance); + status = "Approaching main quest station on " + QUEST_BASE_MAP + ": " + (int) Math.round(distance); return false; } @@ -992,7 +1057,7 @@ private void navigateToQuestBase() { Optional destination = starSystem.findMap(QUEST_BASE_MAP); if (destination.isEmpty() || !starSystem.isAccessible(destination.get())) { attack.stopAttack(); - status = "Görev merkezi harita verisi bekleniyor: " + QUEST_BASE_MAP; + status = "Waiting for quest-center map data: " + QUEST_BASE_MAP; return; } @@ -1002,7 +1067,7 @@ private void navigateToQuestBase() { // normal loading state, so wait for the next tick instead of // entering a permanent safe pause. attack.stopAttack(); - status = "Görev merkezi rotası yükleniyor: " + QUEST_BASE_MAP; + status = "Loading quest-center route: " + QUEST_BASE_MAP; return; } @@ -1010,7 +1075,7 @@ private void navigateToQuestBase() { hero.setRoamMode(); if (hero.distanceTo(portal) > 220d) movement.moveTo(portal); else movement.jumpPortal(portal); - status = "Görev merkezi için " + QUEST_BASE_MAP + " haritasına gidiliyor"; + status = "Travelling to " + QUEST_BASE_MAP + " for the quest center"; } private void navigateTo(String mapName) { @@ -1019,7 +1084,7 @@ private void navigateTo(String mapName) { // Map/route data can be temporarily unavailable while a portal jump is // loading. Keep the daily plan intact and retry on the next tick. attack.stopAttack(); - status = "Harita rotası yükleniyor: " + mapName; + status = "Loading map route: " + mapName; return; } Portal portal = starSystem.findNext(destination.get()); @@ -1105,7 +1170,7 @@ private boolean recoverStagnantPlan(QuestAPI.Quest quest, double progress, long movement.moveRandom(); } updateProgressStatus(quest, progress, - "Quest Engine ilerleme yenilemesi: hedef ve rota yeniden değerlendirildi"); + "Quest Engine recovery: target and route reevaluated"); return true; } @@ -1116,28 +1181,29 @@ private void resetProgressTracking() { private void updateProgressStatus(QuestAPI.Quest quest, double progress, String action) { int percent = (int) Math.round(progress * 100d); - status = "Günlük görev | Tamamlanan: " + completedQuestIds.size() + + status = "Daily quest | Completed: " + completedQuestIds.size() + " | " + safeTitle(quest) + " | %" + percent + " | " + action; } private void pauseSafely(String reason) { stopCurrentAction(); state = State.PAUSED; - status = "Güvenli duraklama: " + reason; + status = "Safe pause: " + reason; } private void finishAll() { stopCurrentAction(); + npcCombat.shutdownPet(); state = State.DONE; - nextActionAt = java.time.LocalDate.now().plusDays(1) - .atStartOfDay(java.time.ZoneId.systemDefault()) + ZoneId localZone = ZoneId.systemDefault(); + nextActionAt = LocalDate.now(localZone).plusDays(1) + .atStartOfDay(localZone) .toInstant().toEpochMilli(); // Keep DarkBot ticking briefly so PetManager can send and observe the // actual PET deactivation before the bot's running flag is cleared. nextRoamAt = System.currentTimeMillis() + 5_000L; - status = "TÜM GÜNLÜK GÖREVLER TAMAMLANDI | Bugün yeniden taranmayacak" + - " | Tamamlanan: " + completedQuestIds.size() + - " | PET kapatılıyor | Bot PET kapanınca duracak"; + status = "ALL DAILY QUESTS COMPLETED | Completed: " + completedQuestIds.size() + + " | PET is shutting down | Bot will stop afterwards"; } private void stopCurrentAction() { @@ -1157,16 +1223,25 @@ private void clearPlan() { targetCoordinates = null; } - String getNpcLocatorTargetDescription() { + private String getNpcLocatorTargetDescription() { if (state != State.RUNNING || targetNpcDescription == null) return null; if (targetMapName != null && !isOnMap(targetMapName)) return null; return targetNpcDescription; } + public boolean hasNpcLocatorTarget() { + return getNpcLocatorTargetDescription() != null; + } + + public boolean matchesNpcLocatorTarget(String npcName) { + String description = getNpcLocatorTargetDescription(); + return DailyTaskPlanner.matchesNpcName(description, npcName); + } + private String safeTitle(QuestAPI.Quest quest) { String title = quest == null ? null : quest.getTitle(); if (title == null || title.isBlank() || title.equalsIgnoreCase("ERROR")) { - return quest == null ? "Görev" : "Görev #" + quest.getId(); + return quest == null ? "Quest" : "Quest #" + quest.getId(); } return title; } @@ -1192,7 +1267,7 @@ public void onTickStopped() { movement.stop(false); hero.setRoamMode(); if (state != State.PAUSED && state != State.DONE) { - status = "DarkBot beklemede; günlük görev planı korundu"; + status = "DarkBot idle; daily quest plan retained"; } } @@ -1203,7 +1278,7 @@ public String getStatus() { @Override public String getStoppedStatus() { - return "DailyTaskAPI durduruldu | " + status; + return "DailyTaskAPI stopped | " + status; } @Override diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java index 1efa353b..4e509b98 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java @@ -13,9 +13,6 @@ public final class DailyTaskConfig { @Number(min = 1, max = 10, step = 1) public int maxSelectionRetries = 1; - @Number(min = 1, max = 5, step = 1) - public int maxMenuScanPasses = 1; - @Number(min = 300, max = 750, step = 25) public int attackRadius = 500; @@ -25,5 +22,4 @@ public final class DailyTaskConfig { @Number(min = 60, max = 900, step = 30) public int stagnationTimeoutSeconds = 300; - public boolean stopWhenAllDailyTasksAreDone = true; } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java index bcc8fdfd..5d1e5c6c 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -14,7 +14,7 @@ import java.util.regex.Pattern; final class DailyTaskPlanner { - private static final Pattern MAP_PATTERN = Pattern.compile("(?i)([1-3]-BL|[1-5]-[1-8]|4-[1-5]|5-[1-3])"); + private static final Pattern MAP_PATTERN = Pattern.compile("(?i)([1-3]-BL|[1-3]-[1-8]|4-[1-5]|5-[1-3])"); private static final double DAILY_DURATION_MS = 86_400_000d; private DailyTaskPlanner() { @@ -39,18 +39,18 @@ static boolean isDailyType(String type) { static boolean isOnlyTetrathrinReward(Collection rewards) { if (rewards == null || rewards.isEmpty()) return false; - boolean hasMaterialReward = false; + boolean hasTetrathrin = false; for (QuestAPI.Reward reward : rewards) { - if (reward == null || reward.getAmount() <= 0) continue; - String type = normalize(reward.getType()); - // Uridium is always valuable for this account. Accept the quest even - // when its only material reward is Tetrathrin. - if (type.contains("uridium")) return false; - if (type.startsWith("currency ")) continue; - hasMaterialReward = true; - if (!type.contains("tetrathrin")) return false; + if (reward != null && reward.getAmount() > 0) { + String type = normalize(reward.getType()); + if (type.contains("uridium")) return false; + if (!type.startsWith("currency ")) { + if (!type.contains("tetrathrin")) return false; + hasTetrathrin = true; + } + } } - return hasMaterialReward; + return hasTetrathrin; } static boolean hasUridiumReward(Collection rewards) { @@ -67,13 +67,7 @@ static List actionable(QuestAPI.Quest quest) { List result = new ArrayList<>(); if (quest == null) return result; for (QuestAPI.Requirement requirement : flatten(quest.getRequirements())) { - if (!requirement.isEnabled() || requirement.isCompleted()) continue; - QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); - if (type == QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE || - type == QuestAPI.Requirement.RequirementType.HASTE || - type == QuestAPI.Requirement.RequirementType.TIMER || - type == QuestAPI.Requirement.RequirementType.COUNTDOWN) continue; - result.add(requirement); + if (isActionable(requirement, false)) result.add(requirement); } return result; } @@ -112,6 +106,17 @@ static Optional findNpc(String description, Collection npcNames) .map(candidate -> candidate.original); } + static boolean matchesNpcName(String description, String npcName) { + if (description == null || npcName == null) return false; + String target = normalizeNpcName(description); + String candidate = normalizeNpcName(npcName); + boolean wantsBoss = target.contains("boss"); + boolean wantsUber = target.contains("uber"); + return !candidate.isBlank() && target.contains(candidate) && + wantsBoss == candidate.startsWith("boss ") && + wantsUber == candidate.startsWith("uber"); + } + static Optional findMap(String description) { if (description == null) return Optional.empty(); Matcher matcher = MAP_PATTERN.matcher(description); @@ -125,26 +130,38 @@ static Optional preferredMapForNpc(String description, String companyPre if (npc.contains("uber")) return Optional.of("4-5"); if (description.contains("StreuneR")) return Optional.of(prefix + "-8"); - if (npc.contains("protecgit") || npc.contains("protegit") || npc.contains("cubikon")) { - return Optional.of(prefix + "-6"); - } - if (npc.contains("boss kristallon")) return Optional.of(prefix + "-7"); - if (npc.contains("boss lordakium")) return Optional.of(prefix + "-5"); - if (npc.contains("kristallin") || npc.contains("kristallon")) return Optional.of(prefix + "-6"); - // The account owner's requested Lordakium farming route is X-6. - if (npc.contains("lordakium")) return Optional.of(prefix + "-6"); - if (npc.contains("sibelonit")) return Optional.of(prefix + "-5"); - if (npc.contains("sibelon")) return Optional.of(prefix + "-4"); - if (npc.contains("saimon") || npc.contains("mordon") || npc.contains("devolarium")) { - return Optional.of(prefix + "-3"); - } - if (npc.contains("lordakia") || npc.contains("streuner")) return Optional.of(prefix + "-2"); + String homeMap = homeMapForNpc(npc, prefix); + if (homeMap != null) return Optional.of(homeMap); + return pirateMapForNpc(npc); + } + + private static String homeMapForNpc(String npc, String prefix) { + if (containsAny(npc, "protecgit", "protegit", "cubikon")) return prefix + "-6"; + if (npc.contains("boss kristallon")) return prefix + "-7"; + if (npc.contains("boss lordakium")) return prefix + "-5"; + if (containsAny(npc, "kristallin", "kristallon", "lordakium")) return prefix + "-6"; + if (npc.contains("sibelonit")) return prefix + "-5"; + if (npc.contains("sibelon")) return prefix + "-4"; + if (containsAny(npc, "saimon", "mordon", "devolarium")) return prefix + "-3"; + if (containsAny(npc, "lordakia", "streuner")) return prefix + "-2"; + return null; + } + + private static Optional pirateMapForNpc(String npc) { if (npc.contains("battleray")) return Optional.of("5-3"); - if (npc.contains("interceptor") || npc.contains("barracuda") || - npc.contains("saboteur") || npc.contains("annihilator")) return Optional.of("5-2"); + if (containsAny(npc, "interceptor", "barracuda", "saboteur", "annihilator")) { + return Optional.of("5-2"); + } return Optional.empty(); } + private static boolean containsAny(String value, String... candidates) { + for (String candidate : candidates) { + if (value.contains(candidate)) return true; + } + return false; + } + static Optional findOre(String description) { String text = normalize(description); if (text.contains("prometium")) return Optional.of(OreAPI.Ore.PROMETIUM); @@ -161,25 +178,23 @@ static Optional findOre(String description) { } static String normalizeNpcName(String value) { - String normalized = normalize(value) + return normalize(value) .replace('-', ' ') .replaceAll("\\b(delta|alpha|beta|gamma)\\s*\\d+\\b", " ") .replaceAll("\\b[a-z]\\d+\\b", " ") .replaceAll("\\s+", " ") .trim(); - return normalized; } static String normalize(String value) { if (value == null) return ""; - String ascii = Normalizer.normalize(value, Normalizer.Form.NFD) + return Normalizer.normalize(value, Normalizer.Form.NFD) .replaceAll("\\p{M}+", "") .toLowerCase(Locale.ROOT) .replace('ı', 'i') .replaceAll("[^a-z0-9-]+", " ") .replaceAll("\\s+", " ") .trim(); - return ascii; } static double progress(QuestAPI.Quest quest) { @@ -187,27 +202,36 @@ static double progress(QuestAPI.Quest quest) { if (requirements.isEmpty()) return 0d; double sum = 0d; for (QuestAPI.Requirement requirement : requirements) { - double goal = requirement.getGoal(); - sum += goal <= 0d ? (requirement.isCompleted() ? 1d : 0d) - : Math.max(0d, Math.min(1d, requirement.getProgress() / goal)); + sum += requirementProgress(requirement); } return sum / requirements.size(); } + private static double requirementProgress(QuestAPI.Requirement requirement) { + double goal = requirement.getGoal(); + if (goal <= 0d) return requirement.isCompleted() ? 1d : 0d; + return Math.max(0d, Math.min(1d, requirement.getProgress() / goal)); + } + private static List actionableIncludingCompleted(QuestAPI.Quest quest) { List result = new ArrayList<>(); if (quest == null) return result; for (QuestAPI.Requirement requirement : flatten(quest.getRequirements())) { - QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); - if (type == QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE || - type == QuestAPI.Requirement.RequirementType.HASTE || - type == QuestAPI.Requirement.RequirementType.TIMER || - type == QuestAPI.Requirement.RequirementType.COUNTDOWN) continue; - result.add(requirement); + if (isActionable(requirement, true)) result.add(requirement); } return result; } + private static boolean isActionable(QuestAPI.Requirement requirement, boolean includeCompleted) { + if (requirement == null || !requirement.isEnabled()) return false; + if (!includeCompleted && requirement.isCompleted()) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type != QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE && + type != QuestAPI.Requirement.RequirementType.HASTE && + type != QuestAPI.Requirement.RequirementType.TIMER && + type != QuestAPI.Requirement.RequirementType.COUNTDOWN; + } + private static final class NpcCandidate { private final String original; private final String clean; diff --git a/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java b/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java index 61b0ad4e..09fef24f 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/QuestMenuSource.java @@ -41,22 +41,16 @@ int y() { } private static final class Sprite { - private final long address; private final int x; private final int y; private final List children; - private Sprite(long address, int x, int y, List children) { - this.address = address; + private Sprite(int x, int y, List children) { this.x = x; this.y = y; this.children = children; } - private long address() { - return address; - } - private int x() { return x; } @@ -73,14 +67,12 @@ private List children() { private static final class Row { private final int x; private final int y; - private final int step; private final List childXs; private final int score; - private Row(int x, int y, int step, List childXs, int score) { + private Row(int x, int y, List childXs, int score) { this.x = x; this.y = y; - this.step = step; this.childXs = childXs; this.score = score; } @@ -93,10 +85,6 @@ private int y() { return y; } - private int step() { - return step; - } - private List childXs() { return childXs; } @@ -120,16 +108,16 @@ private int score() { List discoverSelectors() { lastError = ""; - if (questGui == null) return fail("Görev penceresi bulunamadı"); + if (questGui == null) return fail("Quest window not found"); if (memory == null || nativeQuestGui == null) { - return fail("DarkBot görev kaynak ağacı kullanılamıyor"); + return fail("DarkBot quest source tree is unavailable"); } try { long rootAddress = nativeQuestGui.getAddress(); - if (rootAddress == 0L) return fail("Görev kaynak adresi henüz hazır değil"); + if (rootAddress == 0L) return fail("Quest source address is not ready"); Sprite root = readSprite(rootAddress, 0, new HashSet<>()); - if (root == null) return fail("Görev kaynak ağacı okunamadı"); + if (root == null) return fail("Quest source tree could not be read"); List rows = new ArrayList<>(); // The root coordinate is the quest window's screen position. @@ -137,7 +125,7 @@ List discoverSelectors() { // window position, animation and resolution cannot affect discovery. findRows(root, -root.x(), -root.y(), 0, rows); Row best = rows.stream().max(Comparator.comparingInt(Row::score)).orElse(null); - if (best == null) return fail("Görev seçici satırı kaynak ağacında bulunamadı"); + if (best == null) return fail("Quest selector row not found in source tree"); List selectors = new ArrayList<>(); for (int childX : best.childXs()) { @@ -148,7 +136,7 @@ List discoverSelectors() { } return List.copyOf(selectors); } catch (RuntimeException error) { - return fail("Görev kaynak ağacı hatası: " + error.getClass().getSimpleName()); + return fail("Quest source tree error: " + error.getClass().getSimpleName()); } } @@ -171,7 +159,7 @@ private Sprite readSprite(long address, int depth, Set visited) { try { nativeQuestGui.forEachSpriteChild(address, childAddresses::add); } catch (RuntimeException error) { - lastError = "Görev kaynak çocuğu okunamadı: " + describe(error); + lastError = "Quest source child could not be read: " + describe(error); } List children = new ArrayList<>(); @@ -179,7 +167,7 @@ private Sprite readSprite(long address, int depth, Set visited) { Sprite child = readSprite(childAddress, depth + 1, visited); if (child != null) children.add(child); } - return new Sprite(address, coordinate(address, 88L), coordinate(address, 92L), List.copyOf(children)); + return new Sprite(coordinate(address, 88L), coordinate(address, 92L), List.copyOf(children)); } private int coordinate(long address, long offset) { @@ -217,7 +205,7 @@ private Row rowCandidate(Sprite sprite, int absoluteX, int absoluteY, int depth) if (differences.stream().anyMatch(value -> Math.abs(value - step) > 2)) return null; int score = children.size() * 100 + Math.min(absoluteX, 500) - absoluteY * 2; - return new Row(absoluteX, absoluteY, step, xs, score); + return new Row(absoluteX, absoluteY, xs, score); } private int median(List values) { @@ -231,9 +219,11 @@ private List fail(String message) { return List.of(); } - private String describe(Throwable error) { - String message = error == null ? null : error.getMessage(); - return error == null ? "bilinmeyen hata" : error.getClass().getSimpleName() + - (message == null || message.isBlank() ? "" : ": " + message); + private String describe(RuntimeException error) { + if (error == null) return "unknown error"; + String description = error.getClass().getSimpleName(); + String message = error.getMessage(); + if (message == null || message.isBlank()) return description; + return description + ": " + message; } } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java b/src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java similarity index 74% rename from src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java rename to src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java index 4c63e243..ea61d770 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcPetLocator.java +++ b/src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java @@ -1,4 +1,6 @@ -package dev.shared.berke.dailytaskapi; +package dev.shared.berke.petlocator; + +import dev.shared.berke.dailytaskapi.DailyTaskAPI; import eu.darkbot.api.PluginAPI; import eu.darkbot.api.extensions.Feature; @@ -57,9 +59,8 @@ public PetAPI.LocatorPick getNpcLocatorPick( Collection available) { DailyTaskAPI daily = activeDaily(); if (daily == null) return PetGearSupplier.super.getNpcLocatorPick(available); - String description = daily.getNpcLocatorTargetDescription(); return available.stream() - .filter(pick -> matches(description, pick)) + .filter(pick -> matches(daily, pick)) .findFirst() .orElse(null); } @@ -68,7 +69,7 @@ public PetAPI.LocatorPick getNpcLocatorPick( public Integer getNpcPickPriority(PetAPI.LocatorPick pick) { DailyTaskAPI daily = activeDaily(); if (daily == null) return PetGearSupplier.super.getNpcPickPriority(pick); - return matches(daily.getNpcLocatorTargetDescription(), pick) ? 0 : null; + return matches(daily, pick) ? 0 : null; } private DailyTaskAPI activeDaily() { @@ -77,19 +78,12 @@ private DailyTaskAPI activeDaily() { if (!(module instanceof DailyTaskAPI)) module = bot.getModule(); if (module instanceof DailyTaskAPI) { DailyTaskAPI daily = (DailyTaskAPI) module; - if (daily.getNpcLocatorTargetDescription() != null) return daily; + if (daily.hasNpcLocatorTarget()) return daily; } return null; } - private boolean matches(String description, PetAPI.LocatorPick pick) { - if (description == null || pick == null || pick.getName() == null) return false; - String target = DailyTaskPlanner.normalizeNpcName(description); - String candidate = DailyTaskPlanner.normalizeNpcName(pick.getName()); - boolean wantsBoss = target.contains("boss"); - boolean wantsUber = target.contains("uber"); - return !candidate.isBlank() && target.contains(candidate) && - wantsBoss == candidate.startsWith("boss ") && - wantsUber == candidate.startsWith("uber"); + private boolean matches(DailyTaskAPI daily, PetAPI.LocatorPick pick) { + return pick != null && daily.matchesNpcLocatorTarget(pick.getName()); } } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java similarity index 91% rename from src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java rename to src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java index 684a176e..6af4b680 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleaner.java +++ b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java @@ -1,4 +1,4 @@ -package dev.shared.berke.dailytaskapi; +package dev.shared.berke.ramcleaner; import com.github.manolo8.darkbot.Main; import com.github.manolo8.darkbot.core.IDarkBotAPI; @@ -19,7 +19,7 @@ * delete files.

*/ @Feature(name = "BerkePlugin RAM Cleaner", - description = "Bellek eşiği aşıldığında DarkBot oyun istemcisinin RAM kullanımını azaltır", + description = "Reduces the game client's working set above a configurable memory threshold", enabledByDefault = true) public final class DailyRamCleaner implements Behavior, Configurable { private static final long MIN_RETRY_AFTER_COMBAT_MS = 5_000L; @@ -89,10 +89,10 @@ public void onTickBehavior() { if (darkbotApi.hasCapability(Capability.HANDLER_CLEAR_RAM)) { darkbotApi.emptyWorkingSet(); } - System.out.println("[BerkePlugin RAM Cleaner] Temizleme tamamlandı: " + System.out.println("[BerkePlugin RAM Cleaner] Cleanup completed: " + memoryUsageMb + " MB"); } catch (RuntimeException cleanupFailure) { - System.err.println("[BerkePlugin RAM Cleaner] Temizleme başarısız: " + System.err.println("[BerkePlugin RAM Cleaner] Cleanup failed: " + cleanupFailure.getMessage()); } } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java similarity index 88% rename from src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java rename to src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java index 37c8dd9c..afb5adf8 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerConfig.java +++ b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java @@ -1,9 +1,9 @@ -package dev.shared.berke.dailytaskapi; +package dev.shared.berke.ramcleaner; import eu.darkbot.api.config.annotations.Configuration; import eu.darkbot.api.config.annotations.Number; -@Configuration("dailytaskapi.ram_cleaner") +@Configuration("berke.ram_cleaner") public final class DailyRamCleanerConfig { public boolean enabled = true; diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java similarity index 91% rename from src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java rename to src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java index d9b913f6..202e9e55 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicy.java +++ b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java @@ -1,4 +1,4 @@ -package dev.shared.berke.dailytaskapi; +package dev.shared.berke.ramcleaner; /** Pure decision logic kept separate so RAM cleanup rules can be unit tested. */ final class DailyRamCleanerPolicy { diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index 36a9295a..9717e660 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -21,8 +21,8 @@ "dev.shared.do_gamer.task.autobuy.Autobuy", "dev.shared.halizeur.log_overlay.LogOverlay", "dev.shared.berke.dailytaskapi.DailyTaskAPI", - "dev.shared.berke.dailytaskapi.DailyNpcPetLocator", - "dev.shared.berke.dailytaskapi.DailyRamCleaner" + "dev.shared.berke.petlocator.DailyNpcPetLocator", + "dev.shared.berke.ramcleaner.DailyRamCleaner" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar" diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java deleted file mode 100644 index efa1b0db..00000000 --- a/src/test/java/dev/shared/berke/dailytaskapi/DailyRamCleanerPolicyTest.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.shared.berke.dailytaskapi; - -public final class DailyRamCleanerPolicyTest { - public static void main(String[] args) { - assertFalse(DailyRamCleanerPolicy.shouldClean(1199, 1200, true, false), - "below threshold"); - assertTrue(DailyRamCleanerPolicy.shouldClean(1200, 1200, true, false), - "at threshold while idle"); - assertFalse(DailyRamCleanerPolicy.shouldClean(1800, 1200, true, true), - "combat protection"); - assertTrue(DailyRamCleanerPolicy.shouldClean(1800, 1200, false, true), - "combat protection disabled"); - System.out.println("DailyRamCleanerPolicyTest: OK"); - } - - private static void assertTrue(boolean value, String message) { - if (!value) throw new AssertionError(message); - } - - private static void assertFalse(boolean value, String message) { - if (value) throw new AssertionError(message); - } -} diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java index e127f87a..cc624764 100644 --- a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java @@ -2,108 +2,129 @@ import eu.darkbot.api.managers.OreAPI; import eu.darkbot.api.managers.QuestAPI; +import org.junit.jupiter.api.Test; import java.util.List; -public final class DailyTaskPlannerTest { - public static void main(String[] args) { - QuestAPI.Requirement dailyTimer = requirement("Bu görevi 1 gün içinde bitir", "REAL_TIME_HASTE", - QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE, 10, 86_400_000, false); - QuestAPI.Requirement kill = requirement("Lordakium imha et.", "KILL_NPC", +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class DailyTaskPlannerTest { + private static final double DAY_MS = 86_400_000d; + + @Test + void identifiesDailyQuestsFromTheSourceTimer() { + QuestAPI.Requirement timer = requirement("Complete within one day", "REAL_TIME_HASTE", + QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE, 10, DAY_MS, false); + QuestAPI.Requirement kill = requirement("Destroy Lordakium", "KILL_NPC", QuestAPI.Requirement.RequirementType.KILL_NPC, 2, 5, false); - QuestAPI.Quest daily = quest(1, true, false, "Galaksi Politikaları", List.of(kill, dailyTimer)); - require(DailyTaskPlanner.isDaily(daily), "24 saatlik görev günlük tanınmalı"); - QuestAPI.Quest normal = quest(2, true, false, "Normal", List.of(kill)); - require(!DailyTaskPlanner.isDaily(normal), "süresiz görev günlük sayılmamalı"); - QuestAPI.Quest dailyOffer = quest(3, false, false, "Teklif", List.of(kill, dailyTimer)); - require(DailyTaskPlanner.isDaily(dailyOffer), - "henüz aktif olmayan istasyon teklifi 24 saat şartından günlük tanınmalı"); + assertTrue(DailyTaskPlanner.isDaily(quest(1, true, false, List.of(kill, timer)))); + assertTrue(DailyTaskPlanner.isDaily(quest(2, false, false, List.of(kill, timer)))); + assertFalse(DailyTaskPlanner.isDaily(quest(3, true, false, List.of(kill)))); + assertTrue(DailyTaskPlanner.isDailyType("questType_daily1")); + assertFalse(DailyTaskPlanner.isDailyType("questType_kill")); + } - QuestAPI.Requirement mapChild = requirement("1-4 haritasında", "MAP", + @Test + void retainsNestedNpcAndMapRequirements() { + QuestAPI.Requirement map = requirement("On map 1-4", "MAP", QuestAPI.Requirement.RequirementType.MAP, 0, 0, false); - QuestAPI.Requirement nestedMordon = requirement("Mordon imha et.", "KILL_NPC", - QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 8, false, List.of(mapChild)); - QuestAPI.Quest nestedNpcQuest = quest(4, true, false, "Hedef: Biliniyor", - List.of(nestedMordon, dailyTimer)); - require(DailyTaskPlanner.actionable(nestedNpcQuest).contains(nestedMordon), - "altında harita şartı bulunan NPC öldürme hedefi kaybolmamalı"); - require(DailyTaskPlanner.actionable(nestedNpcQuest).contains(mapChild), - "NPC hedefinin altındaki harita şartı da korunmalı"); - - DailyQuestConditionEngine.Plan nestedPlan = DailyQuestConditionEngine.build(nestedNpcQuest, "1"); - require("1-4".equals(nestedPlan.targetMapName()), - "Quest Engine must include a nested MAP condition"); - require(nestedPlan.targetNpcDescription().contains("Mordon"), - "Quest Engine must include a nested NPC condition"); + QuestAPI.Requirement npc = requirement("Destroy Mordon", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 8, false, List.of(map)); + QuestAPI.Quest daily = quest(4, true, false, List.of(npc, dailyTimer())); + + assertTrue(DailyTaskPlanner.actionable(daily).contains(npc)); + assertTrue(DailyTaskPlanner.actionable(daily).contains(map)); + + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(daily, "1"); + assertEquals("1-4", plan.targetMapName()); + assertTrue(plan.targetNpcDescription().contains("Mordon")); + } + @Test + void parsesCoordinatesAndTypedMapVisits() { QuestAPI.Requirement coordinates = requirement("X: 12400 Y: 7600", "COORDINATES", QuestAPI.Requirement.RequirementType.COORDINATES, 0, 1, false); - QuestAPI.Quest coordinateQuest = quest(5, true, false, "Coordinates", List.of(coordinates, dailyTimer)); - DailyQuestConditionEngine.Plan coordinatePlan = DailyQuestConditionEngine.build(coordinateQuest, "1"); - require(coordinatePlan.targetCoordinates() != null && - coordinatePlan.targetCoordinates().x() == 12400 && - coordinatePlan.targetCoordinates().y() == 7600, - "Quest Engine must parse coordinates from the source condition"); - require(coordinatePlan.coordinatesOnly(), "coordinate-only plans must be recognized"); - - QuestAPI.Requirement visitMap = requirement("Visit map 1-8", "VISIT_MAP", - QuestAPI.Requirement.RequirementType.VISIT_MAP, 0, 1, false); - QuestAPI.Quest visitQuest = quest(6, true, false, "Map visit", List.of(visitMap, dailyTimer)); - require("1-8".equals(DailyQuestConditionEngine.build(visitQuest, "1").targetMapName()), - "Quest Engine must support typed VISIT_MAP conditions"); - require(DailyQuestConditionEngine.supports(visitQuest), - "a map visit must be considered executable"); + DailyQuestConditionEngine.Plan coordinatePlan = DailyQuestConditionEngine.build( + quest(5, true, false, List.of(coordinates, dailyTimer())), "1"); + assertEquals(12400, coordinatePlan.targetCoordinates().x()); + assertEquals(7600, coordinatePlan.targetCoordinates().y()); + assertTrue(coordinatePlan.coordinatesOnly()); - require(DailyTaskPlanner.isDailyType("questType_daily1"), - "quest catalog daily type should be recognized"); - require(!DailyTaskPlanner.isDailyType("questType_kill"), - "normal quest catalog type must not be daily"); + QuestAPI.Requirement visit = requirement("Visit map 1-8", "VISIT_MAP", + QuestAPI.Requirement.RequirementType.VISIT_MAP, 0, 1, false); + QuestAPI.Quest visitQuest = quest(6, true, false, List.of(visit, dailyTimer())); + assertEquals("1-8", DailyQuestConditionEngine.build(visitQuest, "1").targetMapName()); + assertTrue(DailyQuestConditionEngine.supports(visitQuest)); + } + @Test + void matchesNpcVariantsAndPreferredMaps() { List names = List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..", "( UberLordakium )"); - require(DailyTaskPlanner.findNpc("Mordon imha et.", List.of("-=[ Mordon ]=-")).isPresent(), - "decorated Mordon name should match the quest target"); - require(DailyTaskPlanner.findNpc("Lordakium imha et.", names).orElseThrow().equals("-=[ Lordakium ]=-"), - "normal Lordakium seçilmeli"); - require(DailyTaskPlanner.findNpc("Boss Lordakium imha et.", names).orElseThrow().contains("Boss Lordakium"), - "Boss Lordakium seçilmeli"); - require(DailyTaskPlanner.findNpc("Boss Sibelonit imha et.", List.of("-=[ Sibelonit ]=-")).isEmpty(), - "Boss isteniyorsa normal NPC kesinlikle seçilmemeli"); - require(DailyTaskPlanner.findNpc("UberLordakium imha et.", - List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..")).isEmpty(), - "Uber isteniyorsa normal veya Boss NPC seçilmemeli"); - require(DailyTaskPlanner.findMap("1-6 haritasına git").orElseThrow().equals("1-6"), - "harita ayrıştırılmalı"); - require(DailyTaskPlanner.preferredMapForNpc("Saimon imha et.", "1").orElseThrow().equals("1-3"), - "Saimon MMO X-3 haritasına yönlendirilmeli"); - require(DailyTaskPlanner.preferredMapForNpc("Lordakium imha et.", "1").orElseThrow().equals("1-6"), - "hesap sahibinin Lordakium X-6 tercihi korunmalı"); - require(DailyTaskPlanner.preferredMapForNpc("Boss Lordakium imha et.", "1").orElseThrow().equals("1-5"), - "Boss Lordakium MMO X-5 haritasına yönlendirilmeli"); - require(DailyTaskPlanner.preferredMapForNpc("UberKristallin imha et.", "1").orElseThrow().equals("4-5"), - "Uber NPC 4-5 haritasına yönlendirilmeli"); - require(DailyTaskPlanner.preferredMapForNpc("StreuneR imha et.", "1").orElseThrow().equals("1-8"), - "StreuneR X-8 haritasına yönlendirilmeli"); - require(DailyTaskPlanner.findOre("Prometium sat.").orElseThrow() == OreAPI.Ore.PROMETIUM, - "Prometium ayrıştırılmalı"); - require(DailyTaskPlanner.isOnlyTetrathrinReward(List.of( - reward("currency_experience", 1000), reward("currency_credits", 1000), - reward("resource_tetrathrin", 10))), - "currency rewards plus only Tetrathrin must be skipped"); - require(!DailyTaskPlanner.isOnlyTetrathrinReward(List.of( - reward("resource_tetrathrin", 10), reward("ammunition_laser_ucb-100", 5000))), - "a quest with Tetrathrin and another special reward must be accepted"); - require(!DailyTaskPlanner.isOnlyTetrathrinReward(List.of( - reward("resource_tetrathrin", 10), reward("currency_uridium", 1500))), - "a quest containing Uridium must be accepted even with Tetrathrin"); - require(!DailyTaskPlanner.isOnlyTetrathrinReward(List.of(reward("currency_experience", 1000))), - "currency-only rewards are not a Tetrathrin variant"); - require(DailyTaskPlanner.hasUridiumReward(List.of( - reward("currency_experience", 1000), reward("currency_uridium", 1500))), - "Uridium reward must be detected from the source reward type"); - require(!DailyTaskPlanner.hasUridiumReward(List.of(reward("resource_tetrathrin", 10))), - "a reward list without Uridium must be rejected"); - System.out.println("DailyTaskPlannerTest: OK"); + assertEquals("-=[ Lordakium ]=-", DailyTaskPlanner.findNpc("Destroy Lordakium", names).orElseThrow()); + assertTrue(DailyTaskPlanner.findNpc("Destroy Boss Lordakium", names).orElseThrow() + .contains("Boss Lordakium")); + assertTrue(DailyTaskPlanner.findNpc("Destroy Boss Sibelonit", List.of("-=[ Sibelonit ]=-")).isEmpty()); + assertTrue(DailyTaskPlanner.findNpc("Destroy UberLordakium", + List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..")).isEmpty()); + assertEquals("1-3", DailyTaskPlanner.preferredMapForNpc("Destroy Saimon", "1").orElseThrow()); + assertEquals("1-6", DailyTaskPlanner.preferredMapForNpc("Destroy Lordakium", "1").orElseThrow()); + assertEquals("1-5", DailyTaskPlanner.preferredMapForNpc("Destroy Boss Lordakium", "1").orElseThrow()); + assertEquals("4-5", DailyTaskPlanner.preferredMapForNpc("Destroy UberKristallin", "1").orElseThrow()); + assertEquals("1-8", DailyTaskPlanner.preferredMapForNpc("Destroy StreuneR", "1").orElseThrow()); + assertEquals(OreAPI.Ore.PROMETIUM, DailyTaskPlanner.findOre("Sell Prometium").orElseThrow()); + } + + @Test + void acceptsOnlyValidMapNames() { + assertEquals("1-6", DailyTaskPlanner.findMap("Travel to 1-6").orElseThrow()); + assertEquals("4-5", DailyTaskPlanner.findMap("Travel to 4-5").orElseThrow()); + assertEquals("5-3", DailyTaskPlanner.findMap("Travel to 5-3").orElseThrow()); + assertTrue(DailyTaskPlanner.findMap("Travel to 4-8").isEmpty()); + assertTrue(DailyTaskPlanner.findMap("Travel to 5-8").isEmpty()); + } + + @Test + void filtersOffersBySourceRewardTypes() { + assertTrue(DailyTaskPlanner.isOnlyTetrathrinReward(List.of( + reward("currency_experience", 1000), reward("currency_credits", 1000), + reward("resource_tetrathrin", 10)))); + assertFalse(DailyTaskPlanner.isOnlyTetrathrinReward(List.of( + reward("resource_tetrathrin", 10), reward("ammunition_laser_ucb-100", 5000)))); + assertFalse(DailyTaskPlanner.isOnlyTetrathrinReward(List.of( + reward("resource_tetrathrin", 10), reward("currency_uridium", 1500)))); + assertFalse(DailyTaskPlanner.isOnlyTetrathrinReward(List.of(reward("currency_experience", 1000)))); + assertTrue(DailyTaskPlanner.hasUridiumReward(List.of(reward("currency_uridium", 1500)))); + assertFalse(DailyTaskPlanner.hasUridiumReward(List.of(reward("resource_tetrathrin", 10)))); + } + + @Test + void calculatesProgressWithoutTimerRequirements() { + QuestAPI.Requirement partial = requirement("Destroy NPC", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 2, 5, false); + QuestAPI.Requirement timer = dailyTimer(); + assertEquals(0.4d, DailyTaskPlanner.progress( + quest(7, true, false, List.of(partial, timer))), 0.000001d); + } + + @Test + void handlesZeroGoalsCompletedRequirementsAndClamping() { + QuestAPI.Requirement incompleteZero = requirement("Visit target", "MAP", + QuestAPI.Requirement.RequirementType.MAP, 0, 0, false); + QuestAPI.Requirement completedZero = requirement("Visit target", "MAP", + QuestAPI.Requirement.RequirementType.MAP, 0, 0, true); + QuestAPI.Requirement overGoal = requirement("Destroy NPC", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 8, 5, false); + assertEquals(0d, DailyTaskPlanner.progress(quest(8, true, false, List.of(incompleteZero))), 0d); + assertEquals(1d, DailyTaskPlanner.progress(quest(9, true, true, List.of(completedZero))), 0d); + assertEquals(1d, DailyTaskPlanner.progress(quest(10, true, false, List.of(overGoal))), 0d); + } + + private static QuestAPI.Requirement dailyTimer() { + return requirement("Complete within one day", "REAL_TIME_HASTE", + QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE, 0, DAY_MS, false); } private static QuestAPI.Reward reward(String type, int amount) { @@ -135,20 +156,16 @@ private static QuestAPI.Requirement requirement(String description, String type, }; } - private static QuestAPI.Quest quest(int id, boolean active, boolean completed, String title, + private static QuestAPI.Quest quest(int id, boolean active, boolean completed, List requirements) { return new QuestAPI.Quest() { public int getId() { return id; } public boolean isActive() { return active; } - public String getTitle() { return title; } + public String getTitle() { return "Quest " + id; } public String getDescription() { return ""; } public boolean isCompleted() { return completed; } public List getRequirements() { return requirements; } public List getRewards() { return List.of(); } }; } - - private static void require(boolean condition, String message) { - if (!condition) throw new AssertionError(message); - } } diff --git a/src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java b/src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java new file mode 100644 index 00000000..2d2127f2 --- /dev/null +++ b/src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java @@ -0,0 +1,20 @@ +package dev.shared.berke.ramcleaner; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class DailyRamCleanerPolicyTest { + @Test + void cleansOnlyAtOrAboveTheThreshold() { + assertFalse(DailyRamCleanerPolicy.shouldClean(1199, 1200, true, false)); + assertTrue(DailyRamCleanerPolicy.shouldClean(1200, 1200, true, false)); + } + + @Test + void respectsCombatProtection() { + assertFalse(DailyRamCleanerPolicy.shouldClean(1800, 1200, true, true)); + assertTrue(DailyRamCleanerPolicy.shouldClean(1800, 1200, false, true)); + } +} From 95c34d6990d26d30baefb661266fe233d6e7e4b4 Mon Sep 17 00:00:00 2001 From: Berke Date: Wed, 12 Aug 2026 18:21:10 +0300 Subject: [PATCH 10/14] Fix Sonar quality gate findings --- .../DailyQuestConditionEngine.java | 16 +++++++++++----- .../berke/dailytaskapi/DailyTaskPlanner.java | 18 ++++++++++-------- .../berke/ramcleaner/DailyRamCleaner.java | 7 ++----- 3 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java index f2418431..eb9c2106 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java @@ -19,9 +19,12 @@ * module's saved configuration.

*/ final class DailyQuestConditionEngine { - private static final Pattern LABELED_COORDINATES = Pattern.compile( - "(?i)(?:x|x-koordinat(?:i|ı)?)\\s*[:=]?\\s*(-?\\d{1,5})\\D{1,24}" + - "(?:y|y-koordinat(?:i|ı)?)\\s*[:=]?\\s*(-?\\d{1,5})"); + private static final Pattern X_COORDINATE = Pattern.compile( + "\\bx(?:-koordinat[iı]?)?\\s*[:=]?\\s*(-?\\d{1,5})", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); + private static final Pattern Y_COORDINATE = Pattern.compile( + "\\by(?:-koordinat[iı]?)?\\s*[:=]?\\s*(-?\\d{1,5})", + Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); private static final Pattern PAIRED_COORDINATES = Pattern.compile( "(? findCoordinates(String description) { if (description == null || description.isBlank()) return Optional.empty(); String normalized = description.toLowerCase(Locale.ROOT); - Matcher labeled = LABELED_COORDINATES.matcher(normalized); - if (labeled.find()) return coordinates(labeled.group(1), labeled.group(2)); + Matcher labeledX = X_COORDINATE.matcher(normalized); + Matcher labeledY = Y_COORDINATE.matcher(normalized); + if (labeledX.find() && labeledY.find()) { + return coordinates(labeledX.group(1), labeledY.group(1)); + } Matcher paired = PAIRED_COORDINATES.matcher(normalized); if (paired.find()) return coordinates(paired.group(1), paired.group(2)); return Optional.empty(); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java index 5d1e5c6c..042ba3ec 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -41,18 +41,20 @@ static boolean isOnlyTetrathrinReward(Collection rewa if (rewards == null || rewards.isEmpty()) return false; boolean hasTetrathrin = false; for (QuestAPI.Reward reward : rewards) { - if (reward != null && reward.getAmount() > 0) { - String type = normalize(reward.getType()); - if (type.contains("uridium")) return false; - if (!type.startsWith("currency ")) { - if (!type.contains("tetrathrin")) return false; - hasTetrathrin = true; - } - } + if (!isPositiveReward(reward)) continue; + String type = normalize(reward.getType()); + if (type.contains("uridium")) return false; + if (type.startsWith("currency ")) continue; + if (!type.contains("tetrathrin")) return false; + hasTetrathrin = true; } return hasTetrathrin; } + private static boolean isPositiveReward(QuestAPI.Reward reward) { + return reward != null && reward.getAmount() > 0; + } + static boolean hasUridiumReward(Collection rewards) { if (rewards == null || rewards.isEmpty()) return false; for (QuestAPI.Reward reward : rewards) { diff --git a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java index 6af4b680..aff8ef67 100644 --- a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java +++ b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java @@ -85,15 +85,12 @@ public void onTickBehavior() { darkbotApi.clearCache(".*"); } - System.gc(); if (darkbotApi.hasCapability(Capability.HANDLER_CLEAR_RAM)) { darkbotApi.emptyWorkingSet(); } - System.out.println("[BerkePlugin RAM Cleaner] Cleanup completed: " - + memoryUsageMb + " MB"); } catch (RuntimeException cleanupFailure) { - System.err.println("[BerkePlugin RAM Cleaner] Cleanup failed: " - + cleanupFailure.getMessage()); + // Best-effort: DarkBot may temporarily reject cleanup while the + // game client is reconnecting. } } } From 0c3e2747af203c9224fb76d39a6957fd34f1187b Mon Sep 17 00:00:00 2001 From: Berke Date: Thu, 13 Aug 2026 18:34:23 +0300 Subject: [PATCH 11/14] Extend DailyTaskAPI policies and combat tasks --- README.md | 2 +- .../berke/dailytaskapi/DailyNpcCombat.java | 6 +- .../berke/dailytaskapi/DailyPlayerCombat.java | 159 ++++++++++++++++++ .../DailyQuestConditionEngine.java | 123 +++++++++++++- .../berke/dailytaskapi/DailyQuestPolicy.java | 83 +++++++++ .../berke/dailytaskapi/DailyTaskAPI.java | 111 +++++++----- .../berke/dailytaskapi/DailyTaskConfig.java | 32 +++- .../berke/dailytaskapi/DailyTaskPlanner.java | 9 + .../dailytaskapi/DailyTaskPlannerTest.java | 102 ++++++++++- 9 files changed, 577 insertions(+), 50 deletions(-) create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java diff --git a/README.md b/README.md index ba9f48a8..871d404b 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ We will seek to implement CI/CD in the repository so that developers have a good - Simple Galaxy Gate: Supports ABG, Delta, Epsilon, Zeta, Hades, Kuiper, LoW, Invasion, Mimesis, EBG, GoP, Treacherous, DSE, Fiesta, Trinity and Voyagers. By @do-gamer - Autobuy: Automatically buys boosters and special items from the shop at a configured interval. By @do-gamer - Log Overlay: Displays the latest in-game log messages on the canvas (top-center, auto-fade after 5s). Built-in keyword whitelist limits the displayed lines to gains and errors so the canvas does not get cluttered. By @Halizeur -- DailyTaskAPI: Detects supported daily quests from DarkBot's quest data, completes their NPC, map, collection and ore requirements, and accepts eligible Uridium-reward dailies. By Berke +- DailyTaskAPI: Detects supported daily quests from DarkBot's typed quest data, executes multi-step NPC, map, collection, ore and opt-in player-combat objectives, and applies configurable reward and safety policies before accepting them. By Berke - DailyTaskAPI PET Locator: Exposes the current quest NPC to DarkBot's PET Enemy Locator without changing another module's saved NPC selection. By Berke - RAM Cleaner: Requests cleanup through DarkBot's supported memory handler when a configurable threshold is exceeded, with optional combat protection. By Berke diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java index 32f811ed..ef11aaad 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java @@ -21,6 +21,7 @@ final class DailyNpcCombat extends LootModule { private final ConfigSetting petEnabledSetting; private final ConfigSetting keepRoamingPointSetting; private String targetDescription; + private int targetRadius = 590; private Boolean originalPetEnabledSetting; private Boolean originalKeepRoamingPointSetting; private Boolean originalPetRuntimeEnabled; @@ -32,8 +33,9 @@ final class DailyNpcCombat extends LootModule { keepRoamingPointSetting = config.requireConfig("general.roaming.keep"); } - void tick(String description) { + void tick(String description, int attackRadius) { targetDescription = description; + targetRadius = attackRadius; rememberUserSettings(); if (!Boolean.TRUE.equals(petEnabledSetting.getValue())) petEnabledSetting.setValue(true); super.onTickModule(); @@ -112,7 +114,7 @@ private boolean isUsableTarget(Npc npc) { return false; } String name = npc.getInfo() == null ? null : npc.getInfo().getName(); - if (name == null || movement.getClosestDistance(npc) >= 590d) return false; + if (name == null || movement.getClosestDistance(npc) >= targetRadius) return false; String target = DailyTaskPlanner.normalizeNpcName(targetDescription); String candidate = DailyTaskPlanner.normalizeNpcName(name); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java new file mode 100644 index 00000000..9dc31903 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java @@ -0,0 +1,159 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.PluginAPI; +import eu.darkbot.api.game.entities.Player; +import eu.darkbot.api.game.other.EntityInfo; +import eu.darkbot.api.managers.AttackAPI; +import eu.darkbot.api.managers.EntitiesAPI; +import eu.darkbot.api.managers.GroupAPI; +import eu.darkbot.api.managers.HeroAPI; +import eu.darkbot.api.managers.MovementAPI; + +import java.util.Comparator; + +/** Executes opt-in player-combat daily objectives with conservative filters. */ +final class DailyPlayerCombat { + private final EntitiesAPI entities; + private final GroupAPI group; + private final MovementAPI movement; + private final AttackAPI attack; + private final HeroAPI hero; + + private long searchStartedAt; + private long nextRoamAt; + private int deaths; + private boolean previouslyAlive = true; + private String status = "waiting"; + + DailyPlayerCombat(PluginAPI api) { + entities = api.requireAPI(EntitiesAPI.class); + group = api.requireAPI(GroupAPI.class); + movement = api.requireAPI(MovementAPI.class); + attack = api.requireAPI(AttackAPI.class); + hero = api.requireAPI(HeroAPI.class); + } + + void startQuest() { + searchStartedAt = 0L; + nextRoamAt = 0L; + deaths = 0; + previouslyAlive = hero.getHealth().hpPercent() > 0d; + status = "searching for an eligible player"; + } + + void tick(String objectiveDescription, DailyTaskConfig config) { + long now = System.currentTimeMillis(); + observeHeroState(); + if (hero.getHealth().hpPercent() <= 0d) { + stopAttackOnly(); + status = "waiting for ship recovery"; + return; + } + + Player target = attack.getTargetAs(Player.class); + if (!isEligible(target, objectiveDescription, config)) { + stopAttackOnly(); + target = entities.getPlayers().stream() + .filter(player -> isEligible(player, objectiveDescription, config)) + .min(playerComparator(config)) + .orElse(null); + attack.setTarget(target); + } + + if (target == null) { + if (searchStartedAt == 0L) searchStartedAt = now; + if (!movement.isMoving() || now >= nextRoamAt) { + movement.moveRandom(); + nextRoamAt = now + 2_500L; + } + status = "searching for an eligible enemy player"; + return; + } + + searchStartedAt = 0L; + double distance = hero.distanceTo(target); + if (distance > config.playerAttackRadius) { + hero.setRoamMode(); + movement.moveTo(target); + status = "approaching player at " + (int) Math.round(distance); + return; + } + + movement.stop(false); + attack.tryLockAndAttack(); + status = "attacking eligible enemy player"; + } + + boolean exceededSafetyLimits(DailyTaskConfig config) { + boolean deathLimit = config.maxPlayerDeaths > 0 && deaths >= config.maxPlayerDeaths; + boolean searchLimit = searchStartedAt > 0L && + System.currentTimeMillis() - searchStartedAt >= config.playerSearchTimeoutSeconds * 1_000L; + return deathLimit || searchLimit; + } + + void observeHeroState() { + observeDeath(); + } + + String failureReason(DailyTaskConfig config) { + if (config.maxPlayerDeaths > 0 && deaths >= config.maxPlayerDeaths) { + return "player-combat death limit reached (" + deaths + ")"; + } + return "no eligible player found before the search timeout"; + } + + String getStatus() { + return status + " | deaths: " + deaths; + } + + void stopCombat() { + stopAttackOnly(); + searchStartedAt = 0L; + } + + private void stopAttackOnly() { + if (attack.getTargetAs(Player.class) != null) { + attack.stopAttack(); + attack.setTarget(null); + } + } + + private void observeDeath() { + boolean alive = hero.getHealth().hpPercent() > 0d; + if (!alive && previouslyAlive) deaths++; + previouslyAlive = alive; + } + + private Comparator playerComparator(DailyTaskConfig config) { + Comparator distance = Comparator.comparingDouble(hero::distanceTo); + if (!config.prioritizePlayersAttackingMe) return distance; + return Comparator.comparing((Player player) -> !player.isAttacking(hero)).thenComparing(distance); + } + + private boolean isEligible(Player player, String objectiveDescription, DailyTaskConfig config) { + if (player == null || !player.isValid() || !player.isSelectable() || + player.isInvisible() || player.getId() == hero.getId()) { + return false; + } + EntityInfo info = player.getEntityInfo(); + if (info == null) return false; + if (!matchesRequiredFaction(info, objectiveDescription)) return false; + if (config.avoidAlliedAndGroupPlayers && isProtected(player, info)) return false; + return !config.enemyPlayersOnly || info.isEnemy() || player.isBlacklisted() || + info.getClanDiplomacy() == EntityInfo.Diplomacy.WAR; + } + + private boolean matchesRequiredFaction(EntityInfo info, String description) { + String normalized = DailyTaskPlanner.normalize(description); + if (normalized.contains("mmo")) return info.getFaction() == EntityInfo.Faction.MMO; + if (normalized.contains("eic")) return info.getFaction() == EntityInfo.Faction.EIC; + if (normalized.contains("vru")) return info.getFaction() == EntityInfo.Faction.VRU; + return true; + } + + private boolean isProtected(Player player, EntityInfo info) { + EntityInfo.Diplomacy diplomacy = info.getClanDiplomacy(); + return group.getMember(player) != null || diplomacy == EntityInfo.Diplomacy.ALLIED || + diplomacy == EntityInfo.Diplomacy.NOT_ATTACK_PACT; + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java index eb9c2106..9275e66f 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java @@ -4,6 +4,7 @@ import eu.darkbot.api.managers.OreAPI; import eu.darkbot.api.managers.QuestAPI; +import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -28,25 +29,52 @@ final class DailyQuestConditionEngine { private static final Pattern PAIRED_COORDINATES = Pattern.compile( "(? steps; - Plan(String targetMapName, String targetNpcDescription, OreAPI.Ore oreToSell, + Plan(String targetMapName, String targetNpcDescription, String targetPlayerDescription, + OreAPI.Ore oreToSell, boolean collectBonus, boolean collectCargo, Locatable targetCoordinates, - String primaryDescription) { + String primaryDescription, List steps) { this.targetMapName = targetMapName; this.targetNpcDescription = targetNpcDescription; + this.targetPlayerDescription = targetPlayerDescription; this.oreToSell = oreToSell; this.collectBonus = collectBonus; this.collectCargo = collectCargo; this.targetCoordinates = targetCoordinates; this.primaryDescription = primaryDescription; + this.steps = List.copyOf(steps); } String targetMapName() { @@ -57,6 +85,10 @@ String targetNpcDescription() { return targetNpcDescription; } + String targetPlayerDescription() { + return targetPlayerDescription; + } + OreAPI.Ore oreToSell() { return oreToSell; } @@ -77,13 +109,27 @@ String primaryDescription() { return primaryDescription; } + List steps() { + return steps; + } + + int priorityScore() { + if (targetPlayerDescription != null) return 4_000; + if (oreToSell != null) return 3_000; + if (collectBonus || collectCargo) return 2_000; + if (targetNpcDescription != null) return 1_000; + return 0; + } + boolean executable() { - return targetMapName != null || targetNpcDescription != null || oreToSell != null || + return targetMapName != null || targetNpcDescription != null || targetPlayerDescription != null || + oreToSell != null || collectBonus || collectCargo || targetCoordinates != null; } boolean coordinatesOnly() { - return targetCoordinates != null && targetNpcDescription == null && oreToSell == null && + return targetCoordinates != null && targetNpcDescription == null && targetPlayerDescription == null && + oreToSell == null && !collectBonus && !collectCargo; } } @@ -93,6 +139,7 @@ private DailyQuestConditionEngine() { static Plan build(QuestAPI.Quest quest, String companyPrefix) { List requirements = DailyTaskPlanner.actionable(quest); + List steps = buildSteps(requirements); String targetMap = requirements.stream() .filter(DailyQuestConditionEngine::isMapCondition) @@ -113,6 +160,12 @@ static Plan build(QuestAPI.Quest quest, String companyPrefix) { .filter(description -> description != null && !description.isBlank()) .findFirst() .orElse(null); + String targetPlayer = requirements.stream() + .filter(DailyQuestConditionEngine::isPlayerCondition) + .map(QuestAPI.Requirement::getDescription) + .filter(description -> description != null && !description.isBlank()) + .findFirst() + .orElse(null); if (targetMap == null && targetNpc != null) { targetMap = DailyTaskPlanner.preferredMapForNpc(targetNpc, companyPrefix).orElse(null); } @@ -137,6 +190,18 @@ static Plan build(QuestAPI.Quest quest, String companyPrefix) { .findFirst() .orElse(null); + StepType activeType = steps.stream() + .map(Step::type) + .filter(type -> type != StepType.MAP) + .findFirst() + .orElse(StepType.MAP); + if (activeType != StepType.NPC_COMBAT) targetNpc = null; + if (activeType != StepType.PLAYER_COMBAT) targetPlayer = null; + if (activeType != StepType.SELL_ORE) ore = null; + if (activeType != StepType.COLLECT_BONUS) collectBonus = false; + if (activeType != StepType.COLLECT_CARGO) collectCargo = false; + if (activeType != StepType.COORDINATES) coordinates = null; + String primary = requirements.stream() .filter(DailyQuestConditionEngine::isExecutableCondition) .map(QuestAPI.Requirement::getDescription) @@ -148,13 +213,29 @@ static Plan build(QuestAPI.Quest quest, String companyPrefix) { .findFirst() .orElse("etkin hedef yok")); - return new Plan(targetMap, targetNpc, ore, collectBonus, collectCargo, coordinates, primary); + return new Plan(targetMap, targetNpc, targetPlayer, ore, collectBonus, collectCargo, + coordinates, primary, steps); } static boolean supports(QuestAPI.Quest quest) { + return supports(quest, false); + } + + static boolean supports(QuestAPI.Quest quest, boolean allowPlayerCombat) { + List requirements = DailyTaskPlanner.actionable(quest); + if (requirements.isEmpty()) return false; + for (QuestAPI.Requirement requirement : requirements) { + if (isPlayerCondition(requirement) && !allowPlayerCombat) return false; + if (!isExecutableCondition(requirement)) return false; + } return build(quest, "1").executable(); } + static boolean hasPlayerCombat(QuestAPI.Quest quest) { + return DailyTaskPlanner.actionable(quest).stream() + .anyMatch(DailyQuestConditionEngine::isPlayerCondition); + } + static boolean isNpcCondition(QuestAPI.Requirement requirement) { if (requirement == null) return false; QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); @@ -165,6 +246,15 @@ static boolean isNpcCondition(QuestAPI.Requirement requirement) { type == QuestAPI.Requirement.RequirementType.RESTRICT_AMMUNITION_KILL_NPC; } + static boolean isPlayerCondition(QuestAPI.Requirement requirement) { + if (requirement == null) return false; + QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); + return type == QuestAPI.Requirement.RequirementType.KILL_PLAYERS || + type == QuestAPI.Requirement.RequirementType.DAMAGE_PLAYERS || + type == QuestAPI.Requirement.RequirementType.DAMAGE_ENEMY_PLAYERS || + type == QuestAPI.Requirement.RequirementType.RESTRICT_AMMUNITION_KILL_PLAYER; + } + private static boolean isMapCondition(QuestAPI.Requirement requirement) { if (requirement == null) return false; QuestAPI.Requirement.RequirementType type = requirement.getRequirementType(); @@ -203,13 +293,34 @@ private static boolean isCargoCondition(QuestAPI.Requirement requirement) { } private static boolean isExecutableCondition(QuestAPI.Requirement requirement) { - return isNpcCondition(requirement) || isMapCondition(requirement) || + return isNpcCondition(requirement) || isPlayerCondition(requirement) || isMapCondition(requirement) || isCoordinateCondition(requirement) || isBonusCondition(requirement) || isCargoCondition(requirement) || (requirement != null && requirement.getRequirementType() == QuestAPI.Requirement.RequirementType.SELL_ORE); } + private static List buildSteps(List requirements) { + List steps = new ArrayList<>(); + for (QuestAPI.Requirement requirement : requirements) { + StepType type = stepType(requirement); + if (type != null) steps.add(new Step(type, requirement.getDescription())); + } + return steps; + } + + private static StepType stepType(QuestAPI.Requirement requirement) { + if (isPlayerCondition(requirement)) return StepType.PLAYER_COMBAT; + if (isNpcCondition(requirement)) return StepType.NPC_COMBAT; + if (isMapCondition(requirement)) return StepType.MAP; + if (isCoordinateCondition(requirement)) return StepType.COORDINATES; + if (isBonusCondition(requirement)) return StepType.COLLECT_BONUS; + if (isCargoCondition(requirement)) return StepType.COLLECT_CARGO; + if (requirement != null && requirement.getRequirementType() == + QuestAPI.Requirement.RequirementType.SELL_ORE) return StepType.SELL_ORE; + return null; + } + static Optional findCoordinates(String description) { if (description == null || description.isBlank()) return Optional.empty(); String normalized = description.toLowerCase(Locale.ROOT); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java new file mode 100644 index 00000000..78789904 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java @@ -0,0 +1,83 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.managers.QuestAPI; + +import java.util.Collection; + +/** Central acceptance and execution policy for daily quests. */ +final class DailyQuestPolicy { + enum Decision { + ACCEPT("accepted"), + WAIT_FOR_REWARDS("reward data is not ready"), + SKIP_NO_URIDIUM("no Uridium reward"), + SKIP_TETRATHRIN_ONLY("Tetrathrin-only reward"), + SKIP_PROTEGIT("Protegit objective is disabled"), + SKIP_PLAYER_COMBAT("player-combat objectives are disabled"), + SKIP_UNSUPPORTED("unsupported objective"); + + private final String message; + + Decision(String message) { + this.message = message; + } + + String message() { + return message; + } + + boolean accepted() { + return this == ACCEPT; + } + } + + private DailyQuestPolicy() { + } + + static Decision evaluateOffer(QuestAPI.Quest quest, DailyTaskConfig config) { + Decision objectiveDecision = evaluateObjectives(quest, config); + if (!objectiveDecision.accepted()) return objectiveDecision; + + Collection rewards = quest == null ? null : quest.getRewards(); + if (rewards == null || rewards.isEmpty()) return Decision.WAIT_FOR_REWARDS; + if (config.skipTetrathrinOnly && DailyTaskPlanner.isOnlyTetrathrinReward(rewards)) { + return Decision.SKIP_TETRATHRIN_ONLY; + } + if (config.requireUridiumReward && !DailyTaskPlanner.hasUridiumReward(rewards)) { + return Decision.SKIP_NO_URIDIUM; + } + return Decision.ACCEPT; + } + + static Decision evaluateActive(QuestAPI.Quest quest, DailyTaskConfig config) { + return evaluateObjectives(quest, config); + } + + static int priority(QuestAPI.Quest quest, DailyTaskConfig config) { + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, "1"); + int score = plan.priorityScore(); + if (config.preferShorterQuests) { + score += (int) Math.min(10_000d, Math.round(DailyTaskPlanner.remainingWork(quest))); + } + return score; + } + + private static Decision evaluateObjectives(QuestAPI.Quest quest, DailyTaskConfig config) { + if (quest == null) return Decision.SKIP_UNSUPPORTED; + if (config.skipProtegitQuests && containsProtegit(quest)) return Decision.SKIP_PROTEGIT; + if (!config.acceptPlayerKillQuests && DailyQuestConditionEngine.hasPlayerCombat(quest)) { + return Decision.SKIP_PLAYER_COMBAT; + } + if (!DailyQuestConditionEngine.supports(quest, config.acceptPlayerKillQuests)) { + return Decision.SKIP_UNSUPPORTED; + } + return Decision.ACCEPT; + } + + private static boolean containsProtegit(QuestAPI.Quest quest) { + return DailyTaskPlanner.actionable(quest).stream() + .filter(DailyQuestConditionEngine::isNpcCondition) + .map(QuestAPI.Requirement::getDescription) + .map(DailyTaskPlanner::normalizeNpcName) + .anyMatch(description -> description.contains("protegit")); + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java index e8929c10..746287f0 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -24,6 +24,7 @@ import java.time.LocalDate; import java.time.ZoneId; import java.util.Comparator; +import java.util.EnumMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -62,11 +63,13 @@ private static final class QuestChoice { private final int id; private final String title; private final QuestMenuSource.Selector selector; + private final int priority; - private QuestChoice(int id, String title, QuestMenuSource.Selector selector) { + private QuestChoice(int id, String title, QuestMenuSource.Selector selector, int priority) { this.id = id; this.title = title; this.selector = selector; + this.priority = priority; } private int id() { @@ -80,6 +83,10 @@ private String title() { private QuestMenuSource.Selector selector() { return selector; } + + private int priority() { + return priority; + } } private final QuestAPI quests; @@ -95,6 +102,7 @@ private QuestMenuSource.Selector selector() { private final QuestMenuSource questMenuSource; private final QuestGiverSource questGiverSource; private final DailyNpcCombat npcCombat; + private final DailyPlayerCombat playerCombat; private DailyTaskConfig settings = new DailyTaskConfig(); private State state = State.STARTING; @@ -108,6 +116,7 @@ private QuestMenuSource.Selector selector() { private long nextRoamAt; private String status = "Starting"; private String targetNpcDescription; + private String targetPlayerDescription; private String targetMapName; private OreAPI.Ore oreToSell; private boolean collectBonus; @@ -134,7 +143,8 @@ private QuestMenuSource.Selector selector() { private final Set reviewedOfferIds = new LinkedHashSet<>(); private final Set seenGiverOfferIds = new LinkedHashSet<>(); private final Set acceptedOfferIds = new LinkedHashSet<>(); - private final Set skippedTetrathrinOfferIds = new LinkedHashSet<>(); + private final Map skippedOfferCounts = + new EnumMap<>(DailyQuestPolicy.Decision.class); public DailyTaskAPI(PluginAPI api) { this.quests = api.requireAPI(QuestAPI.class); @@ -150,6 +160,7 @@ public DailyTaskAPI(PluginAPI api) { this.questMenuSource = new QuestMenuSource(questGui); this.questGiverSource = new QuestGiverSource(gameScreen); this.npcCombat = new DailyNpcCombat(api); + this.playerCombat = new DailyPlayerCombat(api); } @Override @@ -223,13 +234,15 @@ private void runDisplayedQuest(long now) { waitForQuestSnapshot(); return; } - if (handleQuestChange(quest, now) || rejectNonDailyQuest(quest, now)) return; + if (handleQuestChange(quest, now) || rejectNonDailyQuest(quest, now) || + rejectDisallowedQuest(quest, now)) return; double progress = DailyTaskPlanner.progress(quest); if (quest.isCompleted() || progress >= 0.9999d) { completeQuest(quest, now); return; } buildPlanFromSource(quest); + if (targetPlayerDescription != null) playerCombat.observeHeroState(); if (recoverStagnantPlan(quest, progress, now)) return; boolean standardNpcCombatOwnsSafety = targetNpcDescription != null && (targetMapName == null || isOnMap(targetMapName)); @@ -246,9 +259,8 @@ private void waitForQuestSnapshot() { private boolean handleQuestChange(QuestAPI.Quest quest, long now) { if (currentQuestId < 0 || quest.getId() == currentQuestId) return false; - completedQuestIds.add(currentQuestId); - scheduleQuestMenuScan(now, true); - status = "Displayed quest changed; rescanning the native quest menu"; + scheduleQuestMenuScan(now, false); + status = "Displayed quest changed before completion; safely rescanning the native quest menu"; return true; } @@ -299,7 +311,7 @@ private void resetSession() { reviewedOfferIds.clear(); seenGiverOfferIds.clear(); acceptedOfferIds.clear(); - skippedTetrathrinOfferIds.clear(); + skippedOfferCounts.clear(); resetProgressTracking(); clearPlan(); status = "Scanning the native quest menu"; @@ -444,7 +456,7 @@ private void finishMenuScan() { pendingChoice = dailyChoices.values().stream() .filter(choice -> !completedQuestIds.contains(choice.id())) .filter(choice -> choice.selector() != null) - .findFirst() + .min(Comparator.comparingInt(QuestChoice::priority)) .orElse(null); if (pendingChoice != null) { selectionRetries = 0; @@ -624,29 +636,21 @@ private void inspectGiverRow() { return; } - if (isProtegitQuest(selected)) { - reviewedOfferIds.add(info.getId()); - status = "Skipped Protegit kill quest: " + safeTitle(selected); - advanceGiverRow(); - return; - } - int id = info.getId(); if (reviewedOfferIds.contains(id) || info.isCompleted() || !info.isActivable()) { reviewedOfferIds.add(id); advanceGiverRow(); return; } - if (selected.getRewards() == null || selected.getRewards().isEmpty()) { + DailyQuestPolicy.Decision decision = DailyQuestPolicy.evaluateOffer(selected, settings); + if (decision == DailyQuestPolicy.Decision.WAIT_FOR_REWARDS) { retryOrAdvanceGiverRow("Waiting for offer rewards from the source"); return; } - if (!DailyTaskPlanner.hasUridiumReward(selected.getRewards())) { + if (!decision.accepted()) { reviewedOfferIds.add(id); - if (DailyTaskPlanner.isOnlyTetrathrinReward(selected.getRewards())) { - skippedTetrathrinOfferIds.add(id); - } - status = "Skipped daily quest without Uridium: " + safeTitle(selected); + skippedOfferCounts.merge(decision, 1, Integer::sum); + status = "Skipped daily quest: " + decision.message() + " | " + safeTitle(selected); advanceGiverRow(); return; } @@ -694,9 +698,8 @@ private boolean isSafeOfferSelection(QuestAPI.QuestListItem info, QuestAPI.Quest if (info == null || selected == null) return false; if (info.getId() != selectedOfferId || selected.getId() != selectedOfferId) return false; if (!DailyTaskPlanner.isDailyType(info.getType()) && !DailyTaskPlanner.isDaily(selected)) return false; - if (isProtegitQuest(selected) || !info.isActivable() || info.isCompleted()) return false; - return selected.getRewards() != null && !selected.getRewards().isEmpty() && - DailyTaskPlanner.hasUridiumReward(selected.getRewards()); + if (!info.isActivable() || info.isCompleted()) return false; + return DailyQuestPolicy.evaluateOffer(selected, settings).accepted(); } private void skipSelectedOffer(QuestAPI.QuestListItem info) { @@ -713,14 +716,6 @@ private void skipSelectedOffer(QuestAPI.QuestListItem info) { advanceGiverRow(); } - private boolean isProtegitQuest(QuestAPI.Quest quest) { - return DailyTaskPlanner.actionable(quest).stream() - .filter(this::isNpcRequirement) - .map(QuestAPI.Requirement::getDescription) - .map(DailyTaskPlanner::normalizeNpcName) - .anyMatch(description -> description.contains("protegit")); - } - private void verifyAcceptedOffer() { QuestAPI.QuestListItem catalog = findQuestItem(selectedOfferId).orElse(null); boolean accepted = catalog != null && !catalog.isActivable(); @@ -762,7 +757,8 @@ private void finishQuestOfferScan() { offerScanNeeded = false; state = State.CLOSING_GIVER; status = "Station scan completed | Accepted: " + acceptedOfferIds.size() + - " | Tetrathrin-only skipped: " + skippedTetrathrinOfferIds.size(); + " | Skipped by policy: " + skippedOfferCounts.values().stream() + .mapToInt(Integer::intValue).sum(); nextActionAt = System.currentTimeMillis() + 300L; } @@ -808,6 +804,7 @@ private void startDaily(QuestAPI.Quest quest) { currentQuestTitle = safeTitle(quest); selectionRetries = 0; resetProgressTracking(); + playerCombat.startQuest(); state = State.RUNNING; buildPlanFromSource(quest); status = "Daily quest verified: " + currentQuestTitle; @@ -867,8 +864,9 @@ private void recordQuest(QuestAPI.Quest quest, QuestMenuSource.Selector selector completedQuestIds.add(quest.getId()); return; } - if (!DailyQuestConditionEngine.supports(quest)) return; - QuestChoice choice = new QuestChoice(quest.getId(), safeTitle(quest), selector); + if (!DailyQuestPolicy.evaluateActive(quest, settings).accepted()) return; + QuestChoice choice = new QuestChoice(quest.getId(), safeTitle(quest), selector, + DailyQuestPolicy.priority(quest, settings)); QuestChoice existing = dailyChoices.get(quest.getId()); if (existing == null || (existing.selector() == null && selector != null)) { dailyChoices.put(quest.getId(), choice); @@ -894,11 +892,17 @@ private void buildPlanFromSource(QuestAPI.Quest quest) { DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, companyPrefix()); targetMapName = plan.targetMapName(); targetNpcDescription = plan.targetNpcDescription(); + targetPlayerDescription = plan.targetPlayerDescription(); oreToSell = plan.oreToSell(); collectBonus = plan.collectBonus(); collectCargo = plan.collectCargo(); targetCoordinates = plan.targetCoordinates(); + if (targetPlayerDescription != null && targetMapName == null && + settings.playerCombatMap != null && !settings.playerCombatMap.isBlank()) { + targetMapName = DailyTaskPlanner.findMap(settings.playerCombatMap).orElse(null); + } + if (oreToSell != null && !ores.canSellOres() && targetMapName == null) { targetMapName = homeBaseMapName(); } @@ -910,6 +914,7 @@ private void executePlan(QuestAPI.Quest quest, double progress) { if (handleOreSale(quest, progress)) return; if (collectRequiredBox(quest, progress)) return; if (huntRequiredNpc(quest, progress)) return; + if (fightRequiredPlayer(quest, progress)) return; if (searchForRequiredBox(quest, progress)) return; if (reportMapArrival(quest, progress)) return; pauseSafely("Unsupported daily objective: " + firstRequirement(quest)); @@ -926,6 +931,7 @@ private boolean handleTargetCoordinates(QuestAPI.Quest quest, double progress) { if (targetCoordinates == null) return false; if (movement.getClosestDistance(targetCoordinates) > 120d) { npcCombat.stopCombat(); + playerCombat.stopCombat(); attack.stopAttack(); hero.setRunMode(); movement.moveTo(targetCoordinates); @@ -941,7 +947,8 @@ private boolean handleTargetCoordinates(QuestAPI.Quest quest, double progress) { } private boolean isCoordinateOnlyPlan() { - return targetNpcDescription == null && oreToSell == null && !collectBonus && !collectCargo; + return targetNpcDescription == null && targetPlayerDescription == null && oreToSell == null && + !collectBonus && !collectCargo; } private boolean handleOreSale(QuestAPI.Quest quest, double progress) { @@ -973,13 +980,32 @@ private boolean collectRequiredBox(QuestAPI.Quest quest, double progress) { private boolean huntRequiredNpc(QuestAPI.Quest quest, double progress) { if (targetNpcDescription == null) return false; - npcCombat.tick(targetNpcDescription); + npcCombat.tick(targetNpcDescription, settings.attackRadius); if (attack.getTarget() == null && !movement.isMoving()) movement.moveRandom(); updateProgressStatus(quest, progress, "NPC Kill and Collect: " + npcCombat.getStatus() + " | Target: " + cleanedRequirement(targetNpcDescription)); return true; } + private boolean rejectDisallowedQuest(QuestAPI.Quest quest, long now) { + DailyQuestPolicy.Decision decision = DailyQuestPolicy.evaluateActive(quest, settings); + if (decision.accepted()) return false; + scheduleQuestMenuScan(now, false); + status = "Daily quest skipped by policy: " + decision.message(); + return true; + } + + private boolean fightRequiredPlayer(QuestAPI.Quest quest, double progress) { + if (targetPlayerDescription == null) return false; + if (playerCombat.exceededSafetyLimits(settings)) { + pauseSafely("Player combat stopped: " + playerCombat.failureReason(settings)); + return true; + } + playerCombat.tick(targetPlayerDescription, settings); + updateProgressStatus(quest, progress, "Player combat: " + playerCombat.getStatus()); + return true; + } + private boolean searchForRequiredBox(QuestAPI.Quest quest, double progress) { if (!collectBonus && !collectCargo) return false; roamIfNeeded(); @@ -995,14 +1021,17 @@ private boolean reportMapArrival(QuestAPI.Quest quest, double progress) { private boolean runLocalSafety() { double hp = hero.getHealth().hpPercent(); - if (hp >= settings.minimumHpPercent) return false; + boolean lowShieldInPlayerCombat = targetPlayerDescription != null && + hero.getHealth().shieldPercent() < settings.playerMinimumShieldPercent; + if (hp >= settings.minimumHpPercent && !lowShieldInPlayerCombat) return false; attack.stopAttack(); hero.setRunMode(); entities.getPortals().stream() .filter(Portal::isValid) .min(Comparator.comparingDouble(hero::distanceTo)) .ifPresent(portal -> movement.moveTo(portal)); - status = "Local safety: HP " + (int) Math.round(hp * 100d) + "% ; waiting near portal"; + status = "Local safety: HP " + (int) Math.round(hp * 100d) + "% / shield " + + (int) Math.round(hero.getHealth().shieldPercent() * 100d) + "% ; waiting near portal"; return true; } @@ -1162,6 +1191,7 @@ private boolean recoverStagnantPlan(QuestAPI.Quest quest, double progress, long lastProgressAt = now; npcCombat.stopCombat(); + playerCombat.stopCombat(); attack.stopAttack(); attack.setTarget(null); if (targetMapName != null && !isOnMap(targetMapName)) { @@ -1208,6 +1238,7 @@ private void finishAll() { private void stopCurrentAction() { npcCombat.stopCombat(); + playerCombat.stopCombat(); attack.stopAttack(); movement.stop(false); hero.setRoamMode(); @@ -1216,6 +1247,7 @@ private void stopCurrentAction() { private void clearPlan() { targetNpcDescription = null; + targetPlayerDescription = null; targetMapName = null; oreToSell = null; collectBonus = false; @@ -1263,6 +1295,7 @@ public void onTickStopped() { // gaps. Stop active movement/attack, but retain the selected quest and its // plan so a slow server does not force another trip to the quest station. npcCombat.stopCombat(); + playerCombat.stopCombat(); attack.stopAttack(); movement.stop(false); hero.setRoamMode(); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java index 4e509b98..f8b5b4fe 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java @@ -7,6 +7,16 @@ public final class DailyTaskConfig { public boolean autoOpenQuestWindow = true; + public boolean requireUridiumReward = true; + + public boolean skipTetrathrinOnly = true; + + public boolean skipProtegitQuests = true; + + public boolean acceptPlayerKillQuests = false; + + public boolean preferShorterQuests = true; + @Number(min = 250, max = 20000, step = 250) public int questSwitchDelayMs = 500; @@ -14,7 +24,27 @@ public final class DailyTaskConfig { public int maxSelectionRetries = 1; @Number(min = 300, max = 750, step = 25) - public int attackRadius = 500; + public int attackRadius = 590; + + @Number(min = 300, max = 750, step = 25) + public int playerAttackRadius = 590; + + public boolean enemyPlayersOnly = true; + + public boolean avoidAlliedAndGroupPlayers = true; + + public boolean prioritizePlayersAttackingMe = true; + + public String playerCombatMap = ""; + + @Number(min = 0.00, max = 0.90, step = 0.05) + public double playerMinimumShieldPercent = 0.20; + + @Number(min = 0, max = 20, step = 1) + public int maxPlayerDeaths = 2; + + @Number(min = 30, max = 1800, step = 30) + public int playerSearchTimeoutSeconds = 300; @Number(min = 0.10, max = 0.90, step = 0.05) public double minimumHpPercent = 0.30; diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java index 042ba3ec..65d15d89 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -209,6 +209,15 @@ static double progress(QuestAPI.Quest quest) { return sum / requirements.size(); } + static double remainingWork(QuestAPI.Quest quest) { + double remaining = 0d; + for (QuestAPI.Requirement requirement : actionable(quest)) { + double goal = Math.max(0d, requirement.getGoal()); + remaining += goal <= 0d ? 1d : Math.max(0d, goal - requirement.getProgress()); + } + return remaining; + } + private static double requirementProgress(QuestAPI.Requirement requirement) { double goal = requirement.getGoal(); if (goal <= 0d) return requirement.isCompleted() ? 1d : 0d; diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java index cc624764..f159b8fb 100644 --- a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java @@ -8,6 +8,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; final class DailyTaskPlannerTest { @@ -122,6 +123,99 @@ void handlesZeroGoalsCompletedRequirementsAndClamping() { assertEquals(1d, DailyTaskPlanner.progress(quest(10, true, false, List.of(overGoal))), 0d); } + @Test + void buildsSequentialStepsFromRemainingObjectives() { + QuestAPI.Requirement first = requirement("Destroy Mordon", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 1, false); + QuestAPI.Requirement second = requirement("Destroy Lordakium", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 1, false); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build( + quest(11, true, false, List.of(first, second, dailyTimer())), "1"); + + assertEquals(2, plan.steps().stream() + .filter(step -> step.type() == DailyQuestConditionEngine.StepType.NPC_COMBAT) + .count()); + assertTrue(plan.targetNpcDescription().contains("Mordon")); + + QuestAPI.Requirement completedFirst = requirement("Destroy Mordon", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 1, 1, true); + DailyQuestConditionEngine.Plan nextPlan = DailyQuestConditionEngine.build( + quest(12, true, false, List.of(completedFirst, second, dailyTimer())), "1"); + assertTrue(nextPlan.targetNpcDescription().contains("Lordakium")); + } + + @Test + void classifiesPlayerCombatWithoutTreatingItAsNpcCombat() { + QuestAPI.Requirement pvp = requirement("Destroy enemy players", "KILL_PLAYERS", + QuestAPI.Requirement.RequirementType.KILL_PLAYERS, 0, 3, false); + QuestAPI.Quest quest = quest(13, true, false, List.of(pvp, dailyTimer())); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, "1"); + + assertTrue(DailyQuestConditionEngine.hasPlayerCombat(quest)); + assertFalse(DailyQuestConditionEngine.supports(quest)); + assertTrue(DailyQuestConditionEngine.supports(quest, true)); + assertTrue(plan.targetPlayerDescription().contains("players")); + assertNull(plan.targetNpcDescription()); + } + + @Test + void policyRejectsRiskyOrUnsupportedOffersBeforeAcceptance() { + DailyTaskConfig config = new DailyTaskConfig(); + QuestAPI.Requirement pvp = requirement("Destroy enemy players", "KILL_PLAYERS", + QuestAPI.Requirement.RequirementType.KILL_PLAYERS, 0, 3, false); + QuestAPI.Quest pvpOffer = quest(14, true, false, List.of(pvp, dailyTimer()), + List.of(reward("currency_uridium", 1500))); + assertEquals(DailyQuestPolicy.Decision.SKIP_PLAYER_COMBAT, + DailyQuestPolicy.evaluateOffer(pvpOffer, config)); + + config.acceptPlayerKillQuests = true; + assertEquals(DailyQuestPolicy.Decision.ACCEPT, + DailyQuestPolicy.evaluateOffer(pvpOffer, config)); + + QuestAPI.Requirement unsupported = requirement("Upgrade Skylab", "UPDATE_SKYLAB_TO_LEVEL", + QuestAPI.Requirement.RequirementType.UPDATE_SKYLAB_TO_LEVEL, 0, 1, false); + QuestAPI.Quest unsupportedOffer = quest(15, true, false, List.of(unsupported, dailyTimer()), + List.of(reward("currency_uridium", 1500))); + assertEquals(DailyQuestPolicy.Decision.SKIP_UNSUPPORTED, + DailyQuestPolicy.evaluateOffer(unsupportedOffer, config)); + } + + @Test + void policyHonorsRewardAndProtegitOptions() { + DailyTaskConfig config = new DailyTaskConfig(); + QuestAPI.Requirement npc = requirement("Destroy Protegit", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 3, false); + QuestAPI.Quest offer = quest(16, true, false, List.of(npc, dailyTimer()), + List.of(reward("currency_uridium", 1500))); + assertEquals(DailyQuestPolicy.Decision.SKIP_PROTEGIT, + DailyQuestPolicy.evaluateOffer(offer, config)); + + config.skipProtegitQuests = false; + assertEquals(DailyQuestPolicy.Decision.ACCEPT, + DailyQuestPolicy.evaluateOffer(offer, config)); + + QuestAPI.Quest noUri = quest(17, true, false, List.of(npc, dailyTimer()), + List.of(reward("resource_tetrathrin", 10))); + assertEquals(DailyQuestPolicy.Decision.SKIP_TETRATHRIN_ONLY, + DailyQuestPolicy.evaluateOffer(noUri, config)); + config.skipTetrathrinOnly = false; + assertEquals(DailyQuestPolicy.Decision.SKIP_NO_URIDIUM, + DailyQuestPolicy.evaluateOffer(noUri, config)); + } + + @Test + void computesRemainingWorkForQuestPriority() { + QuestAPI.Requirement shortTask = requirement("Destroy Mordon", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 4, 5, false); + QuestAPI.Requirement longTask = requirement("Destroy Mordon", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 1, 20, false); + assertTrue(DailyTaskPlanner.remainingWork( + quest(18, true, false, List.of(shortTask, dailyTimer()))) < + DailyTaskPlanner.remainingWork( + quest(19, true, false, List.of(longTask, dailyTimer())))); + assertEquals(590, new DailyTaskConfig().attackRadius); + } + private static QuestAPI.Requirement dailyTimer() { return requirement("Complete within one day", "REAL_TIME_HASTE", QuestAPI.Requirement.RequirementType.REAL_TIME_HASTE, 0, DAY_MS, false); @@ -158,6 +252,12 @@ private static QuestAPI.Requirement requirement(String description, String type, private static QuestAPI.Quest quest(int id, boolean active, boolean completed, List requirements) { + return quest(id, active, completed, requirements, List.of()); + } + + private static QuestAPI.Quest quest(int id, boolean active, boolean completed, + List requirements, + List rewards) { return new QuestAPI.Quest() { public int getId() { return id; } public boolean isActive() { return active; } @@ -165,7 +265,7 @@ private static QuestAPI.Quest quest(int id, boolean active, boolean completed, public String getDescription() { return ""; } public boolean isCompleted() { return completed; } public List getRequirements() { return requirements; } - public List getRewards() { return List.of(); } + public List getRewards() { return rewards; } }; } } From 8a543f977a5df22326b13bdbca6322bae74597e8 Mon Sep 17 00:00:00 2001 From: Berke Date: Thu, 13 Aug 2026 18:40:23 +0300 Subject: [PATCH 12/14] Resolve DailyTaskAPI Sonar findings --- .../DailyQuestConditionEngine.java | 45 +++++++++++++------ .../berke/dailytaskapi/DailyTaskAPI.java | 4 -- .../berke/dailytaskapi/DailyTaskPlanner.java | 14 +++--- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java index 9275e66f..e26add22 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java @@ -21,10 +21,10 @@ */ final class DailyQuestConditionEngine { private static final Pattern X_COORDINATE = Pattern.compile( - "\\bx(?:-koordinat[iı]?)?\\s*[:=]?\\s*(-?\\d{1,5})", + "\\bx(?:-koordinat[i\\x{0131}]?)?\\s*+(?:[:=]\\s*+)?(-?\\d{1,5}+)", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); private static final Pattern Y_COORDINATE = Pattern.compile( - "\\by(?:-koordinat[iı]?)?\\s*[:=]?\\s*(-?\\d{1,5})", + "\\by(?:-koordinat[i\\x{0131}]?)?\\s*+(?:[:=]\\s*+)?(-?\\d{1,5}+)", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE); private static final Pattern PAIRED_COORDINATES = Pattern.compile( "(? steps; - Plan(String targetMapName, String targetNpcDescription, String targetPlayerDescription, - OreAPI.Ore oreToSell, - boolean collectBonus, boolean collectCargo, Locatable targetCoordinates, + Plan(String targetMapName, ActiveObjective objective, String primaryDescription, List steps) { this.targetMapName = targetMapName; - this.targetNpcDescription = targetNpcDescription; - this.targetPlayerDescription = targetPlayerDescription; - this.oreToSell = oreToSell; - this.collectBonus = collectBonus; - this.collectCargo = collectCargo; - this.targetCoordinates = targetCoordinates; + this.targetNpcDescription = objective.targetNpcDescription; + this.targetPlayerDescription = objective.targetPlayerDescription; + this.oreToSell = objective.oreToSell; + this.collectBonus = objective.collectBonus; + this.collectCargo = objective.collectCargo; + this.targetCoordinates = objective.targetCoordinates; this.primaryDescription = primaryDescription; this.steps = List.copyOf(steps); } @@ -134,6 +132,26 @@ boolean coordinatesOnly() { } } + private static final class ActiveObjective { + private final String targetNpcDescription; + private final String targetPlayerDescription; + private final OreAPI.Ore oreToSell; + private final boolean collectBonus; + private final boolean collectCargo; + private final Locatable targetCoordinates; + + private ActiveObjective(String targetNpcDescription, String targetPlayerDescription, + OreAPI.Ore oreToSell, boolean collectBonus, boolean collectCargo, + Locatable targetCoordinates) { + this.targetNpcDescription = targetNpcDescription; + this.targetPlayerDescription = targetPlayerDescription; + this.oreToSell = oreToSell; + this.collectBonus = collectBonus; + this.collectCargo = collectCargo; + this.targetCoordinates = targetCoordinates; + } + } + private DailyQuestConditionEngine() { } @@ -213,8 +231,9 @@ static Plan build(QuestAPI.Quest quest, String companyPrefix) { .findFirst() .orElse("etkin hedef yok")); - return new Plan(targetMap, targetNpc, targetPlayer, ore, collectBonus, collectCargo, - coordinates, primary, steps); + ActiveObjective objective = new ActiveObjective(targetNpc, targetPlayer, ore, + collectBonus, collectCargo, coordinates); + return new Plan(targetMap, objective, primary, steps); } static boolean supports(QuestAPI.Quest quest) { diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java index 746287f0..04612fe7 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -1159,10 +1159,6 @@ private void roamIfNeeded() { nextRoamAt = now + 2500L; } - private boolean isNpcRequirement(QuestAPI.Requirement requirement) { - return DailyQuestConditionEngine.isNpcCondition(requirement); - } - private boolean isOnMap(String mapName) { GameMap current = starSystem.getCurrentMap(); return current != null && current.getName().equalsIgnoreCase(mapName); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java index 65d15d89..ace711e2 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -41,12 +41,14 @@ static boolean isOnlyTetrathrinReward(Collection rewa if (rewards == null || rewards.isEmpty()) return false; boolean hasTetrathrin = false; for (QuestAPI.Reward reward : rewards) { - if (!isPositiveReward(reward)) continue; - String type = normalize(reward.getType()); - if (type.contains("uridium")) return false; - if (type.startsWith("currency ")) continue; - if (!type.contains("tetrathrin")) return false; - hasTetrathrin = true; + if (isPositiveReward(reward)) { + String type = normalize(reward.getType()); + if (type.contains("uridium")) return false; + if (!type.startsWith("currency ")) { + if (!type.contains("tetrathrin")) return false; + hasTetrathrin = true; + } + } } return hasTetrathrin; } From bba2eaa92a32a9175de71edbdccb7710eb85afc2 Mon Sep 17 00:00:00 2001 From: Berke Date: Tue, 1 Sep 2026 18:03:50 +0300 Subject: [PATCH 13/14] Address daily task review feedback --- README.md | 4 +- .../dailytaskapi/DailyNpcMapResolver.java | 54 +++++++++++ .../DailyQuestConditionEngine.java | 11 ++- .../berke/dailytaskapi/DailyQuestPolicy.java | 2 +- .../berke/dailytaskapi/DailyTaskAPI.java | 65 +++++++++++-- .../berke/dailytaskapi/DailyTaskPlanner.java | 39 -------- .../berke/petlocator/DailyNpcPetLocator.java | 89 ----------------- .../berke/ramcleaner/DailyRamCleaner.java | 96 ------------------- .../ramcleaner/DailyRamCleanerConfig.java | 23 ----- .../ramcleaner/DailyRamCleanerPolicy.java | 13 --- src/main/resources/plugin.json | 4 +- .../dailytaskapi/DailyTaskPlannerTest.java | 31 +++--- .../ramcleaner/DailyRamCleanerPolicyTest.java | 20 ---- 13 files changed, 143 insertions(+), 308 deletions(-) create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java delete mode 100644 src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java delete mode 100644 src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java delete mode 100644 src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java delete mode 100644 src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java delete mode 100644 src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java diff --git a/README.md b/README.md index 871d404b..97fcf41c 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,7 @@ We will seek to implement CI/CD in the repository so that developers have a good - Simple Galaxy Gate: Supports ABG, Delta, Epsilon, Zeta, Hades, Kuiper, LoW, Invasion, Mimesis, EBG, GoP, Treacherous, DSE, Fiesta, Trinity and Voyagers. By @do-gamer - Autobuy: Automatically buys boosters and special items from the shop at a configured interval. By @do-gamer - Log Overlay: Displays the latest in-game log messages on the canvas (top-center, auto-fade after 5s). Built-in keyword whitelist limits the displayed lines to gains and errors so the canvas does not get cluttered. By @Halizeur -- DailyTaskAPI: Detects supported daily quests from DarkBot's typed quest data, executes multi-step NPC, map, collection, ore and opt-in player-combat objectives, and applies configurable reward and safety policies before accepting them. By Berke -- DailyTaskAPI PET Locator: Exposes the current quest NPC to DarkBot's PET Enemy Locator without changing another module's saved NPC selection. By Berke -- RAM Cleaner: Requests cleanup through DarkBot's supported memory handler when a configurable threshold is exceeded, with optional combat protection. By Berke +- DailyTaskAPI: Detects supported daily quests from DarkBot's typed quest data, executes multi-step NPC, map, collection, ore and opt-in player-combat objectives, applies configurable reward and safety policies before accepting them, and uses DarkBot's NPC metadata and PET Enemy Locator while hunting. By Berke ## Contributing diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java new file mode 100644 index 00000000..8f7ed288 --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java @@ -0,0 +1,54 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.config.types.NpcInfo; +import eu.darkbot.api.game.other.GameMap; +import eu.darkbot.api.managers.StarSystemAPI; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; + +/** Resolves NPC maps from DarkBot's own NPC metadata instead of a static list. */ +final class DailyNpcMapResolver { + private final ConfigSetting> npcInfos; + private final StarSystemAPI starSystem; + + DailyNpcMapResolver(ConfigSetting> npcInfos, StarSystemAPI starSystem) { + this.npcInfos = npcInfos; + this.starSystem = starSystem; + } + + Optional resolve(String description, String companyPrefix) { + Map configured = npcInfos.getValue(); + if (configured == null || configured.isEmpty()) return Optional.empty(); + + Optional target = configured.values().stream() + .filter(info -> info != null && DailyTaskPlanner.matchesNpcName(description, info.getName())) + .max(Comparator.comparingInt(info -> + DailyTaskPlanner.normalizeNpcName(info.getName()).length())); + if (target.isEmpty()) return Optional.empty(); + + Collection mapIds = target.get().getMapIds(); + if (mapIds == null || mapIds.isEmpty()) return Optional.empty(); + + GameMap current = starSystem.getCurrentMap(); + if (current != null && mapIds.contains(current.getId()) && starSystem.isAccessible(current)) { + return Optional.of(current.getName()); + } + + String prefix = companyPrefix != null && companyPrefix.matches("[1-3]") + ? companyPrefix + "-" + : ""; + return mapIds.stream() + .map(starSystem::findMap) + .flatMap(Optional::stream) + .filter(starSystem::isAccessible) + .filter(map -> !map.isGG()) + .min(Comparator + .comparingInt((GameMap map) -> map.getName().startsWith(prefix) ? 0 : 1) + .thenComparing(GameMap::getName)) + .map(GameMap::getName); + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java index e26add22..548d0a68 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestConditionEngine.java @@ -8,6 +8,7 @@ import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -155,7 +156,11 @@ private ActiveObjective(String targetNpcDescription, String targetPlayerDescript private DailyQuestConditionEngine() { } - static Plan build(QuestAPI.Quest quest, String companyPrefix) { + static Plan build(QuestAPI.Quest quest) { + return build(quest, ignored -> Optional.empty()); + } + + static Plan build(QuestAPI.Quest quest, Function> npcMapResolver) { List requirements = DailyTaskPlanner.actionable(quest); List steps = buildSteps(requirements); @@ -185,7 +190,7 @@ static Plan build(QuestAPI.Quest quest, String companyPrefix) { .findFirst() .orElse(null); if (targetMap == null && targetNpc != null) { - targetMap = DailyTaskPlanner.preferredMapForNpc(targetNpc, companyPrefix).orElse(null); + targetMap = npcMapResolver.apply(targetNpc).orElse(null); } OreAPI.Ore ore = requirements.stream() @@ -247,7 +252,7 @@ static boolean supports(QuestAPI.Quest quest, boolean allowPlayerCombat) { if (isPlayerCondition(requirement) && !allowPlayerCombat) return false; if (!isExecutableCondition(requirement)) return false; } - return build(quest, "1").executable(); + return build(quest).executable(); } static boolean hasPlayerCombat(QuestAPI.Quest quest) { diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java index 78789904..d5421648 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyQuestPolicy.java @@ -53,7 +53,7 @@ static Decision evaluateActive(QuestAPI.Quest quest, DailyTaskConfig config) { } static int priority(QuestAPI.Quest quest, DailyTaskConfig config) { - DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, "1"); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest); int score = plan.priorityScore(); if (config.preferShorterQuests) { score += (int) Math.min(10_000d, Math.round(DailyTaskPlanner.remainingWork(quest))); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java index 04612fe7..f35f0942 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -5,24 +5,31 @@ import eu.darkbot.api.extensions.Configurable; import eu.darkbot.api.extensions.Feature; import eu.darkbot.api.extensions.Module; +import eu.darkbot.api.extensions.selectors.GearSelector; +import eu.darkbot.api.extensions.selectors.PetGearSupplier; +import eu.darkbot.api.extensions.selectors.PrioritizedSupplier; import eu.darkbot.api.game.entities.Box; import eu.darkbot.api.game.entities.Portal; import eu.darkbot.api.game.entities.Station; +import eu.darkbot.api.game.enums.PetGear; import eu.darkbot.api.game.other.GameMap; import eu.darkbot.api.game.other.Gui; import eu.darkbot.api.game.other.Locatable; import eu.darkbot.api.managers.AttackAPI; import eu.darkbot.api.managers.BotAPI; +import eu.darkbot.api.managers.ConfigAPI; import eu.darkbot.api.managers.EntitiesAPI; import eu.darkbot.api.managers.GameScreenAPI; import eu.darkbot.api.managers.HeroAPI; import eu.darkbot.api.managers.MovementAPI; import eu.darkbot.api.managers.OreAPI; +import eu.darkbot.api.managers.PetAPI; import eu.darkbot.api.managers.QuestAPI; import eu.darkbot.api.managers.StarSystemAPI; import java.time.LocalDate; import java.time.ZoneId; +import java.util.Collection; import java.util.Comparator; import java.util.EnumMap; import java.util.HashSet; @@ -43,7 +50,8 @@ * that module's saved settings untouched.

*/ @Feature(name = "DailyTaskAPI", description = "Completes only verified 24-hour daily quests") -public final class DailyTaskAPI implements Module, Configurable { +public final class DailyTaskAPI implements Module, Configurable, + GearSelector, PetGearSupplier { private static final String QUEST_BASE_MAP = "1-8"; private static final String PARK_AFTER_DAILY = "__PARK_AFTER_DAILY__"; private static final double QUEST_STATION_DISTANCE = 300d; @@ -98,10 +106,12 @@ private int priority() { private final MovementAPI movement; private final AttackAPI attack; private final HeroAPI hero; + private final PetAPI pet; private final Gui questGui; private final QuestMenuSource questMenuSource; private final QuestGiverSource questGiverSource; private final DailyNpcCombat npcCombat; + private final DailyNpcMapResolver npcMapResolver; private final DailyPlayerCombat playerCombat; private DailyTaskConfig settings = new DailyTaskConfig(); @@ -156,10 +166,14 @@ public DailyTaskAPI(PluginAPI api) { this.movement = api.requireAPI(MovementAPI.class); this.attack = api.requireAPI(AttackAPI.class); this.hero = api.requireAPI(HeroAPI.class); + this.pet = api.requireAPI(PetAPI.class); this.questGui = gameScreen.getGui("quests"); this.questMenuSource = new QuestMenuSource(questGui); this.questGiverSource = new QuestGiverSource(gameScreen); this.npcCombat = new DailyNpcCombat(api); + ConfigAPI config = api.requireAPI(ConfigAPI.class); + this.npcMapResolver = new DailyNpcMapResolver( + config.requireConfig("loot.npc_infos"), starSystem); this.playerCombat = new DailyPlayerCombat(api); } @@ -889,7 +903,8 @@ private String scanSummary() { private void buildPlanFromSource(QuestAPI.Quest quest) { clearPlan(); - DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, companyPrefix()); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build( + quest, description -> npcMapResolver.resolve(description, companyPrefix())); targetMapName = plan.targetMapName(); targetNpcDescription = plan.targetNpcDescription(); targetPlayerDescription = plan.targetPlayerDescription(); @@ -1257,13 +1272,51 @@ private String getNpcLocatorTargetDescription() { return targetNpcDescription; } - public boolean hasNpcLocatorTarget() { - return getNpcLocatorTargetDescription() != null; + @Override + public PetGearSupplier getGearSupplier() { + return this; + } + + @Override + public PrioritizedSupplier.Priority getPriority() { + return getNpcLocatorTargetDescription() == null + ? PrioritizedSupplier.Priority.LOWEST + : PrioritizedSupplier.Priority.HIGHEST; + } + + @Override + public PetGear get() { + if (getNpcLocatorTargetDescription() != null && pet.hasGear(PetGear.ENEMY_LOCATOR)) { + return PetGear.ENEMY_LOCATOR; + } + PetGear current = pet.getGear(); + return current == null ? PetGear.PASSIVE : current; + } + + @Override + public Boolean enablePet() { + return getNpcLocatorTargetDescription() == null ? null : Boolean.TRUE; } - public boolean matchesNpcLocatorTarget(String npcName) { + @Override + public PetAPI.LocatorPick getNpcLocatorPick( + Collection available) { + String description = getNpcLocatorTargetDescription(); + if (description == null) return PetGearSupplier.super.getNpcLocatorPick(available); + return available.stream() + .filter(pick -> pick != null && + DailyTaskPlanner.matchesNpcName(description, pick.getName())) + .findFirst() + .orElse(null); + } + + @Override + public Integer getNpcPickPriority(PetAPI.LocatorPick pick) { String description = getNpcLocatorTargetDescription(); - return DailyTaskPlanner.matchesNpcName(description, npcName); + if (description == null) return PetGearSupplier.super.getNpcPickPriority(pick); + return pick != null && DailyTaskPlanner.matchesNpcName(description, pick.getName()) + ? 0 + : null; } private String safeTitle(QuestAPI.Quest quest) { diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java index ace711e2..78467317 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskPlanner.java @@ -127,45 +127,6 @@ static Optional findMap(String description) { return matcher.find() ? Optional.of(matcher.group(1).toUpperCase(Locale.ROOT)) : Optional.empty(); } - static Optional preferredMapForNpc(String description, String companyPrefix) { - if (description == null) return Optional.empty(); - String prefix = companyPrefix != null && companyPrefix.matches("[1-3]") ? companyPrefix : "1"; - String npc = normalizeNpcName(description); - - if (npc.contains("uber")) return Optional.of("4-5"); - if (description.contains("StreuneR")) return Optional.of(prefix + "-8"); - String homeMap = homeMapForNpc(npc, prefix); - if (homeMap != null) return Optional.of(homeMap); - return pirateMapForNpc(npc); - } - - private static String homeMapForNpc(String npc, String prefix) { - if (containsAny(npc, "protecgit", "protegit", "cubikon")) return prefix + "-6"; - if (npc.contains("boss kristallon")) return prefix + "-7"; - if (npc.contains("boss lordakium")) return prefix + "-5"; - if (containsAny(npc, "kristallin", "kristallon", "lordakium")) return prefix + "-6"; - if (npc.contains("sibelonit")) return prefix + "-5"; - if (npc.contains("sibelon")) return prefix + "-4"; - if (containsAny(npc, "saimon", "mordon", "devolarium")) return prefix + "-3"; - if (containsAny(npc, "lordakia", "streuner")) return prefix + "-2"; - return null; - } - - private static Optional pirateMapForNpc(String npc) { - if (npc.contains("battleray")) return Optional.of("5-3"); - if (containsAny(npc, "interceptor", "barracuda", "saboteur", "annihilator")) { - return Optional.of("5-2"); - } - return Optional.empty(); - } - - private static boolean containsAny(String value, String... candidates) { - for (String candidate : candidates) { - if (value.contains(candidate)) return true; - } - return false; - } - static Optional findOre(String description) { String text = normalize(description); if (text.contains("prometium")) return Optional.of(OreAPI.Ore.PROMETIUM); diff --git a/src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java b/src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java deleted file mode 100644 index ea61d770..00000000 --- a/src/main/java/dev/shared/berke/petlocator/DailyNpcPetLocator.java +++ /dev/null @@ -1,89 +0,0 @@ -package dev.shared.berke.petlocator; - -import dev.shared.berke.dailytaskapi.DailyTaskAPI; - -import eu.darkbot.api.PluginAPI; -import eu.darkbot.api.extensions.Feature; -import eu.darkbot.api.extensions.Module; -import eu.darkbot.api.extensions.selectors.GearSelector; -import eu.darkbot.api.extensions.selectors.PetGearSupplier; -import eu.darkbot.api.extensions.selectors.PrioritizedSupplier; -import eu.darkbot.api.game.enums.PetGear; -import eu.darkbot.api.managers.BotAPI; -import eu.darkbot.api.managers.PetAPI; - -import java.util.Collection; - -/** Selects the quest NPC in P.E.T. Enemy Locator while DailyTaskAPI hunts. */ -@Feature( - name = "DailyTaskAPI PET Locator", - description = "Uses Enemy Locator for the active daily NPC", - enabledByDefault = true) -public final class DailyNpcPetLocator implements GearSelector, PetGearSupplier { - private final BotAPI bot; - private final PetAPI pet; - - public DailyNpcPetLocator(PluginAPI api) { - this.bot = api.requireAPI(BotAPI.class); - this.pet = api.requireAPI(PetAPI.class); - } - - @Override - public PetGearSupplier getGearSupplier() { - return this; - } - - @Override - public PrioritizedSupplier.Priority getPriority() { - return activeDaily() == null - ? PrioritizedSupplier.Priority.LOWEST - : PrioritizedSupplier.Priority.HIGHEST; - } - - @Override - public PetGear get() { - if (activeDaily() != null && pet.hasGear(PetGear.ENEMY_LOCATOR)) { - return PetGear.ENEMY_LOCATOR; - } - PetGear current = pet.getGear(); - return current == null ? PetGear.PASSIVE : current; - } - - @Override - public Boolean enablePet() { - return activeDaily() == null ? null : Boolean.TRUE; - } - - @Override - public PetAPI.LocatorPick getNpcLocatorPick( - Collection available) { - DailyTaskAPI daily = activeDaily(); - if (daily == null) return PetGearSupplier.super.getNpcLocatorPick(available); - return available.stream() - .filter(pick -> matches(daily, pick)) - .findFirst() - .orElse(null); - } - - @Override - public Integer getNpcPickPriority(PetAPI.LocatorPick pick) { - DailyTaskAPI daily = activeDaily(); - if (daily == null) return PetGearSupplier.super.getNpcPickPriority(pick); - return matches(daily, pick) ? 0 : null; - } - - private DailyTaskAPI activeDaily() { - if (!bot.isRunning()) return null; - Module module = bot.getNonTemporalModule(); - if (!(module instanceof DailyTaskAPI)) module = bot.getModule(); - if (module instanceof DailyTaskAPI) { - DailyTaskAPI daily = (DailyTaskAPI) module; - if (daily.hasNpcLocatorTarget()) return daily; - } - return null; - } - - private boolean matches(DailyTaskAPI daily, PetAPI.LocatorPick pick) { - return pick != null && daily.matchesNpcLocatorTarget(pick.getName()); - } -} diff --git a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java deleted file mode 100644 index aff8ef67..00000000 --- a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleaner.java +++ /dev/null @@ -1,96 +0,0 @@ -package dev.shared.berke.ramcleaner; - -import com.github.manolo8.darkbot.Main; -import com.github.manolo8.darkbot.core.IDarkBotAPI; -import com.github.manolo8.darkbot.core.api.Capability; -import eu.darkbot.api.PluginAPI; -import eu.darkbot.api.config.ConfigSetting; -import eu.darkbot.api.extensions.Behavior; -import eu.darkbot.api.extensions.Configurable; -import eu.darkbot.api.extensions.Feature; -import eu.darkbot.api.managers.AttackAPI; -import eu.darkbot.api.managers.HeroAPI; - -/** - * Threshold-based game-client working-set cleaner. - * - *

The implementation uses the same RAM operation exposed by DarkBot's own - * API settings screen. It does not terminate processes, edit JVM arguments or - * delete files.

- */ -@Feature(name = "BerkePlugin RAM Cleaner", - description = "Reduces the game client's working set above a configurable memory threshold", - enabledByDefault = true) -public final class DailyRamCleaner implements Behavior, Configurable { - private static final long MIN_RETRY_AFTER_COMBAT_MS = 5_000L; - - private final IDarkBotAPI darkbotApi; - private final HeroAPI hero; - private final AttackAPI attack; - - private DailyRamCleanerConfig config = new DailyRamCleanerConfig(); - private long nextCheckAt; - - public DailyRamCleaner(PluginAPI api) { - this.darkbotApi = Main.API; - this.hero = api.requireAPI(HeroAPI.class); - this.attack = api.requireAPI(AttackAPI.class); - } - - @Override - public void setConfig(ConfigSetting setting) { - DailyRamCleanerConfig configured = setting.getValue(); - if (configured != null) config = configured; - setting.addListener(updated -> { - if (updated != null) config = updated; - nextCheckAt = 0L; - }); - } - - @Override - public void onTickBehavior() { - DailyRamCleanerConfig current = config; - if (current == null || !current.enabled) { - nextCheckAt = 0L; - return; - } - - long now = System.currentTimeMillis(); - if (now < nextCheckAt) return; - - long checkIntervalMs = Math.max(15, current.checkIntervalSeconds) * 1_000L; - nextCheckAt = now + checkIntervalMs; - - long memoryUsageMb; - try { - memoryUsageMb = darkbotApi.getMemoryUsage(); - } catch (RuntimeException unavailable) { - return; - } - - boolean inCombat = attack.isAttacking() - || attack.hasTarget() - || hero.getLocalTarget() != null; - if (!DailyRamCleanerPolicy.shouldClean(memoryUsageMb, - current.ramThresholdMb, current.avoidCombat, inCombat)) { - if (current.avoidCombat && inCombat && memoryUsageMb >= current.ramThresholdMb) { - nextCheckAt = Math.min(nextCheckAt, now + MIN_RETRY_AFTER_COMBAT_MS); - } - return; - } - - try { - if (current.clearGameCache - && darkbotApi.hasCapability(Capability.HANDLER_CLEAR_CACHE)) { - darkbotApi.clearCache(".*"); - } - - if (darkbotApi.hasCapability(Capability.HANDLER_CLEAR_RAM)) { - darkbotApi.emptyWorkingSet(); - } - } catch (RuntimeException cleanupFailure) { - // Best-effort: DarkBot may temporarily reject cleanup while the - // game client is reconnecting. - } - } -} diff --git a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java deleted file mode 100644 index afb5adf8..00000000 --- a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerConfig.java +++ /dev/null @@ -1,23 +0,0 @@ -package dev.shared.berke.ramcleaner; - -import eu.darkbot.api.config.annotations.Configuration; -import eu.darkbot.api.config.annotations.Number; - -@Configuration("berke.ram_cleaner") -public final class DailyRamCleanerConfig { - public boolean enabled = true; - - @Number(min = 512, max = 16384, step = 128) - public int ramThresholdMb = 1200; - - @Number(min = 15, max = 600, step = 15) - public int checkIntervalSeconds = 30; - - public boolean avoidCombat = true; - - /** - * Clearing the game cache can cause assets to be loaded again. Keep this - * disabled unless ordinary working-set trimming is insufficient. - */ - public boolean clearGameCache = false; -} diff --git a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java b/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java deleted file mode 100644 index 202e9e55..00000000 --- a/src/main/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicy.java +++ /dev/null @@ -1,13 +0,0 @@ -package dev.shared.berke.ramcleaner; - -/** Pure decision logic kept separate so RAM cleanup rules can be unit tested. */ -final class DailyRamCleanerPolicy { - private DailyRamCleanerPolicy() { - } - - static boolean shouldClean(long memoryUsageMb, int thresholdMb, - boolean avoidCombat, boolean inCombat) { - if (memoryUsageMb < thresholdMb) return false; - return !avoidCombat || !inCombat; - } -} diff --git a/src/main/resources/plugin.json b/src/main/resources/plugin.json index 9717e660..5e825073 100644 --- a/src/main/resources/plugin.json +++ b/src/main/resources/plugin.json @@ -20,9 +20,7 @@ "dev.shared.do_gamer.module.simple_galaxy_gate.SimpleGalaxyGate", "dev.shared.do_gamer.task.autobuy.Autobuy", "dev.shared.halizeur.log_overlay.LogOverlay", - "dev.shared.berke.dailytaskapi.DailyTaskAPI", - "dev.shared.berke.petlocator.DailyNpcPetLocator", - "dev.shared.berke.ramcleaner.DailyRamCleaner" + "dev.shared.berke.dailytaskapi.DailyTaskAPI" ], "update": "https://raw.githubusercontent.com/Darkbot-Plugins/SharedPlugin/main/src/main/resources/plugin.json", "download": "https://github.com/Darkbot-Plugins/SharedPlugin/releases/latest/download/SharedPlugin.jar" diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java index f159b8fb..0a7719fe 100644 --- a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java @@ -39,7 +39,7 @@ void retainsNestedNpcAndMapRequirements() { assertTrue(DailyTaskPlanner.actionable(daily).contains(npc)); assertTrue(DailyTaskPlanner.actionable(daily).contains(map)); - DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(daily, "1"); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(daily); assertEquals("1-4", plan.targetMapName()); assertTrue(plan.targetNpcDescription().contains("Mordon")); } @@ -49,7 +49,7 @@ void parsesCoordinatesAndTypedMapVisits() { QuestAPI.Requirement coordinates = requirement("X: 12400 Y: 7600", "COORDINATES", QuestAPI.Requirement.RequirementType.COORDINATES, 0, 1, false); DailyQuestConditionEngine.Plan coordinatePlan = DailyQuestConditionEngine.build( - quest(5, true, false, List.of(coordinates, dailyTimer())), "1"); + quest(5, true, false, List.of(coordinates, dailyTimer()))); assertEquals(12400, coordinatePlan.targetCoordinates().x()); assertEquals(7600, coordinatePlan.targetCoordinates().y()); assertTrue(coordinatePlan.coordinatesOnly()); @@ -57,12 +57,12 @@ void parsesCoordinatesAndTypedMapVisits() { QuestAPI.Requirement visit = requirement("Visit map 1-8", "VISIT_MAP", QuestAPI.Requirement.RequirementType.VISIT_MAP, 0, 1, false); QuestAPI.Quest visitQuest = quest(6, true, false, List.of(visit, dailyTimer())); - assertEquals("1-8", DailyQuestConditionEngine.build(visitQuest, "1").targetMapName()); + assertEquals("1-8", DailyQuestConditionEngine.build(visitQuest).targetMapName()); assertTrue(DailyQuestConditionEngine.supports(visitQuest)); } @Test - void matchesNpcVariantsAndPreferredMaps() { + void matchesNpcVariantsWithoutMaintainingNpcMapLists() { List names = List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..", "( UberLordakium )"); assertEquals("-=[ Lordakium ]=-", DailyTaskPlanner.findNpc("Destroy Lordakium", names).orElseThrow()); assertTrue(DailyTaskPlanner.findNpc("Destroy Boss Lordakium", names).orElseThrow() @@ -70,14 +70,21 @@ void matchesNpcVariantsAndPreferredMaps() { assertTrue(DailyTaskPlanner.findNpc("Destroy Boss Sibelonit", List.of("-=[ Sibelonit ]=-")).isEmpty()); assertTrue(DailyTaskPlanner.findNpc("Destroy UberLordakium", List.of("-=[ Lordakium ]=-", "..::{ Boss Lordakium }::..")).isEmpty()); - assertEquals("1-3", DailyTaskPlanner.preferredMapForNpc("Destroy Saimon", "1").orElseThrow()); - assertEquals("1-6", DailyTaskPlanner.preferredMapForNpc("Destroy Lordakium", "1").orElseThrow()); - assertEquals("1-5", DailyTaskPlanner.preferredMapForNpc("Destroy Boss Lordakium", "1").orElseThrow()); - assertEquals("4-5", DailyTaskPlanner.preferredMapForNpc("Destroy UberKristallin", "1").orElseThrow()); - assertEquals("1-8", DailyTaskPlanner.preferredMapForNpc("Destroy StreuneR", "1").orElseThrow()); assertEquals(OreAPI.Ore.PROMETIUM, DailyTaskPlanner.findOre("Sell Prometium").orElseThrow()); } + @Test + void resolvesNpcMapThroughRuntimeMetadataResolver() { + QuestAPI.Requirement npc = requirement("Destroy a newly added NPC", "KILL_NPC", + QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 3, false); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build( + quest(20, true, false, List.of(npc, dailyTimer())), + ignored -> java.util.Optional.of("5-3")); + + assertEquals("5-3", plan.targetMapName()); + assertTrue(plan.targetNpcDescription().contains("newly added NPC")); + } + @Test void acceptsOnlyValidMapNames() { assertEquals("1-6", DailyTaskPlanner.findMap("Travel to 1-6").orElseThrow()); @@ -130,7 +137,7 @@ void buildsSequentialStepsFromRemainingObjectives() { QuestAPI.Requirement second = requirement("Destroy Lordakium", "KILL_NPC", QuestAPI.Requirement.RequirementType.KILL_NPC, 0, 1, false); DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build( - quest(11, true, false, List.of(first, second, dailyTimer())), "1"); + quest(11, true, false, List.of(first, second, dailyTimer()))); assertEquals(2, plan.steps().stream() .filter(step -> step.type() == DailyQuestConditionEngine.StepType.NPC_COMBAT) @@ -140,7 +147,7 @@ void buildsSequentialStepsFromRemainingObjectives() { QuestAPI.Requirement completedFirst = requirement("Destroy Mordon", "KILL_NPC", QuestAPI.Requirement.RequirementType.KILL_NPC, 1, 1, true); DailyQuestConditionEngine.Plan nextPlan = DailyQuestConditionEngine.build( - quest(12, true, false, List.of(completedFirst, second, dailyTimer())), "1"); + quest(12, true, false, List.of(completedFirst, second, dailyTimer()))); assertTrue(nextPlan.targetNpcDescription().contains("Lordakium")); } @@ -149,7 +156,7 @@ void classifiesPlayerCombatWithoutTreatingItAsNpcCombat() { QuestAPI.Requirement pvp = requirement("Destroy enemy players", "KILL_PLAYERS", QuestAPI.Requirement.RequirementType.KILL_PLAYERS, 0, 3, false); QuestAPI.Quest quest = quest(13, true, false, List.of(pvp, dailyTimer())); - DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest, "1"); + DailyQuestConditionEngine.Plan plan = DailyQuestConditionEngine.build(quest); assertTrue(DailyQuestConditionEngine.hasPlayerCombat(quest)); assertFalse(DailyQuestConditionEngine.supports(quest)); diff --git a/src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java b/src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java deleted file mode 100644 index 2d2127f2..00000000 --- a/src/test/java/dev/shared/berke/ramcleaner/DailyRamCleanerPolicyTest.java +++ /dev/null @@ -1,20 +0,0 @@ -package dev.shared.berke.ramcleaner; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -final class DailyRamCleanerPolicyTest { - @Test - void cleansOnlyAtOrAboveTheThreshold() { - assertFalse(DailyRamCleanerPolicy.shouldClean(1199, 1200, true, false)); - assertTrue(DailyRamCleanerPolicy.shouldClean(1200, 1200, true, false)); - } - - @Test - void respectsCombatProtection() { - assertFalse(DailyRamCleanerPolicy.shouldClean(1800, 1200, true, true)); - assertTrue(DailyRamCleanerPolicy.shouldClean(1800, 1200, false, true)); - } -} From 2bce7829b40ed547228f72e3944528d4ac0a72df Mon Sep 17 00:00:00 2001 From: Berke Date: Tue, 1 Sep 2026 18:54:11 +0300 Subject: [PATCH 14/14] Harden DailyTaskAPI runtime behavior --- .../berke/dailytaskapi/DailyNpcCombat.java | 34 ----- .../dailytaskapi/DailyNpcMapResolver.java | 15 +- .../berke/dailytaskapi/DailyPlayerCombat.java | 4 +- .../berke/dailytaskapi/DailyTaskAPI.java | 137 +++++++++++++----- .../dailytaskapi/DailyTaskCompanyMap.java | 36 +++++ .../berke/dailytaskapi/DailyTaskConfig.java | 23 ++- .../dev/shared/lang/strings_en.properties | 42 +++++- .../dev/shared/lang/strings_tr.properties | 42 +++++- .../dailytaskapi/DailyTaskPlannerTest.java | 65 +++++++++ 9 files changed, 318 insertions(+), 80 deletions(-) create mode 100644 src/main/java/dev/shared/berke/dailytaskapi/DailyTaskCompanyMap.java diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java index ef11aaad..ed50ef6c 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcCombat.java @@ -1,9 +1,7 @@ package dev.shared.berke.dailytaskapi; import eu.darkbot.api.PluginAPI; -import eu.darkbot.api.config.ConfigSetting; import eu.darkbot.api.game.entities.Npc; -import eu.darkbot.api.managers.ConfigAPI; import eu.darkbot.shared.modules.LootModule; import java.util.Comparator; @@ -18,26 +16,16 @@ * owner of quest and map selection.

*/ final class DailyNpcCombat extends LootModule { - private final ConfigSetting petEnabledSetting; - private final ConfigSetting keepRoamingPointSetting; private String targetDescription; private int targetRadius = 590; - private Boolean originalPetEnabledSetting; - private Boolean originalKeepRoamingPointSetting; - private Boolean originalPetRuntimeEnabled; DailyNpcCombat(PluginAPI api) { super(api); - ConfigAPI config = api.requireAPI(ConfigAPI.class); - petEnabledSetting = config.requireConfig("pet.enabled"); - keepRoamingPointSetting = config.requireConfig("general.roaming.keep"); } void tick(String description, int attackRadius) { targetDescription = description; targetRadius = attackRadius; - rememberUserSettings(); - if (!Boolean.TRUE.equals(petEnabledSetting.getValue())) petEnabledSetting.setValue(true); super.onTickModule(); } @@ -45,7 +33,6 @@ void stopCombat() { attack.stopAttack(); attack.setTarget(null); targetDescription = null; - restoreUserSettings(); } void shutdownPet() { @@ -82,32 +69,11 @@ protected boolean findTarget() { // MovementAPI.moveRandom follows the saved preferred-zone route for // this map and falls back to a random map point when no route exists. if (selected == null && !movement.isMoving()) { - rememberUserSettings(); - if (Boolean.TRUE.equals(keepRoamingPointSetting.getValue())) { - keepRoamingPointSetting.setValue(false); - } movement.moveRandom(); } return selected != null; } - private void rememberUserSettings() { - if (originalPetEnabledSetting != null) return; - originalPetEnabledSetting = petEnabledSetting.getValue(); - originalKeepRoamingPointSetting = keepRoamingPointSetting.getValue(); - originalPetRuntimeEnabled = pet.isEnabled(); - } - - private void restoreUserSettings() { - if (originalPetEnabledSetting == null) return; - petEnabledSetting.setValue(originalPetEnabledSetting); - keepRoamingPointSetting.setValue(originalKeepRoamingPointSetting); - pet.setEnabled(Boolean.TRUE.equals(originalPetRuntimeEnabled)); - originalPetEnabledSetting = null; - originalKeepRoamingPointSetting = null; - originalPetRuntimeEnabled = null; - } - private boolean isUsableTarget(Npc npc) { if (npc == null || targetDescription == null || !npc.isValid() || !npc.isSelectable() || npc.isBlacklisted()) { diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java index 8f7ed288..126251a3 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyNpcMapResolver.java @@ -5,8 +5,10 @@ import eu.darkbot.api.game.other.GameMap; import eu.darkbot.api.managers.StarSystemAPI; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.List; import java.util.Map; import java.util.Optional; @@ -21,10 +23,17 @@ final class DailyNpcMapResolver { } Optional resolve(String description, String companyPrefix) { - Map configured = npcInfos.getValue(); - if (configured == null || configured.isEmpty()) return Optional.empty(); + try { + Map configured = npcInfos.getValue(); + if (configured == null || configured.isEmpty()) return Optional.empty(); + return resolve(description, companyPrefix, new ArrayList<>(configured.values())); + } catch (RuntimeException ignored) { + return Optional.empty(); + } + } - Optional target = configured.values().stream() + private Optional resolve(String description, String companyPrefix, List configured) { + Optional target = configured.stream() .filter(info -> info != null && DailyTaskPlanner.matchesNpcName(description, info.getName())) .max(Comparator.comparingInt(info -> DailyTaskPlanner.normalizeNpcName(info.getName()).length())); diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java index 9dc31903..dd487540 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyPlayerCombat.java @@ -132,14 +132,14 @@ private Comparator playerComparator(DailyTaskConfig config) { private boolean isEligible(Player player, String objectiveDescription, DailyTaskConfig config) { if (player == null || !player.isValid() || !player.isSelectable() || - player.isInvisible() || player.getId() == hero.getId()) { + player.isInvisible() || player.isBlacklisted() || player.getId() == hero.getId()) { return false; } EntityInfo info = player.getEntityInfo(); if (info == null) return false; if (!matchesRequiredFaction(info, objectiveDescription)) return false; if (config.avoidAlliedAndGroupPlayers && isProtected(player, info)) return false; - return !config.enemyPlayersOnly || info.isEnemy() || player.isBlacklisted() || + return !config.enemyPlayersOnly || info.isEnemy() || info.getClanDiplomacy() == EntityInfo.Diplomacy.WAR; } diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java index f35f0942..84d72403 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskAPI.java @@ -12,6 +12,7 @@ import eu.darkbot.api.game.entities.Portal; import eu.darkbot.api.game.entities.Station; import eu.darkbot.api.game.enums.PetGear; +import eu.darkbot.api.game.other.EntityInfo; import eu.darkbot.api.game.other.GameMap; import eu.darkbot.api.game.other.Gui; import eu.darkbot.api.game.other.Locatable; @@ -52,10 +53,10 @@ @Feature(name = "DailyTaskAPI", description = "Completes only verified 24-hour daily quests") public final class DailyTaskAPI implements Module, Configurable, GearSelector, PetGearSupplier { - private static final String QUEST_BASE_MAP = "1-8"; private static final String PARK_AFTER_DAILY = "__PARK_AFTER_DAILY__"; private static final double QUEST_STATION_DISTANCE = 300d; - private static final Set SPECIAL_DAILY_QUEST_IDS = Set.of(318001); + private static final int MAX_SOURCE_RECOVERY_ATTEMPTS = 3; + private static final int MAX_GIVER_OPEN_ATTEMPTS = 30; private enum State { STARTING, DISCOVERING, WAITING_FOR_DISCOVERY, @@ -117,7 +118,7 @@ private int priority() { private DailyTaskConfig settings = new DailyTaskConfig(); private State state = State.STARTING; private int selectionRetries; - private int menuScanPass; + private int sourceRecoveryAttempts; private int selectorIndex; private int questIdBeforeSelection = -1; private int currentQuestId = -1; @@ -135,6 +136,7 @@ private int priority() { private double lastTrackedProgress = -1d; private long lastProgressAt; private final Set completedQuestIds = new HashSet<>(); + private final Set skippedActiveQuestIds = new HashSet<>(); private final Set knownDailyQuestIds = new HashSet<>(); private final Set scannedQuestIds = new LinkedHashSet<>(); private final Map dailyChoices = new LinkedHashMap<>(); @@ -142,7 +144,6 @@ private int priority() { private QuestChoice pendingChoice; private boolean offerScanNeeded = true; private int giverOpenAttempts; - private int dailyTabRetries; private int giverScrollPage; private boolean giverPageHadNewId; private int giverRowIndex = 1; @@ -155,6 +156,7 @@ private int priority() { private final Set acceptedOfferIds = new LinkedHashSet<>(); private final Map skippedOfferCounts = new EnumMap<>(DailyQuestPolicy.Decision.class); + private boolean completionStopIssued; public DailyTaskAPI(PluginAPI api) { this.quests = api.requireAPI(QuestAPI.class); @@ -197,18 +199,25 @@ public void onTickModule() { private boolean handleCompletedState(long now) { if (state != State.DONE) return false; + if (completionStopIssued && bot.isRunning()) { + state = State.STARTING; + completionStopIssued = false; + return false; + } if (nextActionAt <= now) { state = State.STARTING; return false; } npcCombat.shutdownPet(); if (npcCombat.isPetActive() || now < nextRoamAt) { - status = "ALL DAILY QUESTS COMPLETED | PET is shutting down"; + status = completionSummary() + " | PET is shutting down"; return true; } - status = "ALL DAILY QUESTS COMPLETED | Completed: " + completedQuestIds.size() + - " | PET off | Bot stopped"; - if (bot.isRunning()) bot.setRunning(false); + status = completionSummary() + " | PET off | Bot stopped"; + if (bot.isRunning()) { + completionStopIssued = true; + bot.setRunning(false); + } return true; } @@ -301,20 +310,20 @@ private void scheduleQuestMenuScan(long now, boolean parkFirst) { private void resetSession() { selectionRetries = 0; - menuScanPass = 0; + sourceRecoveryAttempts = 0; selectorIndex = 0; questIdBeforeSelection = -1; currentQuestId = -1; currentQuestTitle = ""; pendingChoice = null; completedQuestIds.clear(); + skippedActiveQuestIds.clear(); knownDailyQuestIds.clear(); scannedQuestIds.clear(); dailyChoices.clear(); selectors = List.of(); offerScanNeeded = true; giverOpenAttempts = 0; - dailyTabRetries = 0; giverScrollPage = 0; giverPageHadNewId = false; giverRowIndex = 1; @@ -326,6 +335,7 @@ private void resetSession() { seenGiverOfferIds.clear(); acceptedOfferIds.clear(); skippedOfferCounts.clear(); + completionStopIssued = false; resetProgressTracking(); clearPlan(); status = "Scanning the native quest menu"; @@ -398,7 +408,7 @@ private boolean ensureQuestSelectors() { retryMenuScan(questMenuSource.getLastError()); return false; } - menuScanPass++; + sourceRecoveryAttempts = 0; selectorIndex = 0; selectionRetries = 0; scannedQuestIds.clear(); @@ -503,7 +513,6 @@ private void beginQuestOfferScan() { // quests had been accepted forever, causing an active-menu scan loop. acceptedOfferIds.clear(); giverOpenAttempts = 0; - dailyTabRetries = 0; giverScrollPage = 0; giverPageHadNewId = false; giverRowIndex = 1; @@ -559,6 +568,10 @@ private void tickOpeningGiver() { findQuestStation().filter(station -> hero.distanceTo(station) <= QUEST_STATION_DISTANCE) .ifPresent(station -> station.trySelect(false)); giverOpenAttempts++; + if (giverOpenAttempts >= MAX_GIVER_OPEN_ATTEMPTS) { + pauseSafely("Quest giver did not open after " + giverOpenAttempts + " attempts"); + return; + } state = State.WAITING_GIVER_OPEN; status = "Waiting for quest giver response (" + giverOpenAttempts + ")"; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; @@ -741,7 +754,6 @@ private void verifyAcceptedOffer() { selectedOfferTitle = ""; giverRowIndex = 1; giverRowRetries = 0; - dailyTabRetries = 0; giverPageHadNewId = false; state = quests.isQuestGiverOpen() ? State.SELECTING_GIVER_ROW : State.OPENING_GIVER; nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; @@ -753,7 +765,11 @@ private void verifyAcceptedOffer() { nextActionAt = System.currentTimeMillis() + settings.questSwitchDelayMs; return; } - pauseSafely("Could not verify daily acceptance: " + selectedOfferTitle); + reviewedOfferIds.add(selectedOfferId); + status = "Could not verify daily acceptance; continuing safely: " + selectedOfferTitle; + selectedOfferId = -1; + selectedOfferTitle = ""; + advanceGiverRow(); } private Optional findQuestItem(int id) { @@ -840,14 +856,22 @@ private void retryDiscoveredSelector(String reason) { } private void retryMenuScan(String reason) { - status = reason + "; single scan completed"; + sourceRecoveryAttempts++; + if (sourceRecoveryAttempts < MAX_SOURCE_RECOVERY_ATTEMPTS) { + selectors = List.of(); + status = reason + "; retrying source discovery (" + sourceRecoveryAttempts + "/" + + MAX_SOURCE_RECOVERY_ATTEMPTS + ")"; + nextActionAt = System.currentTimeMillis() + Math.max(750L, settings.questSwitchDelayMs); + return; + } + status = reason + "; source discovery unavailable after safe retries"; if (offerScanNeeded) beginQuestOfferScan(); else finishAll(); } private void prepareNewScan() { state = State.DISCOVERING; - menuScanPass = 0; + sourceRecoveryAttempts = 0; selectorIndex = 0; selectionRetries = 0; questIdBeforeSelection = -1; @@ -855,6 +879,7 @@ private void prepareNewScan() { selectors = List.of(); scannedQuestIds.clear(); dailyChoices.clear(); + skippedActiveQuestIds.clear(); } private void refreshDailyCatalog() { @@ -876,9 +901,14 @@ private void recordQuest(QuestAPI.Quest quest, QuestMenuSource.Selector selector if (!isVerifiedDaily(quest)) return; if (isFinished(quest)) { completedQuestIds.add(quest.getId()); + skippedActiveQuestIds.remove(quest.getId()); return; } - if (!DailyQuestPolicy.evaluateActive(quest, settings).accepted()) return; + if (!DailyQuestPolicy.evaluateActive(quest, settings).accepted()) { + skippedActiveQuestIds.add(quest.getId()); + return; + } + skippedActiveQuestIds.remove(quest.getId()); QuestChoice choice = new QuestChoice(quest.getId(), safeTitle(quest), selector, DailyQuestPolicy.priority(quest, settings)); QuestChoice existing = dailyChoices.get(quest.getId()); @@ -888,8 +918,8 @@ private void recordQuest(QuestAPI.Quest quest, QuestMenuSource.Selector selector } private boolean isVerifiedDaily(QuestAPI.Quest quest) { - return quest != null && (SPECIAL_DAILY_QUEST_IDS.contains(quest.getId()) || - acceptedOfferIds.contains(quest.getId()) || knownDailyQuestIds.contains(quest.getId()) || + return quest != null && (acceptedOfferIds.contains(quest.getId()) || + knownDailyQuestIds.contains(quest.getId()) || DailyTaskPlanner.isDaily(quest)); } @@ -975,6 +1005,10 @@ private boolean handleOreSale(QuestAPI.Quest quest, double progress) { } if (!ores.canSellOres()) { String base = homeBaseMapName(); + if (base == null) { + updateProgressStatus(quest, progress, "Waiting for company data before travelling to base"); + return true; + } if (!isOnMap(base)) navigateTo(base); updateProgressStatus(quest, progress, "Travelling to base to sell ore"); return true; @@ -1051,10 +1085,16 @@ private boolean runLocalSafety() { } private boolean ensureQuestStation() { - if (!isOnMap(QUEST_BASE_MAP)) { + String questBaseMap = questBaseMapName(); + if (questBaseMap == null) { + attack.stopAttack(); + status = "Waiting for company data before locating the quest center"; + return false; + } + if (!isOnMap(questBaseMap)) { if (!selectors.isEmpty() || state != State.DISCOVERING) prepareNewScan(); if (questGui != null && questGui.isVisible()) questGui.setVisible(false); - navigateToQuestBase(); + navigateToQuestBase(questBaseMap); return false; } @@ -1062,7 +1102,7 @@ private boolean ensureQuestStation() { if (station.isEmpty()) { attack.stopAttack(); hero.setRoamMode(); - status = QUEST_BASE_MAP + " ana istasyon verisi bekleniyor"; + status = "Waiting for main-station data on " + questBaseMap; return false; } @@ -1071,7 +1111,8 @@ private boolean ensureQuestStation() { attack.stopAttack(); hero.setRoamMode(); movement.moveTo(station.get()); - status = "Approaching main quest station on " + QUEST_BASE_MAP + ": " + (int) Math.round(distance); + status = "Approaching main quest station on " + questBaseMap + ": " + + (int) Math.round(distance); return false; } @@ -1097,11 +1138,11 @@ private Optional findQuestStation() { .min(Comparator.comparingDouble(hero::distanceTo)); } - private void navigateToQuestBase() { - Optional destination = starSystem.findMap(QUEST_BASE_MAP); + private void navigateToQuestBase(String questBaseMap) { + Optional destination = starSystem.findMap(questBaseMap); if (destination.isEmpty() || !starSystem.isAccessible(destination.get())) { attack.stopAttack(); - status = "Waiting for quest-center map data: " + QUEST_BASE_MAP; + status = "Waiting for quest-center map data: " + questBaseMap; return; } @@ -1111,7 +1152,7 @@ private void navigateToQuestBase() { // normal loading state, so wait for the next tick instead of // entering a permanent safe pause. attack.stopAttack(); - status = "Loading quest-center route: " + QUEST_BASE_MAP; + status = "Loading quest-center route: " + questBaseMap; return; } @@ -1119,10 +1160,14 @@ private void navigateToQuestBase() { hero.setRoamMode(); if (hero.distanceTo(portal) > 220d) movement.moveTo(portal); else movement.jumpPortal(portal); - status = "Travelling to " + QUEST_BASE_MAP + " for the quest center"; + status = "Travelling to " + questBaseMap + " for the quest center"; } private void navigateTo(String mapName) { + if (mapName == null || mapName.isBlank()) { + status = "Waiting for destination map data"; + return; + } Optional destination = starSystem.findMap(mapName); if (destination.isEmpty() || !starSystem.isAccessible(destination.get())) { // Map/route data can be temporarily unavailable while a portal jump is @@ -1176,20 +1221,30 @@ private void roamIfNeeded() { private boolean isOnMap(String mapName) { GameMap current = starSystem.getCurrentMap(); - return current != null && current.getName().equalsIgnoreCase(mapName); + return current != null && current.getName() != null && + current.getName().equalsIgnoreCase(mapName); } private String homeBaseMapName() { - GameMap current = starSystem.getCurrentMap(); - String prefix = current == null ? "1" : current.getName().split("-")[0]; - if (!prefix.matches("[1-3]")) prefix = "1"; - return prefix + "-1"; + return DailyTaskCompanyMap.homeBase(heroFaction(), currentMapName()); + } + + private String questBaseMapName() { + return DailyTaskCompanyMap.questBase(heroFaction(), currentMapName()); } private String companyPrefix() { + return DailyTaskCompanyMap.prefix(heroFaction(), currentMapName()); + } + + private EntityInfo.Faction heroFaction() { + EntityInfo info = hero.getEntityInfo(); + return info == null ? null : info.getFaction(); + } + + private String currentMapName() { GameMap current = starSystem.getCurrentMap(); - String prefix = current == null ? "1" : current.getName().split("-")[0]; - return prefix.matches("[1-3]") ? prefix : "1"; + return current == null ? null : current.getName(); } private boolean recoverStagnantPlan(QuestAPI.Quest quest, double progress, long now) { @@ -1243,8 +1298,15 @@ private void finishAll() { // Keep DarkBot ticking briefly so PetManager can send and observe the // actual PET deactivation before the bot's running flag is cleared. nextRoamAt = System.currentTimeMillis() + 5_000L; - status = "ALL DAILY QUESTS COMPLETED | Completed: " + completedQuestIds.size() + - " | PET is shutting down | Bot will stop afterwards"; + status = completionSummary() + " | PET is shutting down | Bot will stop afterwards"; + } + + private String completionSummary() { + if (skippedActiveQuestIds.isEmpty()) { + return "ALL SUPPORTED DAILY QUESTS COMPLETED | Completed: " + completedQuestIds.size(); + } + return "NO RUNNABLE DAILY QUESTS REMAIN | Completed: " + completedQuestIds.size() + + " | Skipped by settings or unsupported: " + skippedActiveQuestIds.size(); } private void stopCurrentAction() { @@ -1267,6 +1329,7 @@ private void clearPlan() { } private String getNpcLocatorTargetDescription() { + if (!bot.isRunning() || bot.getNonTemporalModule() != this) return null; if (state != State.RUNNING || targetNpcDescription == null) return null; if (targetMapName != null && !isOnMap(targetMapName)) return null; return targetNpcDescription; diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskCompanyMap.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskCompanyMap.java new file mode 100644 index 00000000..3a8df59a --- /dev/null +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskCompanyMap.java @@ -0,0 +1,36 @@ +package dev.shared.berke.dailytaskapi; + +import eu.darkbot.api.game.other.EntityInfo; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Resolves company-owned maps without relying on the ship's current map. */ +final class DailyTaskCompanyMap { + private static final Pattern COMPANY_MAP = Pattern.compile("^([1-3])-"); + + private DailyTaskCompanyMap() { + } + + static String prefix(EntityInfo.Faction faction, String currentMapName) { + if (faction == EntityInfo.Faction.MMO) return "1"; + if (faction == EntityInfo.Faction.EIC) return "2"; + if (faction == EntityInfo.Faction.VRU) return "3"; + if (currentMapName == null) return null; + Matcher matcher = COMPANY_MAP.matcher(currentMapName); + return matcher.find() ? matcher.group(1) : null; + } + + static String homeBase(EntityInfo.Faction faction, String currentMapName) { + return companyMap(faction, currentMapName, "1"); + } + + static String questBase(EntityInfo.Faction faction, String currentMapName) { + return companyMap(faction, currentMapName, "8"); + } + + private static String companyMap(EntityInfo.Faction faction, String currentMapName, String suffix) { + String prefix = prefix(faction, currentMapName); + return prefix == null ? null : prefix + "-" + suffix; + } +} diff --git a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java index f8b5b4fe..ae05630e 100644 --- a/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java +++ b/src/main/java/dev/shared/berke/dailytaskapi/DailyTaskConfig.java @@ -2,54 +2,73 @@ import eu.darkbot.api.config.annotations.Configuration; import eu.darkbot.api.config.annotations.Number; +import eu.darkbot.api.config.annotations.Option; @Configuration("dailytaskapi.config") public final class DailyTaskConfig { + @Option("dailytaskapi.auto_open_quest_window") public boolean autoOpenQuestWindow = true; + @Option("dailytaskapi.require_uridium_reward") public boolean requireUridiumReward = true; + @Option("dailytaskapi.skip_tetrathrin_only") public boolean skipTetrathrinOnly = true; + @Option("dailytaskapi.skip_protegit") public boolean skipProtegitQuests = true; + @Option("dailytaskapi.accept_player_kills") public boolean acceptPlayerKillQuests = false; + @Option("dailytaskapi.prefer_shorter") public boolean preferShorterQuests = true; + @Option("dailytaskapi.server_delay") @Number(min = 250, max = 20000, step = 250) public int questSwitchDelayMs = 500; + @Option("dailytaskapi.response_retries") @Number(min = 1, max = 10, step = 1) - public int maxSelectionRetries = 1; + public int maxSelectionRetries = 4; + @Option("dailytaskapi.npc_radius") @Number(min = 300, max = 750, step = 25) public int attackRadius = 590; + @Option("dailytaskapi.player_radius") @Number(min = 300, max = 750, step = 25) public int playerAttackRadius = 590; + @Option("dailytaskapi.enemy_players_only") public boolean enemyPlayersOnly = true; + @Option("dailytaskapi.avoid_allies") public boolean avoidAlliedAndGroupPlayers = true; + @Option("dailytaskapi.prioritize_attackers") public boolean prioritizePlayersAttackingMe = true; + @Option("dailytaskapi.player_map") public String playerCombatMap = ""; + @Option("dailytaskapi.player_min_shield") @Number(min = 0.00, max = 0.90, step = 0.05) public double playerMinimumShieldPercent = 0.20; + @Option("dailytaskapi.max_player_deaths") @Number(min = 0, max = 20, step = 1) public int maxPlayerDeaths = 2; + @Option("dailytaskapi.player_search_timeout") @Number(min = 30, max = 1800, step = 30) public int playerSearchTimeoutSeconds = 300; + @Option("dailytaskapi.minimum_hp") @Number(min = 0.10, max = 0.90, step = 0.05) public double minimumHpPercent = 0.30; + @Option("dailytaskapi.stagnation_timeout") @Number(min = 60, max = 900, step = 30) public int stagnationTimeoutSeconds = 300; - } diff --git a/src/main/resources/dev/shared/lang/strings_en.properties b/src/main/resources/dev/shared/lang/strings_en.properties index 35da6b6d..ceda1975 100644 --- a/src/main/resources/dev/shared/lang/strings_en.properties +++ b/src/main/resources/dev/shared/lang/strings_en.properties @@ -251,4 +251,44 @@ do_gamer.autobuy.ammo.slugElsD01=ELS-D01 (Slug) do_gamer.autobuy.ammo.spoolGsp30=GSP-30 (Spool) do_gamer.autobuy.purchase.amount=Purchase amount do_gamer.autobuy.purchase.min= Min in inventory -do_gamer.autobuy.purchase.min.desc=Minimum amount of the item in inventory to trigger the purchase. \ No newline at end of file +do_gamer.autobuy.purchase.min.desc=Minimum amount of the item in inventory to trigger the purchase. + +dailytaskapi.config=Daily Task API +dailytaskapi.auto_open_quest_window=Open the quest window automatically +dailytaskapi.auto_open_quest_window.desc=Opens the active quest window when a daily-task scan starts. +dailytaskapi.require_uridium_reward=Accept only quests with Uridium +dailytaskapi.require_uridium_reward.desc=Skips station daily offers that do not contain a positive Uridium reward. +dailytaskapi.skip_tetrathrin_only=Skip Tetrathrin-only quests +dailytaskapi.skip_tetrathrin_only.desc=Skips offers whose only non-currency reward is Tetrathrin. +dailytaskapi.skip_protegit=Skip Protegit quests +dailytaskapi.skip_protegit.desc=Prevents accepting or running daily quests that require Protegit kills. +dailytaskapi.accept_player_kills=Accept player-combat quests +dailytaskapi.accept_player_kills.desc=Opt in to daily quests that require attacking players. Disabled by default for safety. +dailytaskapi.prefer_shorter=Prefer shorter quests +dailytaskapi.prefer_shorter.desc=Runs supported daily quests with less remaining work first. +dailytaskapi.server_delay=Server response delay (ms) +dailytaskapi.server_delay.desc=Wait time between quest-window actions. Increase this on slow servers. +dailytaskapi.response_retries=Server response retries +dailytaskapi.response_retries.desc=Number of safe retries before an unresponsive quest row is skipped. +dailytaskapi.npc_radius=NPC attack radius +dailytaskapi.npc_radius.desc=Maximum distance for selecting the NPC required by the current daily quest. +dailytaskapi.player_radius=Player attack radius +dailytaskapi.player_radius.desc=Maximum distance for engaging an eligible player target. +dailytaskapi.enemy_players_only=Enemy players only +dailytaskapi.enemy_players_only.desc=Rejects player targets that DarkBot does not classify as hostile. +dailytaskapi.avoid_allies=Avoid allies and group members +dailytaskapi.avoid_allies.desc=Never selects group members, allies or non-aggression-pact players. +dailytaskapi.prioritize_attackers=Prioritize players attacking me +dailytaskapi.prioritize_attackers.desc=Prefers an eligible player who is already attacking the ship. +dailytaskapi.player_map=Player-combat map +dailytaskapi.player_map.desc=Optional map used for player-combat quests when the quest does not specify one. +dailytaskapi.player_min_shield=Minimum shield for player combat +dailytaskapi.player_min_shield.desc=Stops local player combat and moves toward safety below this shield percentage. +dailytaskapi.max_player_deaths=Maximum player-combat deaths +dailytaskapi.max_player_deaths.desc=Safely pauses a player-combat quest after this many deaths. Set to 0 for no limit. +dailytaskapi.player_search_timeout=Player search timeout (seconds) +dailytaskapi.player_search_timeout.desc=Safely pauses when no eligible player is found within this time. +dailytaskapi.minimum_hp=Minimum HP +dailytaskapi.minimum_hp.desc=Moves toward the nearest portal when local task handling falls below this HP percentage. +dailytaskapi.stagnation_timeout=Stagnation recovery (seconds) +dailytaskapi.stagnation_timeout.desc=Reevaluates the target and route if quest progress does not change for this long. diff --git a/src/main/resources/dev/shared/lang/strings_tr.properties b/src/main/resources/dev/shared/lang/strings_tr.properties index 4f07a24f..fef06d24 100644 --- a/src/main/resources/dev/shared/lang/strings_tr.properties +++ b/src/main/resources/dev/shared/lang/strings_tr.properties @@ -251,4 +251,44 @@ do_gamer.autobuy.ammo.slugElsD01=ELS-D01 (Slug) do_gamer.autobuy.ammo.spoolGsp30=GSP-30 (Spool) do_gamer.autobuy.purchase.amount=Purchase amount do_gamer.autobuy.purchase.min= Min in inventory -do_gamer.autobuy.purchase.min.desc=Minimum amount of the item in inventory to trigger the purchase. \ No newline at end of file +do_gamer.autobuy.purchase.min.desc=Minimum amount of the item in inventory to trigger the purchase. + +dailytaskapi.config=Günlük Görev API +dailytaskapi.auto_open_quest_window=Görev penceresini otomatik aç +dailytaskapi.auto_open_quest_window.desc=Günlük görev taraması başladığında aktif görev penceresini açar. +dailytaskapi.require_uridium_reward=Yalnız Uridium ödüllü görevleri al +dailytaskapi.require_uridium_reward.desc=Pozitif Uridium ödülü içermeyen istasyon günlüklerini atlar. +dailytaskapi.skip_tetrathrin_only=Yalnız Tetrathrin verenleri atla +dailytaskapi.skip_tetrathrin_only.desc=Tek eşya ödülü Tetrathrin olan görevleri atlar. +dailytaskapi.skip_protegit=Protegit görevlerini atla +dailytaskapi.skip_protegit.desc=Protegit öldürmeyi isteyen günlük görevlerin alınmasını ve çalıştırılmasını engeller. +dailytaskapi.accept_player_kills=Oyuncu savaşı görevlerini al +dailytaskapi.accept_player_kills.desc=Oyunculara saldırmayı isteyen günlük görevleri etkinleştirir. Güvenlik için varsayılan olarak kapalıdır. +dailytaskapi.prefer_shorter=Daha kısa görevleri önce yap +dailytaskapi.prefer_shorter.desc=Kalan işi daha az olan desteklenen günlük görevleri önce çalıştırır. +dailytaskapi.server_delay=Sunucu yanıt gecikmesi (ms) +dailytaskapi.server_delay.desc=Görev penceresi işlemleri arasındaki bekleme süresi. Yavaş sunucularda artırın. +dailytaskapi.response_retries=Sunucu yanıt denemeleri +dailytaskapi.response_retries.desc=Yanıt vermeyen bir görev satırı atlanmadan önce yapılacak güvenli deneme sayısı. +dailytaskapi.npc_radius=NPC saldırı menzili +dailytaskapi.npc_radius.desc=Etkin günlük görevin istediği NPC'yi seçmek için kullanılacak en yüksek mesafe. +dailytaskapi.player_radius=Oyuncu saldırı menzili +dailytaskapi.player_radius.desc=Uygun bir oyuncu hedefiyle çatışmaya girilecek en yüksek mesafe. +dailytaskapi.enemy_players_only=Yalnız düşman oyuncular +dailytaskapi.enemy_players_only.desc=DarkBot'un düşman olarak sınıflandırmadığı oyuncu hedeflerini reddeder. +dailytaskapi.avoid_allies=Müttefiklerden ve grup üyelerinden kaçın +dailytaskapi.avoid_allies.desc=Grup üyelerini, müttefikleri ve saldırmazlık anlaşmalı oyuncuları asla seçmez. +dailytaskapi.prioritize_attackers=Saldıran oyunculara öncelik ver +dailytaskapi.prioritize_attackers.desc=Gemiye zaten saldıran uygun oyuncuya öncelik verir. +dailytaskapi.player_map=Oyuncu savaşı haritası +dailytaskapi.player_map.desc=Görev harita belirtmiyorsa oyuncu savaşı görevlerinde kullanılacak isteğe bağlı harita. +dailytaskapi.player_min_shield=Oyuncu savaşı asgari kalkanı +dailytaskapi.player_min_shield.desc=Kalkan bu oranın altına indiğinde oyuncu savaşını bırakıp güvenli alana gider. +dailytaskapi.max_player_deaths=En fazla oyuncu savaşı ölümü +dailytaskapi.max_player_deaths.desc=Bu ölüm sayısından sonra oyuncu savaşı görevini güvenli biçimde duraklatır. Sınırsız için 0. +dailytaskapi.player_search_timeout=Oyuncu arama zaman aşımı (saniye) +dailytaskapi.player_search_timeout.desc=Bu süre içinde uygun oyuncu bulunamazsa güvenli biçimde duraklatır. +dailytaskapi.minimum_hp=Asgari DP +dailytaskapi.minimum_hp.desc=Yerel görev işlemleri sırasında DP bu oranın altına düşerse en yakın portala gider. +dailytaskapi.stagnation_timeout=İlerleme kurtarma süresi (saniye) +dailytaskapi.stagnation_timeout.desc=Görev ilerlemesi bu süre boyunca değişmezse hedefi ve güzergâhı yeniden değerlendirir. diff --git a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java index 0a7719fe..ab5a72ef 100644 --- a/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java +++ b/src/test/java/dev/shared/berke/dailytaskapi/DailyTaskPlannerTest.java @@ -1,10 +1,19 @@ package dev.shared.berke.dailytaskapi; +import eu.darkbot.api.config.ConfigSetting; +import eu.darkbot.api.config.types.NpcInfo; +import eu.darkbot.api.game.other.EntityInfo; +import eu.darkbot.api.game.other.GameMap; import eu.darkbot.api.managers.OreAPI; import eu.darkbot.api.managers.QuestAPI; +import eu.darkbot.api.managers.StarSystemAPI; import org.junit.jupiter.api.Test; +import java.lang.reflect.Proxy; import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -221,6 +230,42 @@ void computesRemainingWorkForQuestPriority() { DailyTaskPlanner.remainingWork( quest(19, true, false, List.of(longTask, dailyTimer())))); assertEquals(590, new DailyTaskConfig().attackRadius); + assertEquals(4, new DailyTaskConfig().maxSelectionRetries); + } + + @Test + void resolvesCompanyMapsFromFactionBeforeCurrentMap() { + assertEquals("1-8", DailyTaskCompanyMap.questBase(EntityInfo.Faction.MMO, "5-3")); + assertEquals("2-1", DailyTaskCompanyMap.homeBase(EntityInfo.Faction.EIC, "1-7")); + assertEquals("3", DailyTaskCompanyMap.prefix(EntityInfo.Faction.VRU, null)); + assertEquals("2", DailyTaskCompanyMap.prefix(EntityInfo.Faction.NONE, "2-BL")); + assertNull(DailyTaskCompanyMap.questBase(EntityInfo.Faction.NONE, "4-4")); + } + + @Test + @SuppressWarnings("unchecked") + void resolvesNpcMapsFromDarkBotMetadataWithoutNpcLists() { + GameMap mmoMap = gameMap(101, "1-5"); + GameMap eicMap = gameMap(202, "2-5"); + NpcInfo npc = proxy(NpcInfo.class, (method, args) -> { + if (method.equals("getName")) return "-=[ Future NPC ]=-"; + if (method.equals("getMapIds")) return Set.of(101, 202); + return null; + }); + ConfigSetting> setting = proxy(ConfigSetting.class, + (method, args) -> method.equals("getValue") ? Map.of("future", npc) : null); + StarSystemAPI maps = proxy(StarSystemAPI.class, (method, args) -> { + if (method.equals("getCurrentMap")) return gameMap(404, "4-4"); + if (method.equals("isAccessible")) return true; + if (method.equals("findMap") && args[0] instanceof Integer) { + int id = (Integer) args[0]; + return Optional.ofNullable(id == 101 ? mmoMap : id == 202 ? eicMap : null); + } + return null; + }); + + DailyNpcMapResolver resolver = new DailyNpcMapResolver(setting, maps); + assertEquals("2-5", resolver.resolve("Destroy Future NPC", "2").orElseThrow()); } private static QuestAPI.Requirement dailyTimer() { @@ -262,6 +307,26 @@ private static QuestAPI.Quest quest(int id, boolean active, boolean completed, return quest(id, active, completed, requirements, List.of()); } + private static GameMap gameMap(int id, String name) { + return proxy(GameMap.class, (method, args) -> { + if (method.equals("getId")) return id; + if (method.equals("getName") || method.equals("getShortName")) return name; + if (method.equals("isGG") || method.equals("isPvp")) return false; + return null; + }); + } + + @SuppressWarnings("unchecked") + private static T proxy(Class type, ProxyCall call) { + return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, + (instance, method, args) -> call.invoke(method.getName(), args)); + } + + @FunctionalInterface + private interface ProxyCall { + Object invoke(String method, Object[] args); + } + private static QuestAPI.Quest quest(int id, boolean active, boolean completed, List requirements, List rewards) {