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 extends PetAPI.LocatorPick> 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 extends Portal> 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 extends Station> 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 extends Station> 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 extends Station> findQuestStation() {
+ Optional extends Station> questGiver = entities.getStations().stream()
+ .filter(Station::isValid)
+ .filter(station -> station instanceof Station.QuestGiver)
+ .min(Comparator.comparingDouble(hero::distanceTo));
+ if (questGiver.isPresent()) return questGiver;
+
+ Optional extends Station> 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 extends Box> 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 extends QuestAPI.Reward> 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 extends QuestAPI.Reward> 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 extends QuestAPI.Requirement> 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 extends QuestAPI.Requirement> 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 extends QuestAPI.Requirement> 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 extends QuestAPI.Requirement> 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 extends QuestAPI.Requirement> 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 extends QuestAPI.Requirement> getRequirements() { return requirements; }
+ public List extends QuestAPI.Reward> 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 extends Portal> 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 extends Portal> 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 extends Portal> 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 extends Station> 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 extends QuestAPI.Reward> 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 extends QuestAPI.Reward> 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 extends PetAPI.LocatorPick> 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 extends QuestAPI.Requirement> 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 extends QuestAPI.Requirement> getRequirements() { return requirements; }
public List extends QuestAPI.Reward> 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 extends QuestAPI.Reward> 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 extends QuestAPI.Reward> 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 extends QuestAPI.Reward> 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 extends QuestAPI.Requirement> requirements) {
+ return quest(id, active, completed, requirements, List.of());
+ }
+
+ private static QuestAPI.Quest quest(int id, boolean active, boolean completed,
+ List extends QuestAPI.Requirement> requirements,
+ List extends QuestAPI.Reward> 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 extends QuestAPI.Requirement> getRequirements() { return requirements; }
- public List extends QuestAPI.Reward> getRewards() { return List.of(); }
+ public List extends QuestAPI.Reward> 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 extends QuestAPI.Reward> 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