diff --git a/server/src/CMakeLists.txt b/server/src/CMakeLists.txt index 1e816a9..f0cb0ed 100644 --- a/server/src/CMakeLists.txt +++ b/server/src/CMakeLists.txt @@ -48,10 +48,12 @@ set(tfs_SRC ${CMAKE_CURRENT_LIST_DIR}/playerbotcombat.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotcombatruntime.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotcontroller.cpp + ${CMAKE_CURRENT_LIST_DIR}/playerbotdepotworkflow.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotdepotsession.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotdeparture.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotequipment.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotequipmentpolicy.cpp + ${CMAKE_CURRENT_LIST_DIR}/playerboteconomy.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotgoalarbiter.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbothuntpolicy.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbothuntplanningsession.cpp @@ -71,6 +73,7 @@ set(tfs_SRC ${CMAKE_CURRENT_LIST_DIR}/playerbottestpolicy.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotservice.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotservicesession.cpp + ${CMAKE_CURRENT_LIST_DIR}/playerbotserviceworkflow.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbothuntregions.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotnavigation.cpp ${CMAKE_CURRENT_LIST_DIR}/playerbotnavigationruntime.cpp diff --git a/server/src/playerbotcombat.cpp b/server/src/playerbotcombat.cpp index f6a0856..d289241 100644 --- a/server/src/playerbotcombat.cpp +++ b/server/src/playerbotcombat.cpp @@ -92,7 +92,7 @@ PlayerBotSurvivalSnapshot PlayerBotController::survivalSnapshot(const Player& pl } } snapshot.canDoAction = player.canDoAction(); - snapshot.buyingPotions = serviceStage == ServiceStage::BuyPotions; + snapshot.buyingPotions = serviceWorkflow.stage() == PlayerBotServiceStage::BuyPotions; snapshot.lootMovePending = lootWorkflow.hasPendingLootMove(); snapshot.progressionActive = progressionSession.active() != PlayerBotProgressionProcedure::None; snapshot.progressionDeparture = progressionSession.active(PlayerBotProgressionProcedure::OracleDeparture); @@ -1099,9 +1099,9 @@ void PlayerBotController::processTraversal(Player* player, const Position& curre if (pauseDepotFixtureForRestart(*player, DepotRestartCheckpoint::Approach, currentPosition)) { return; } - if (!processNavigation(player, currentPosition, depotSession.approachPosition())) { + if (!processNavigation(player, currentPosition, depotWorkflow.approachPosition())) { if (fixedTargetRouteFailureCount != 0) { - depotSession.rejectApproach(depotSession.approachPosition(), std::chrono::steady_clock::now() + depotApproachSuppression); + depotWorkflow.rejectApproach(depotWorkflow.approachPosition(), std::chrono::steady_clock::now() + depotApproachSuppression); clearDepotDiscovery(); clearNavigation(); } @@ -1125,12 +1125,12 @@ void PlayerBotController::processTraversal(Player* player, const Position& curre if (!discoverDepot(*player, currentPosition)) { return; } - if (!Position::areInRange<1, 1, 0>(currentPosition, depotSession.lockerPosition())) { + if (!Position::areInRange<1, 1, 0>(currentPosition, depotWorkflow.lockerPosition())) { setCyclePhase(CyclePhase::ReturnToDepot, currentPosition, "displaced_during_deposit"); clearNavigation(); - if (!processNavigation(player, currentPosition, depotSession.approachPosition())) { + if (!processNavigation(player, currentPosition, depotWorkflow.approachPosition())) { if (fixedTargetRouteFailureCount != 0) { - depotSession.rejectApproach(depotSession.approachPosition(), std::chrono::steady_clock::now() + depotApproachSuppression); + depotWorkflow.rejectApproach(depotWorkflow.approachPosition(), std::chrono::steady_clock::now() + depotApproachSuppression); clearDepotDiscovery(); clearNavigation(); } diff --git a/server/src/playerbotcontroller.cpp b/server/src/playerbotcontroller.cpp index 7f6380b..eedd928 100644 --- a/server/src/playerbotcontroller.cpp +++ b/server/src/playerbotcontroller.cpp @@ -20,7 +20,7 @@ PlayerBotController::PlayerBotController(const Player& player, playerId(player.getID()), playerGuid(player.getGUID()), playerName(player.getName()), fixtureRuntime(playerBotTestPolicyFromEnvironment()), telemetry(player.getName(), player.getGUID()), equipmentPolicy(oracleVocationId), - inventoryPolicy(itemSellValues, [this](const Player& candidatePlayer, const Item& item) { + inventoryPolicy(economyCatalog.sellValues(), [this](const Player& candidatePlayer, const Item& item) { return equipmentPolicy.evaluateUpgrade(candidatePlayer, item).has_value(); }), huntRegionCooldowns(sharedHuntRegionCooldowns) {} @@ -143,8 +143,7 @@ Item* PlayerBotController::findActionableSlottedItem(const Player& player, uint1 if (!item || !inventoryPolicy.isActionableSlottedItem(player, *item, candidateSlot, itemId)) { continue; } - auto suppressed = unavailableSlottedSales.find({item->getID(), candidateSlot}); - if (suppressed != unavailableSlottedSales.end() && suppressed->second > now) { + if (serviceWorkflow.slottedSaleUnavailable(item->getID(), candidateSlot, now)) { continue; } slot = candidateSlot; diff --git a/server/src/playerbotcontroller.h b/server/src/playerbotcontroller.h index 38a9514..2c322f2 100644 --- a/server/src/playerbotcontroller.h +++ b/server/src/playerbotcontroller.h @@ -15,7 +15,8 @@ #include "playerbot.h" #include "playerbotcombatruntime.h" -#include "playerbotdepotsession.h" +#include "playerbotdepotworkflow.h" +#include "playerboteconomy.h" #include "playerbotequipmentpolicy.h" #include "playerbotgoalarbiter.h" #include "playerbothuntregions.h" @@ -24,12 +25,11 @@ #include "playerbotinventorypolicy.h" #include "playerbotlootworkflow.h" #include "playerbotnavigationruntime.h" -#include "playerbotnpcsession.h" #include "playerbotprogressionsession.h" #include "playerbotsurvivalruntime.h" #include "playerbottestpolicy.h" #include "playerbottelemetry.h" -#include "playerbotservicesession.h" +#include "playerbotserviceworkflow.h" #include "container.h" #include "condition.h" @@ -176,16 +176,9 @@ class PlayerBotController : public std::enable_shared_from_this& services, const Position& position); + const ServiceNpc* findNearestService(const std::vector& services, const Position& position) const; - ServiceNpc* findShopFor(uint16_t itemId, bool buying, const Position& position); + const ServiceNpc* findShopFor(uint16_t itemId, bool buying, const Position& position) const; - ServiceNpc* findLootSeller(Player* player, const Position& position, uint16_t& itemId); + const ServiceNpc* findLootSeller(Player* player, const Position& position, uint16_t& itemId) const; bool prepareSlottedSaleItem(Player* player, uint16_t itemId, const Position& position); void completeServiceAction(Player* player, const char* action, const PlayerBotServiceTransaction& transaction, const Position& position); - void processServiceShop(Player* player, const Position& currentPosition, ServiceNpc& service, const char* action, + void processServiceShop(Player* player, const Position& currentPosition, const ServiceNpc& service, const char* action, uint16_t itemId, uint32_t amount, bool purchase); - void processBank(Player* player, const Position& currentPosition, ServiceNpc& banker); + void processBank(Player* player, const Position& currentPosition, const ServiceNpc& banker); void processService(Player* player, const Position& currentPosition); @@ -553,11 +543,12 @@ class PlayerBotController : public std::enable_shared_from_this itemSellValues; + PlayerBotEconomyCatalog economyCatalog; + PlayerBotDispositionPolicy dispositionPolicy; PlayerBotEquipmentPolicy equipmentPolicy; playerbot::PlayerBotInventoryPolicy inventoryPolicy; PlayerBotSurvivalRuntime survivalRuntime; - PlayerBotDepotSession depotSession; + PlayerBotDepotWorkflow depotWorkflow; PlayerBotCombatRuntime combatRuntime{PlayerBotCombatRuntimeConfig{ playerbot::traversalCombatTimeout, playerbot::traversalTargetSuppression, playerbot::lostTargetPursuitTimeout, playerbot::lostTargetSuppression, @@ -569,7 +560,6 @@ class PlayerBotController : public std::enable_shared_from_this serviceShops; - std::vector serviceBankers; - Position serviceApproachTarget; - std::set serviceRejectedApproaches; - uint16_t pendingSlottedSaleItemId = 0; - slots_t pendingSlottedSaleSourceSlot = CONST_SLOT_WHEREEVER; - uint32_t pendingSlottedSaleBackpackCount = 0; - uint32_t slottedSaleMoveAttempts = 0; - std::map, std::chrono::steady_clock::time_point> unavailableSlottedSales; - PlayerBotNpcSession npcSession; - PlayerBotServiceSession serviceSession; + PlayerBotServiceWorkflow serviceWorkflow; size_t huntRouteIndex = 0; uint32_t completedCycles = 0; std::chrono::steady_clock::time_point huntDeadline; @@ -609,4 +589,5 @@ class PlayerBotController : public std::enable_shared_from_this route; @@ -153,7 +153,7 @@ void PlayerBotController::beginOracleDeparture(Player& player, const Position& p departureSession.begin(std::move(plan)); progressionSession.begin(PlayerBotProgressionProcedure::OracleDeparture); const auto& departure = departureSession.plan(); - npcSession.reset(departure.npcId); + serviceWorkflow.resetNpc(departure.npcId); navigationRuntime.adopt(departure.approachPosition, std::move(steps)); emit("strategy_selection", position, "\"goal\":\"oracle_departure\",\"npc_id\":" + std::to_string(departure.npcId) + @@ -171,7 +171,7 @@ void PlayerBotController::finishOracleDeparture(Player* player, const Position& ",\"reason\":" + jsonString(reason)); progressionSession.reset(); departureSession.reset(); - npcSession.reset(); + serviceWorkflow.resetNpc(); clearNavigation(); if (std::strcmp(result, "success") == 0) { if (player) { @@ -226,7 +226,7 @@ void PlayerBotController::processOracleDeparture(Player* player, const Position& } if (departureSession.stage() == PlayerBotOracleDepartureStage::Greet) { - npcSession.resetGreetingAcknowledgement(); + serviceWorkflow.resetGreetingAcknowledgement(); telemetry.recordActionAttempt(); oracle->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "hi"); departureSession.setStage(PlayerBotOracleDepartureStage::ConfirmReady); @@ -234,7 +234,7 @@ void PlayerBotController::processOracleDeparture(Player* player, const Position& return; } if (departureSession.stage() == PlayerBotOracleDepartureStage::ConfirmReady) { - if (!npcSession.isGreetingAcknowledged()) { + if (!serviceWorkflow.isGreetingAcknowledged()) { if (departureSession.incrementRetries() >= maximumProgressionAttempts) { finishOracleDeparture(player, currentPosition, "failed", "oracle_focus_unconfirmed"); return; diff --git a/server/src/playerbotdepotsession.h b/server/src/playerbotdepotsession.h index d38fc34..a38289e 100644 --- a/server/src/playerbotdepotsession.h +++ b/server/src/playerbotdepotsession.h @@ -79,16 +79,11 @@ class PlayerBotDepotSession bool candidatesPrepared() const { return candidatesReady; } void prepareCandidates(const Position& anchor); const Position& discoveryAnchor() const { return anchorPosition; } - std::vector& candidates() { return depotCandidates; } const std::vector& candidates() const { return depotCandidates; } size_t nextCandidate() const { return nextCandidateIndex; } void advanceCandidate() { ++nextCandidateIndex; } void resetCandidates(); - uint32_t& indexedCandidateCount() { return indexedCandidates; } - uint32_t& inScopeCandidateCount() { return inScopeCandidates; } - uint32_t& standableCandidateCount() { return standableCandidates; } - uint32_t& suppressedApproachCount() { return suppressedApproaches; } uint32_t indexedCandidateCount() const { return indexedCandidates; } uint32_t inScopeCandidateCount() const { return inScopeCandidates; } uint32_t standableCandidateCount() const { return standableCandidates; } diff --git a/server/src/playerbotdepotworkflow.cpp b/server/src/playerbotdepotworkflow.cpp new file mode 100644 index 0000000..b32ea44 --- /dev/null +++ b/server/src/playerbotdepotworkflow.cpp @@ -0,0 +1,66 @@ +#include "otpch.h" + +#include "playerbotdepotworkflow.h" + +void PlayerBotDepotWorkflow::reset() +{ + session.reset(); + discoveryCandidates.clear(); + nextCandidateOffset = 0; + indexedCandidates = 0; + inScopeCandidates = 0; + standableCandidates = 0; + suppressedApproaches = 0; +} + +void PlayerBotDepotWorkflow::clearDiscovery() +{ + session.clearDiscovery(); + discoveryCandidates.clear(); + nextCandidateOffset = 0; + indexedCandidates = 0; + inScopeCandidates = 0; + standableCandidates = 0; + suppressedApproaches = 0; +} + +void PlayerBotDepotWorkflow::recordCandidate(PlayerBotDepotCandidate candidate) +{ + discoveryCandidates.push_back(candidate); +} + +void PlayerBotDepotWorkflow::sortCandidates() +{ + std::sort(discoveryCandidates.begin(), discoveryCandidates.end(), [](const auto& left, const auto& right) { + return left.distance != right.distance ? left.distance < right.distance : + left.depotId != right.depotId ? left.depotId < right.depotId : + left.lockerPosition != right.lockerPosition ? left.lockerPosition < right.lockerPosition : + left.approachPosition < right.approachPosition; + }); +} + +bool PlayerBotDepotWorkflow::hasNextCandidate() const +{ + return nextCandidateOffset < discoveryCandidates.size(); +} + +std::optional PlayerBotDepotWorkflow::takeNextCandidate() +{ + if (!hasNextCandidate()) { + return std::nullopt; + } + return discoveryCandidates[nextCandidateOffset++]; +} + +std::optional PlayerBotDepotWorkflow::earliestRejectedApproachExpiry() const +{ + const auto& rejected = session.rejectedApproaches(); + if (rejected.empty()) { + return std::nullopt; + } + auto earliest = rejected.begin()->second; + for (const auto& entry : rejected) { + earliest = std::min(earliest, entry.second); + } + return earliest; +} diff --git a/server/src/playerbotdepotworkflow.h b/server/src/playerbotdepotworkflow.h new file mode 100644 index 0000000..e59e248 --- /dev/null +++ b/server/src/playerbotdepotworkflow.h @@ -0,0 +1,70 @@ +/** Depot state machine over a DepotSession. World inspection and dispatch remain external. */ +#ifndef FS_PLAYERBOTDEPOTWORKFLOW_H +#define FS_PLAYERBOTDEPOTWORKFLOW_H + +#include "playerbotdepotsession.h" + +#include + +enum class PlayerBotDepotCommandType : uint8_t { None, Approach, OpenLocker, OpenChest, Deposit, Depart }; +enum class PlayerBotDepotOutcome : uint8_t { Pending, Ready, Retry, Moved, Partial, Deferred, Rejected, Unavailable }; + +class PlayerBotDepotWorkflow +{ + public: + void reset(); + void clearDiscovery(); + PlayerBotDepotStage stage() const { return session.stage(); } + void setStage(PlayerBotDepotStage stage) { session.setStage(stage); } + uint32_t attempts() const { return session.attempts(); } + uint32_t incrementAttempts() { return session.incrementAttempts(); } + void resetAttempts() { session.resetAttempts(); } + uint16_t depotId() const { return session.depotId(); } + uint16_t lockerItemId() const { return session.lockerItemId(); } + const Position& lockerPosition() const { return session.lockerPosition(); } + const Position& approachPosition() const { return session.approachPosition(); } + bool hasSelectedDepot() const { return session.hasSelectedDepot(); } + void select(PlayerBotDepotCandidate candidate) { session.select(candidate); } + + bool candidatesPrepared() const { return session.candidatesPrepared(); } + void beginDiscovery(const Position& anchor) { session.prepareCandidates(anchor); } + const Position& discoveryAnchor() const { return session.discoveryAnchor(); } + void recordIndexedCandidate() { ++indexedCandidates; } + void recordInScopeCandidate() { ++inScopeCandidates; } + void recordStandableCandidate() { ++standableCandidates; } + void recordSuppressedApproach() { ++suppressedApproaches; } + void recordCandidate(PlayerBotDepotCandidate candidate); + void sortCandidates(); + bool hasCandidates() const { return !discoveryCandidates.empty(); } + bool hasNextCandidate() const; + std::optional takeNextCandidate(); + uint32_t indexedCandidateCount() const { return indexedCandidates; } + uint32_t inScopeCandidateCount() const { return inScopeCandidates; } + uint32_t standableCandidateCount() const { return standableCandidates; } + uint32_t suppressedApproachCount() const { return suppressedApproaches; } + + void expireRejectedApproaches(std::chrono::steady_clock::time_point now) { session.expireRejectedApproaches(now); } + bool isApproachRejected(const Position& position) const { return session.isApproachRejected(position); } + void rejectApproach(const Position& position, std::chrono::steady_clock::time_point expires) { session.rejectApproach(position, expires); } + std::optional earliestRejectedApproachExpiry() const; + + bool hasPendingMove() const { return session.hasPendingMove(); } + const PlayerBotDepotMove& move() const { return session.move(); } + void beginMove(PlayerBotDepotMove move) { session.beginMove(move); } + void clearMove() { session.clearMove(); } + PlayerBotDepotMoveVerification verifyMove(uint32_t inventory, uint32_t destination, uint32_t maximumAttempts) + { + return session.verifyMove(inventory, destination, maximumAttempts); + } + + private: + PlayerBotDepotSession session; + std::vector discoveryCandidates; + size_t nextCandidateOffset = 0; + uint32_t indexedCandidates = 0; + uint32_t inScopeCandidates = 0; + uint32_t standableCandidates = 0; + uint32_t suppressedApproaches = 0; +}; + +#endif diff --git a/server/src/playerboteconomy.cpp b/server/src/playerboteconomy.cpp new file mode 100644 index 0000000..432458b --- /dev/null +++ b/server/src/playerboteconomy.cpp @@ -0,0 +1,101 @@ +#include "otpch.h" + +#include "playerboteconomy.h" + +namespace { + uint32_t providerDistance(const Position& from, const Position& to) + { + return std::max(Position::getDistanceX(from, to), Position::getDistanceY(from, to)) + + (from.z == to.z ? 0 : 32 * Position::getDistanceZ(from, to)); + } +} + +void PlayerBotEconomyCatalog::learn(const std::vector& providers) +{ + for (const PlayerBotEconomyProvider& provider : providers) { + for (const PlayerBotEconomyOffer& offer : provider.offers) { + if (offer.sellPrice != 0) { + learnedSellValues[offer.itemId] = std::max(learnedSellValues[offer.itemId], offer.sellPrice); + } + } + } +} + +uint32_t PlayerBotEconomyCatalog::sellValue(uint16_t itemId) const +{ + auto it = learnedSellValues.find(itemId); + return it == learnedSellValues.end() ? 0 : it->second; +} + +const PlayerBotEconomyProvider* PlayerBotEconomyCatalog::rankedProvider(const std::vector& providers, + uint16_t itemId, bool purchase, const Position& from) const +{ + const PlayerBotEconomyProvider* selected = nullptr; + uint32_t selectedPrice = 0; + for (const PlayerBotEconomyProvider& provider : providers) { + auto offer = std::find_if(provider.offers.begin(), provider.offers.end(), [itemId, purchase](const auto& candidate) { + return candidate.itemId == itemId && (purchase ? candidate.buyPrice != 0 : candidate.sellPrice != 0); + }); + if (offer == provider.offers.end()) { + continue; + } + const uint32_t price = purchase ? offer->buyPrice : offer->sellPrice; + if (!selected || (!purchase && price > selectedPrice) || + (!purchase && price == selectedPrice && providerDistance(from, provider.position) < providerDistance(from, selected->position)) || + (!purchase && price == selectedPrice && providerDistance(from, provider.position) == providerDistance(from, selected->position) && provider.id < selected->id) || + (purchase && providerDistance(from, provider.position) < providerDistance(from, selected->position)) || + (purchase && providerDistance(from, provider.position) == providerDistance(from, selected->position) && provider.id < selected->id)) { + selected = &provider; + selectedPrice = price; + } + } + return selected; +} + +uint32_t PlayerBotDispositionPolicy::protectedReserve(uint16_t itemId, bool food) const +{ + if (itemId == 2120 || itemId == 2554) { + return 1; + } + if (food) { + return 2; + } + return itemId == smallHealthPotionItemId ? potionRestockTarget : 0; +} + +uint32_t PlayerBotDispositionPolicy::sellQuantity(const PlayerBotEconomyInventorySnapshot& inventory, uint16_t reserve) const +{ + return inventory.itemCount > reserve ? inventory.itemCount - reserve : 0; +} + +PlayerBotEconomyRestockDecision PlayerBotDispositionPolicy::restock(const PlayerBotEconomyInventorySnapshot& inventory, + uint32_t unitPrice, uint32_t unitWeight) const +{ + if (inventory.itemCount >= potionRestockTarget || unitPrice == 0) { + return {}; + } + const uint32_t targetGap = potionRestockTarget - inventory.itemCount; + const uint64_t totalMoney = inventory.money + inventory.bankBalance; + const uint32_t requiredGap = inventory.itemCount <= potionReturnThreshold ? potionReturnThreshold + 1 - inventory.itemCount : 0; + if (totalMoney / unitPrice < requiredGap) { + return {0, true}; + } + uint32_t amount = totalMoney / unitPrice >= targetGap ? targetGap : static_cast(std::min( + targetGap, totalMoney > carriedGoldReserve ? (totalMoney - carriedGoldReserve) / unitPrice : 0)); + if (inventory.itemCount <= potionReturnThreshold) { + amount = std::max(amount, static_cast(std::min(requiredGap, totalMoney / unitPrice))); + } + if (unitWeight != 0) { + amount = std::min(amount, inventory.freeCapacity / unitWeight); + } + return {amount, false}; +} + +uint32_t PlayerBotDispositionPolicy::bankWithdrawal(const PlayerBotEconomyInventorySnapshot& inventory, uint32_t coinWeight) const +{ + uint32_t amount = static_cast(std::min(carriedGoldReserve, inventory.bankBalance)); + if (coinWeight != 0) { + amount = std::min(amount, inventory.freeCapacity / coinWeight); + } + return amount; +} diff --git a/server/src/playerboteconomy.h b/server/src/playerboteconomy.h new file mode 100644 index 0000000..5e1e38f --- /dev/null +++ b/server/src/playerboteconomy.h @@ -0,0 +1,64 @@ +/** Economy knowledge and pure disposition decisions for one playerbot. */ +#ifndef FS_PLAYERBOTECONOMY_H +#define FS_PLAYERBOTECONOMY_H + +#include +#include +#include + +#include "position.h" + +struct PlayerBotEconomyOffer { + uint16_t itemId = 0; + uint32_t buyPrice = 0; + uint32_t sellPrice = 0; + uint8_t subType = 0; +}; + +struct PlayerBotEconomyProvider { + uint32_t id = 0; + Position position; + std::vector offers; +}; + +struct PlayerBotEconomyInventorySnapshot { + uint32_t itemCount = 0; + uint32_t freeCapacity = 0; + uint64_t money = 0; + uint64_t bankBalance = 0; +}; + +struct PlayerBotEconomyRestockDecision { + uint32_t amount = 0; + bool insufficientFunds = false; +}; + +class PlayerBotEconomyCatalog +{ + public: + void learn(const std::vector& providers); + uint32_t sellValue(uint16_t itemId) const; + const PlayerBotEconomyProvider* rankedProvider(const std::vector& providers, + uint16_t itemId, bool purchase, const Position& from) const; + const std::map& sellValues() const { return learnedSellValues; } + + private: + std::map learnedSellValues; +}; + +class PlayerBotDispositionPolicy +{ + public: + static constexpr uint16_t smallHealthPotionItemId = 8704; + static constexpr uint32_t potionReturnThreshold = 1; + static constexpr uint32_t potionRestockTarget = 10; + static constexpr uint32_t carriedGoldReserve = 100; + + uint32_t protectedReserve(uint16_t itemId, bool food) const; + uint32_t sellQuantity(const PlayerBotEconomyInventorySnapshot& inventory, uint16_t reserve) const; + PlayerBotEconomyRestockDecision restock(const PlayerBotEconomyInventorySnapshot& inventory, + uint32_t unitPrice, uint32_t unitWeight) const; + uint32_t bankWithdrawal(const PlayerBotEconomyInventorySnapshot& inventory, uint32_t coinWeight) const; +}; + +#endif diff --git a/server/src/playerbotequipment.cpp b/server/src/playerbotequipment.cpp index 0d046d1..122dba6 100644 --- a/server/src/playerbotequipment.cpp +++ b/server/src/playerbotequipment.cpp @@ -301,11 +301,11 @@ void PlayerBotController::beginEquipmentPurchase(Player& player, const Position& equipmentPurchaseSession.begin(std::move(evaluation)); progressionSession.begin(PlayerBotProgressionProcedure::BuyEquipment); const auto& purchase = equipmentPurchaseSession.plan(); - serviceSession.reset(); + serviceWorkflow.resetTransactions(); if (!purchase.carried) { - npcSession.reset(purchase.npcId); - serviceApproachTarget = Position(); - serviceRejectedApproaches.clear(); + serviceWorkflow.resetNpc(purchase.npcId); + serviceWorkflow.clearApproach(); + serviceWorkflow.clearRejectedApproaches(); clearNavigation(); } std::ostringstream fields; @@ -348,8 +348,8 @@ void PlayerBotController::finishEquipmentPurchase(Player* player, const Position succeeded ? equipmentPurchaseSuccessCooldown : equipmentPurchaseFailureCooldown); progressionSession.reset(); equipmentPurchaseSession.reset(); - npcSession.reset(); - serviceSession.reset(); + serviceWorkflow.resetNpc(); + serviceWorkflow.resetTransactions(); clearNavigation(); cyclePhase = CyclePhase::Service; if (succeeded && fixtureRuntime.equipmentPurchaseFixture()) { @@ -406,15 +406,17 @@ void PlayerBotController::processEquipmentPurchase(Player* player, const Positio finishEquipmentPurchase(player, position, "failed", "provider_moved"); return; } - const PlayerBotNpcSessionResult sessionResult = npcSession.openShop(*player, *npc, telemetry.actionsAttemptedForSession(), - maximumServiceAttempts); - if (sessionResult != PlayerBotNpcSessionResult::Ready) { - if (sessionResult == PlayerBotNpcSessionResult::Failed) { - logActionFailure("shop", npcSession.step() == PlayerBotNpcConversationStep::Request ? + const PlayerBotNpcSessionOutcome sessionOutcome = serviceWorkflow.openNpcShop(*player, *npc, maximumServiceAttempts); + for (uint8_t action = 0; action < sessionOutcome.actionsIssued; ++action) { + telemetry.recordActionAttempt(); + } + if (sessionOutcome.result != PlayerBotNpcSessionResult::Ready) { + if (sessionOutcome.result == PlayerBotNpcSessionResult::Failed) { + logActionFailure("shop", serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Request ? "npc_focus_unconfirmed" : "shop_window_unavailable", position); finishEquipmentPurchase(player, position, "failed", "shop_window_unavailable"); } else { - schedule(npcSession.nextDelay() == 0 ? SCHEDULER_MINTICKS : npcSession.nextDelay()); + schedule(serviceWorkflow.npcNextDelay() == 0 ? SCHEDULER_MINTICKS : serviceWorkflow.npcNextDelay()); } return; } @@ -425,8 +427,8 @@ void PlayerBotController::processEquipmentPurchase(Player* player, const Positio finishEquipmentPurchase(player, position, "failed", "reserve_changed"); return; } - if (!serviceSession.hasShopTransaction()) { - serviceSession.beginShopTransaction({purchase.itemId, 1, + if (!serviceWorkflow.hasShopTransaction()) { + serviceWorkflow.beginShopTransaction({purchase.itemId, 1, inventoryPolicy.inventoryItemCount(*player, purchase.itemId), player->getMoney(), player->getBankBalance()}); } @@ -441,7 +443,7 @@ void PlayerBotController::processEquipmentPurchase(Player* player, const Positio } if (equipmentPurchaseSession.stage() == PlayerBotEquipmentPurchaseStage::VerifyPurchase) { - const PlayerBotServiceVerification verification = serviceSession.verifyShopTransaction( + const PlayerBotServiceVerification verification = serviceWorkflow.verifyShopTransaction( inventoryPolicy.inventoryItemCount(*player, purchase.itemId), player->getMoney(), player->getBankBalance(), true, purchase.price, maximumProgressionAttempts); if (verification.result == PlayerBotServiceVerificationResult::Success) { @@ -469,7 +471,7 @@ void PlayerBotController::processEquipmentPurchase(Player* player, const Positio } equipmentPurchaseSession.incrementRetries(); equipmentPurchaseSession.setStage(PlayerBotEquipmentPurchaseStage::Purchase); - npcSession.setStep(PlayerBotNpcConversationStep::Ready); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Ready); schedule(navigationDecisionDelay(*player)); return; } diff --git a/server/src/playerbotnpcsession.cpp b/server/src/playerbotnpcsession.cpp index 1646235..bae5d13 100644 --- a/server/src/playerbotnpcsession.cpp +++ b/server/src/playerbotnpcsession.cpp @@ -42,29 +42,26 @@ bool PlayerBotNpcSession::retryLimitReached(uint32_t maximumRetries) return ++retries >= maximumRetries; } -PlayerBotNpcSessionResult PlayerBotNpcSession::establishFocus(Player& player, Npc& npc, uint64_t& actionsAttempted, - uint32_t maximumRetries) +PlayerBotNpcSessionOutcome PlayerBotNpcSession::establishFocus(Player& player, Npc& npc, uint32_t maximumRetries) { if (conversationStep == PlayerBotNpcConversationStep::Greet) { - ++actionsAttempted; npc.receiveSpeech(&player, TALKTYPE_PRIVATE_PN, "hi"); conversationStep = PlayerBotNpcConversationStep::Request; pendingDelay = npcReplyDelay; - return PlayerBotNpcSessionResult::Pending; + return {PlayerBotNpcSessionResult::Pending, 1}; } if (conversationStep != PlayerBotNpcConversationStep::Request || greetingAcknowledged) { - return PlayerBotNpcSessionResult::Ready; + return {PlayerBotNpcSessionResult::Ready, 0}; } if (retryLimitReached(maximumRetries)) { - return PlayerBotNpcSessionResult::Failed; + return {PlayerBotNpcSessionResult::Failed, 0}; } conversationStep = PlayerBotNpcConversationStep::Greet; pendingDelay = npcReplyDelay; - return PlayerBotNpcSessionResult::Pending; + return {PlayerBotNpcSessionResult::Pending, 0}; } -PlayerBotNpcSessionResult PlayerBotNpcSession::openShop(Player& player, Npc& npc, uint64_t& actionsAttempted, - uint32_t maximumRetries) +PlayerBotNpcSessionOutcome PlayerBotNpcSession::openShop(Player& player, Npc& npc, uint32_t maximumRetries) { int32_t onBuy; int32_t onSell; @@ -74,7 +71,7 @@ PlayerBotNpcSessionResult PlayerBotNpcSession::openShop(Player& player, Npc& npc conversationStep = PlayerBotNpcConversationStep::Ready; retries = 0; } - return PlayerBotNpcSessionResult::Ready; + return {PlayerBotNpcSessionResult::Ready, 0}; } if (shopOwner && shopOwner != &npc && conversationStep == PlayerBotNpcConversationStep::Greet) { player.closeShopWindow(false); @@ -82,21 +79,20 @@ PlayerBotNpcSessionResult PlayerBotNpcSession::openShop(Player& player, Npc& npc if (conversationStep == PlayerBotNpcConversationStep::Greet || conversationStep == PlayerBotNpcConversationStep::Request) { - const PlayerBotNpcSessionResult focus = establishFocus(player, npc, actionsAttempted, maximumRetries); - if (focus != PlayerBotNpcSessionResult::Ready) { + const PlayerBotNpcSessionOutcome focus = establishFocus(player, npc, maximumRetries); + if (focus.result != PlayerBotNpcSessionResult::Ready) { return focus; } - ++actionsAttempted; npc.receiveSpeech(&player, TALKTYPE_PRIVATE_PN, "trade"); conversationStep = PlayerBotNpcConversationStep::Ready; pendingDelay = npcReplyDelay; - return PlayerBotNpcSessionResult::Pending; + return {PlayerBotNpcSessionResult::Pending, 1}; } if (retryLimitReached(maximumRetries)) { - return PlayerBotNpcSessionResult::Failed; + return {PlayerBotNpcSessionResult::Failed, 0}; } conversationStep = PlayerBotNpcConversationStep::Greet; pendingDelay = 0; - return PlayerBotNpcSessionResult::Pending; + return {PlayerBotNpcSessionResult::Pending, 0}; } diff --git a/server/src/playerbotnpcsession.h b/server/src/playerbotnpcsession.h index f7d0b96..13e65e6 100644 --- a/server/src/playerbotnpcsession.h +++ b/server/src/playerbotnpcsession.h @@ -22,6 +22,11 @@ enum class PlayerBotNpcSessionResult : uint8_t { Failed, }; +struct PlayerBotNpcSessionOutcome { + PlayerBotNpcSessionResult result = PlayerBotNpcSessionResult::Pending; + uint8_t actionsIssued = 0; +}; + enum class PlayerBotNpcConversationStep : uint8_t { Greet, Request, @@ -47,10 +52,8 @@ class PlayerBotNpcSession void resetRetries() { retries = 0; } bool retryLimitReached(uint32_t maximumRetries); - PlayerBotNpcSessionResult establishFocus(Player& player, Npc& npc, uint64_t& actionsAttempted, - uint32_t maximumRetries); - PlayerBotNpcSessionResult openShop(Player& player, Npc& npc, uint64_t& actionsAttempted, - uint32_t maximumRetries); + PlayerBotNpcSessionOutcome establishFocus(Player& player, Npc& npc, uint32_t maximumRetries); + PlayerBotNpcSessionOutcome openShop(Player& player, Npc& npc, uint32_t maximumRetries); uint32_t nextDelay() const { return pendingDelay; } private: diff --git a/server/src/playerbotprogression.cpp b/server/src/playerbotprogression.cpp index 63e6479..9959e0b 100644 --- a/server/src/playerbotprogression.cpp +++ b/server/src/playerbotprogression.cpp @@ -134,7 +134,7 @@ void PlayerBotController::processReadinessEquipment(Player* player, const Positi pendingReadinessAttempts = 0; if (readinessResumeService) { readinessResumeService = false; - depotSession.setStage(PlayerBotDepotStage::Deposit); + depotWorkflow.setStage(PlayerBotDepotStage::Deposit); schedule(SCHEDULER_MINTICKS); return; } @@ -322,12 +322,11 @@ void PlayerBotController::inspectRewardItem(Player& player, const Item& item, ui inspected.classes.emplace_back("tool"); inspection.shovelCount += item.getItemCount(); } - const auto sellIt = itemSellValues.find(item.getID()); + const uint32_t learnedSellValue = economyCatalog.sellValue(item.getID()); const ItemType& itemType = Item::items[item.getID()]; const bool unsupportedTwoHandedWeapon = (itemType.slotPosition & SLOTP_TWO_HAND) != 0 && itemType.weaponType != WEAPON_NONE; - const uint32_t sellPrice = inspected.worth == 0 && !unsupportedTwoHandedWeapon && - sellIt != itemSellValues.end() ? sellIt->second : 0; + const uint32_t sellPrice = inspected.worth == 0 && !unsupportedTwoHandedWeapon ? learnedSellValue : 0; if (sellPrice != 0) { inspected.classes.emplace_back("sellable"); inspected.sellValue = sellPrice * item.getItemCount(); @@ -979,7 +978,7 @@ bool PlayerBotController::findPickupReward(Player& player, const Position& posit uint32_t PlayerBotController::saleableItemCount(const Player& player) const { uint32_t count = 0; - for (const auto& [itemId, value] : itemSellValues) { + for (const auto& [itemId, value] : economyCatalog.sellValues()) { if (value != 0) { count += getSaleItemCount(player, itemId); } @@ -1228,8 +1227,8 @@ void PlayerBotController::finishProgressionObjective(Player* player, const Posit progressionSession.reset(); rewardSession.reset(); clearNavigation(); - serviceStage = ServiceStage::Discover; - npcSession.reset(); + serviceWorkflow.setStage(PlayerBotServiceStage::Discover); + serviceWorkflow.resetNpc(); cyclePhase = CyclePhase::Service; goalArbiter.setCooldown(TopLevelGoal::PickupReward, std::strcmp(result, "success") == 0 ? pickupRewardSuccessCooldown : pickupRewardFailureCooldown); diff --git a/server/src/playerbotservice.cpp b/server/src/playerbotservice.cpp index e2072ff..f5f26b9 100644 --- a/server/src/playerbotservice.cpp +++ b/server/src/playerbotservice.cpp @@ -59,7 +59,7 @@ void PlayerBotController::beginReturn(Player* player, const Position& position, clearTraversalTarget(position, reason); clearNavigation(); lootWorkflow.reset(); - depotSession.reset(); + depotWorkflow.reset(); fixedTargetRouteFailureCount = 0; player->closeContainer(corpseContainerId); setStage(ScenarioStage::Traverse, position); @@ -73,7 +73,7 @@ void PlayerBotController::beginReturn(Player* player, const Position& position, void PlayerBotController::onNpcReply(uint32_t replyingPlayerId, uint32_t npcId, uint8_t type, const std::string& text) { - if (!npcSession.acceptReply(playerId, replyingPlayerId, npcId, type)) { + if (!serviceWorkflow.acceptNpcReply(playerId, replyingPlayerId, npcId, type)) { return; } Npc* npc = g_game.getNpcByID(npcId); @@ -104,17 +104,9 @@ void PlayerBotController::beginService(Player* player, const Position& position, clearTraversalTarget(position, reason); clearNavigation(); lootWorkflow.reset(); - pendingSlottedSaleItemId = 0; - pendingSlottedSaleSourceSlot = CONST_SLOT_WHEREEVER; - slottedSaleMoveAttempts = 0; player->closeContainer(corpseContainerId); setStage(ScenarioStage::Traverse, position); - serviceShops.clear(); - serviceBankers.clear(); - serviceApproachTarget = Position(); - serviceStage = ServiceStage::Discover; - npcSession.reset(); - serviceSession.reset(); + serviceWorkflow.reset(); setCyclePhase(CyclePhase::Service, position, reason); } @@ -137,7 +129,8 @@ void PlayerBotController::finishHuntAndSelectGoal(Player* player, const Position void PlayerBotController::discoverServices(const Position& position) { - refreshItemValues(); + std::vector shops; + std::vector bankers; Player* player = g_game.getPlayerByID(playerId); for (const auto& entry : g_game.getNpcs()) { Npc* npc = entry.second; @@ -146,46 +139,52 @@ void PlayerBotController::discoverServices(const Position& position) serviceDistance(player->getTemplePosition(), {npc->getID(), npc->getPosition()}) > maximumServiceDistanceFromTemple) { continue; } - std::vector* services = *capability == "shop" ? &serviceShops : - (*capability == "banker" ? &serviceBankers : nullptr); + std::vector* services = *capability == "shop" ? &shops : + (*capability == "banker" ? &bankers : nullptr); if (!services) { continue; } - services->push_back({npc->getID(), npc->getPosition()}); + ServiceNpc provider{npc->getID(), npc->getPosition()}; + for (const ShopInfo& offer : npc->getShopOffers()) { + const ItemType& type = Item::items[offer.itemId]; + if (!type.isFluidContainer() && !type.isSplash()) { + provider.offers.push_back({offer.itemId, offer.buyPrice, offer.sellPrice, static_cast(offer.subType)}); + } + } + services->push_back(std::move(provider)); emit("service_discovered", position, "\"capability\":" + jsonString(*capability) + ",\"npc_id\":" + std::to_string(npc->getID()) + ",\"npc_name\":" + jsonString(npc->getName()) + ",\"offers\":" + std::to_string(npc->getShopOffers().size())); } - if (serviceShops.empty() || serviceBankers.empty()) { + if (shops.empty() || bankers.empty()) { stop("service_npc_unavailable", position); return; } - std::sort(serviceShops.begin(), serviceShops.end(), [](const ServiceNpc& left, const ServiceNpc& right) { - return left.id < right.id; - }); - serviceStage = ServiceStage::SellLoot; + serviceWorkflow.setProviders(std::move(shops), std::move(bankers)); + economyCatalog.learn(serviceWorkflow.shops()); + serviceWorkflow.setStage(PlayerBotServiceStage::SellLoot); } -bool PlayerBotController::approachServiceNpc(Player* player, ServiceNpc& service, const Position& currentPosition) +bool PlayerBotController::approachServiceNpc(Player* player, const ServiceNpc& service, const Position& currentPosition) { Npc* npc = g_game.getNpcByID(service.id); if (!npc || npc->isRemoved()) { stop("service_npc_unavailable", currentPosition); return false; } - service.position = npc->getPosition(); - if (Position::areInRange<3, 3, 0>(currentPosition, service.position)) { - serviceApproachTarget = Position(); + const Position servicePosition = npc->getPosition(); + if (Position::areInRange<3, 3, 0>(currentPosition, servicePosition)) { + serviceWorkflow.clearApproach(); return true; } - if (serviceApproachTarget != Position()) { - if (currentPosition == serviceApproachTarget) { - serviceApproachTarget = Position(); + if (serviceWorkflow.approachTarget() != Position()) { + if (currentPosition == serviceWorkflow.approachTarget()) { + serviceWorkflow.clearApproach(); clearNavigation(); schedule(SCHEDULER_MINTICKS); return false; } - return processNavigation(player, currentPosition, serviceApproachTarget); + return processNavigation(player, currentPosition, serviceWorkflow.approachTarget()); } std::vector candidates; @@ -193,7 +192,7 @@ bool PlayerBotController::approachServiceNpc(Player* player, ServiceNpc& service for (int32_t xOffset = -3; xOffset <= 3; ++xOffset) { for (int32_t yOffset = -3; yOffset <= 3; ++yOffset) { if (xOffset != 0 || yOffset != 0) { - candidates.emplace_back(service.position.x + xOffset, service.position.y + yOffset, service.position.z); + candidates.emplace_back(servicePosition.x + xOffset, servicePosition.y + yOffset, servicePosition.z); } } } @@ -203,7 +202,7 @@ bool PlayerBotController::approachServiceNpc(Player* player, ServiceNpc& service return leftDistance == rightDistance ? left < right : leftDistance < rightDistance; }); for (const Position& candidate : candidates) { - if (serviceRejectedApproaches.find(candidate) != serviceRejectedApproaches.end()) { + if (serviceWorkflow.isApproachRejected(candidate)) { continue; } Tile* tile = g_game.map.getTile(candidate); @@ -220,11 +219,11 @@ bool PlayerBotController::approachServiceNpc(Player* player, ServiceNpc& service telemetry.recordPathfinding(std::chrono::duration_cast( std::chrono::steady_clock::now() - startedAt), planned && !candidateSteps.empty()); if (!planned || candidateSteps.empty()) { - serviceRejectedApproaches.insert(candidate); + serviceWorkflow.rejectApproach(candidate); schedule(SCHEDULER_MINTICKS); return false; } - serviceApproachTarget = candidate; + serviceWorkflow.setApproachTarget(candidate); navigationRuntime.adopt(candidate, std::move(candidateSteps)); std::ostringstream fields; fields << "\"action\":\"plan\",\"result\":\"success\",\"steps\":" << navigationRuntime.routeSize() @@ -239,19 +238,23 @@ bool PlayerBotController::approachServiceNpc(Player* player, ServiceNpc& service void PlayerBotController::refreshItemValues() { + std::vector providers; for (const auto& entry : g_game.getNpcs()) { Npc* npc = entry.second; const std::string* capability = npc && !npc->isRemoved() ? npc->getParameter("playerbot_service") : nullptr; if (!capability || *capability != "shop") { continue; } + PlayerBotEconomyProvider provider{npc->getID(), npc->getPosition()}; for (const ShopInfo& offer : npc->getShopOffers()) { const ItemType& type = Item::items[offer.itemId]; if (offer.sellPrice != 0 && !type.isFluidContainer() && !type.isSplash()) { - itemSellValues[offer.itemId] = std::max(itemSellValues[offer.itemId], offer.sellPrice); + provider.offers.push_back({offer.itemId, offer.buyPrice, offer.sellPrice, static_cast(offer.subType)}); } } + providers.push_back(std::move(provider)); } + economyCatalog.learn(providers); } const ShopInfo* PlayerBotController::findOffer(const ServiceNpc& service, uint16_t itemId, bool buying) const @@ -276,7 +279,7 @@ uint32_t PlayerBotController::serviceDistance(const Position& from, const Servic (from.z == service.position.z ? 0 : 32 * Position::getDistanceZ(from, service.position)); } -PlayerBotController::ServiceNpc* PlayerBotController::findNearestService(std::vector& services, const Position& position) +const PlayerBotController::ServiceNpc* PlayerBotController::findNearestService(const std::vector& services, const Position& position) const { auto it = std::min_element(services.begin(), services.end(), [this, &position](const ServiceNpc& left, const ServiceNpc& right) { return serviceDistance(position, left) < serviceDistance(position, right); @@ -284,22 +287,16 @@ PlayerBotController::ServiceNpc* PlayerBotController::findNearestService(std::ve return it == services.end() ? nullptr : &*it; } -PlayerBotController::ServiceNpc* PlayerBotController::findShopFor(uint16_t itemId, bool buying, const Position& position) +const PlayerBotController::ServiceNpc* PlayerBotController::findShopFor(uint16_t itemId, bool buying, const Position& position) const { - ServiceNpc* nearest = nullptr; - for (ServiceNpc& service : serviceShops) { - if (findOffer(service, itemId, buying) && (!nearest || serviceDistance(position, service) < serviceDistance(position, *nearest))) { - nearest = &service; - } - } - return nearest; + return serviceWorkflow.rankedProvider(economyCatalog, itemId, buying, position); } -PlayerBotController::ServiceNpc* PlayerBotController::findLootSeller(Player* player, const Position& position, uint16_t& itemId) +const PlayerBotController::ServiceNpc* PlayerBotController::findLootSeller(Player* player, const Position& position, uint16_t& itemId) const { - ServiceNpc* nearest = nullptr; + const ServiceNpc* nearest = nullptr; uint32_t selectedSellPrice = 0; - for (ServiceNpc& service : serviceShops) { + for (const ServiceNpc& service : serviceWorkflow.shops()) { Npc* npc = g_game.getNpcByID(service.id); if (!npc || npc->isRemoved()) { continue; @@ -326,35 +323,29 @@ bool PlayerBotController::prepareSlottedSaleItem(Player* player, uint16_t itemId if (!backpack) { return false; } - if (pendingSlottedSaleItemId != 0) { - Item* sourceAfter = player->getInventoryItem(pendingSlottedSaleSourceSlot); - const uint32_t backpackAfter = backpack->getItemTypeCount(pendingSlottedSaleItemId); - if ((!sourceAfter || sourceAfter->getID() != pendingSlottedSaleItemId) && - backpackAfter > pendingSlottedSaleBackpackCount) { + if (const auto pending = serviceWorkflow.pendingSlottedSale()) { + Item* sourceAfter = player->getInventoryItem(pending->sourceSlot); + const uint32_t backpackAfter = backpack->getItemTypeCount(pending->itemId); + const bool moved = (!sourceAfter || sourceAfter->getID() != pending->itemId) && backpackAfter > pending->backpackCount; + const PlayerBotSlottedSaleObservation observation = serviceWorkflow.observeSlottedSale( + moved, maximumServiceAttempts, std::chrono::steady_clock::now(), unavailableDispositionCooldown); + if (observation == PlayerBotSlottedSaleObservation::Moved) { emit("action_result", position, "\"action\":\"item_disposition\",\"result\":\"success\",\"disposition\":\"sell\",\"item_id\":" + - std::to_string(pendingSlottedSaleItemId) + ",\"source_slot\":" + - std::to_string(pendingSlottedSaleSourceSlot) + ",\"provider_available\":true"); - pendingSlottedSaleItemId = 0; - pendingSlottedSaleSourceSlot = CONST_SLOT_WHEREEVER; - slottedSaleMoveAttempts = 0; + std::to_string(pending->itemId) + ",\"source_slot\":" + + std::to_string(pending->sourceSlot) + ",\"provider_available\":true"); schedule(SCHEDULER_MINTICKS); return true; } - const uint16_t failedItemId = pendingSlottedSaleItemId; - const slots_t failedSlot = pendingSlottedSaleSourceSlot; - pendingSlottedSaleItemId = 0; - pendingSlottedSaleSourceSlot = CONST_SLOT_WHEREEVER; - if (slottedSaleMoveAttempts >= maximumServiceAttempts) { - unavailableSlottedSales[{failedItemId, failedSlot}] = - std::chrono::steady_clock::now() + unavailableDispositionCooldown; + const uint16_t failedItemId = pending->itemId; + const slots_t failedSlot = pending->sourceSlot; + if (observation == PlayerBotSlottedSaleObservation::Deferred) { emit("action_result", position, "\"action\":\"item_disposition\",\"result\":\"deferred\",\"reason\":\"move_not_verified\",\"disposition\":\"sell\",\"item_id\":" + std::to_string(failedItemId) + ",\"source_slot\":" + std::to_string(failedSlot) + ",\"provider_available\":true,\"cooldown_ms\":" + std::to_string(std::chrono::duration_cast(unavailableDispositionCooldown).count())); - serviceStage = ServiceStage::BuyPotions; - slottedSaleMoveAttempts = 0; + serviceWorkflow.setStage(PlayerBotServiceStage::BuyPotions); schedule(SCHEDULER_MINTICKS); return true; } @@ -386,10 +377,7 @@ bool PlayerBotController::prepareSlottedSaleItem(Player* player, uint16_t itemId Position sourcePosition; uint8_t sourceIndex = 0; g_game.internalGetPosition(item, sourcePosition, sourceIndex); - pendingSlottedSaleItemId = itemId; - pendingSlottedSaleSourceSlot = sourceSlot; - pendingSlottedSaleBackpackCount = backpack->getItemTypeCount(itemId); - ++slottedSaleMoveAttempts; + serviceWorkflow.beginSlottedSale(itemId, sourceSlot, backpack->getItemTypeCount(itemId)); telemetry.recordActionAttempt(); g_game.playerMoveItem(player, sourcePosition, item->getClientID(), sourceIndex, Position(0xFFFF, 0x40 | static_cast(backpackId), @@ -398,7 +386,7 @@ bool PlayerBotController::prepareSlottedSaleItem(Player* player, uint16_t itemId emit("action_result", position, "\"action\":\"item_disposition\",\"result\":\"requested\",\"disposition\":\"sell\",\"item_id\":" + std::to_string(itemId) + ",\"source_slot\":" + std::to_string(sourceSlot) + - ",\"provider_available\":true,\"attempt\":" + std::to_string(slottedSaleMoveAttempts)); + ",\"provider_available\":true,\"attempt\":" + std::to_string(serviceWorkflow.slottedSaleAttempts())); schedule(navigationDecisionDelay(*player)); return true; } @@ -417,12 +405,12 @@ void PlayerBotController::completeServiceAction(Player* player, const char* acti say(*player, std::string(action) == "sell" ? "Sold " + std::to_string(transaction.amount) + " " + itemName + '.' : "Bought " + std::to_string(transaction.amount) + " " + itemName + '.'); - npcSession.setStep(PlayerBotNpcConversationStep::Ready); - npcSession.resetRetries(); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Ready); + serviceWorkflow.resetNpcRetries(); schedule(SCHEDULER_MINTICKS); } -void PlayerBotController::processServiceShop(Player* player, const Position& currentPosition, ServiceNpc& service, const char* action, +void PlayerBotController::processServiceShop(Player* player, const Position& currentPosition, const ServiceNpc& service, const char* action, uint16_t itemId, uint32_t amount, bool purchase) { if (!approachServiceNpc(player, service, currentPosition)) { @@ -433,15 +421,17 @@ void PlayerBotController::processServiceShop(Player* player, const Position& cur stop("service_npc_unavailable", currentPosition); return; } - const PlayerBotNpcSessionResult sessionResult = npcSession.openShop(*player, *npc, telemetry.actionsAttemptedForSession(), - maximumServiceAttempts); - if (sessionResult != PlayerBotNpcSessionResult::Ready) { - if (sessionResult == PlayerBotNpcSessionResult::Failed) { - logActionFailure("shop", npcSession.step() == PlayerBotNpcConversationStep::Request ? + const PlayerBotNpcSessionOutcome sessionOutcome = serviceWorkflow.openNpcShop(*player, *npc, maximumServiceAttempts); + for (uint8_t action = 0; action < sessionOutcome.actionsIssued; ++action) { + telemetry.recordActionAttempt(); + } + if (sessionOutcome.result != PlayerBotNpcSessionResult::Ready) { + if (sessionOutcome.result == PlayerBotNpcSessionResult::Failed) { + logActionFailure("shop", serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Request ? "npc_focus_unconfirmed" : "shop_window_unavailable", currentPosition); stop("shop_transaction_unavailable", currentPosition); } else { - schedule(npcSession.nextDelay() == 0 ? SCHEDULER_MINTICKS : npcSession.nextDelay()); + schedule(serviceWorkflow.npcNextDelay() == 0 ? SCHEDULER_MINTICKS : serviceWorkflow.npcNextDelay()); } return; } @@ -450,12 +440,12 @@ void PlayerBotController::processServiceShop(Player* player, const Position& cur stop("shop_offer_unavailable", currentPosition); return; } - if (npcSession.step() == PlayerBotNpcConversationStep::Ready) { - if (!serviceSession.hasShopTransaction()) { - serviceSession.beginShopTransaction({itemId, amount, inventoryPolicy.inventoryItemCount(*player, itemId), + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Ready) { + if (!serviceWorkflow.hasShopTransaction()) { + serviceWorkflow.beginShopTransaction({itemId, amount, inventoryPolicy.inventoryItemCount(*player, itemId), player->getMoney(), player->getBankBalance()}); } - npcSession.setStep(PlayerBotNpcConversationStep::Verify); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Verify); telemetry.recordActionAttempt(); if (purchase) { g_game.playerPurchaseItem(playerId, Item::items[itemId].clientId, static_cast(offer->subType), @@ -468,12 +458,12 @@ void PlayerBotController::processServiceShop(Player* player, const Position& cur return; } - const PlayerBotServiceTransaction* transaction = serviceSession.shopTransaction(); + const PlayerBotServiceTransaction* transaction = serviceWorkflow.shopTransaction(); if (!transaction) { stop("shop_transaction_missing", currentPosition); return; } - const PlayerBotServiceVerification verification = serviceSession.verifyShopTransaction( + const PlayerBotServiceVerification verification = serviceWorkflow.verifyShopTransaction( inventoryPolicy.inventoryItemCount(*player, transaction->itemId), player->getMoney(), player->getBankBalance(), purchase, purchase ? offer->buyPrice : offer->sellPrice, maximumServiceAttempts); if (verification.result == PlayerBotServiceVerificationResult::Success) { @@ -490,11 +480,11 @@ void PlayerBotController::processServiceShop(Player* player, const Position& cur stop("shop_transaction_not_verified", currentPosition); return; } - npcSession.setStep(PlayerBotNpcConversationStep::Ready); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Ready); schedule(navigationDecisionDelay(*player)); } -void PlayerBotController::processBank(Player* player, const Position& currentPosition, ServiceNpc& banker) +void PlayerBotController::processBank(Player* player, const Position& currentPosition, const ServiceNpc& banker) { if (!approachServiceNpc(player, banker, currentPosition)) { return; @@ -504,45 +494,47 @@ void PlayerBotController::processBank(Player* player, const Position& currentPos stop("banker_unavailable", currentPosition); return; } - if (npcSession.step() == PlayerBotNpcConversationStep::Greet || - npcSession.step() == PlayerBotNpcConversationStep::Request) { - const PlayerBotNpcSessionResult focus = npcSession.establishFocus(*player, *npc, telemetry.actionsAttemptedForSession(), - maximumServiceAttempts); - if (focus == PlayerBotNpcSessionResult::Failed) { + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Greet || + serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Request) { + const PlayerBotNpcSessionOutcome focus = serviceWorkflow.establishNpcFocus(*player, *npc, maximumServiceAttempts); + for (uint8_t action = 0; action < focus.actionsIssued; ++action) { + telemetry.recordActionAttempt(); + } + if (focus.result == PlayerBotNpcSessionResult::Failed) { logActionFailure("bank", "npc_focus_unconfirmed", currentPosition); stop("banker_focus_unconfirmed", currentPosition); return; } - if (focus == PlayerBotNpcSessionResult::Pending) { - schedule(npcSession.nextDelay()); + if (focus.result == PlayerBotNpcSessionResult::Pending) { + schedule(serviceWorkflow.npcNextDelay()); return; } } - if (npcSession.step() == PlayerBotNpcConversationStep::Request) { - if (!serviceSession.hasBankDeposit()) { - serviceSession.beginBankDeposit(player->getMoney(), player->getBankBalance()); + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Request) { + if (!serviceWorkflow.hasBankDeposit()) { + serviceWorkflow.beginBankDeposit(player->getMoney(), player->getBankBalance()); } - if (serviceSession.bankTransaction().money == 0) { - serviceSession.setBankDepositComplete(true); - npcSession.setStep(PlayerBotNpcConversationStep::Ready); + if (serviceWorkflow.bankTransaction().money == 0) { + serviceWorkflow.setBankDepositComplete(true); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Ready); schedule(SCHEDULER_MINTICKS); return; } telemetry.recordActionAttempt(); npc->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "deposit all"); - npcSession.setStep(PlayerBotNpcConversationStep::Confirm); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Confirm); schedule(SCHEDULER_MINTICKS); return; } - if (npcSession.step() == PlayerBotNpcConversationStep::Confirm) { + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Confirm) { telemetry.recordActionAttempt(); npc->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "yes"); - npcSession.setStep(PlayerBotNpcConversationStep::Verify); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Verify); schedule(SCHEDULER_MINTICKS); return; } - if (npcSession.step() == PlayerBotNpcConversationStep::Verify && !serviceSession.bankDepositComplete()) { - const PlayerBotServiceVerification verification = serviceSession.verifyBankDeposit( + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Verify && !serviceWorkflow.bankDepositComplete()) { + const PlayerBotServiceVerification verification = serviceWorkflow.verifyBankDeposit( player->getMoney(), player->getBankBalance(), maximumServiceAttempts); if (verification.result != PlayerBotServiceVerificationResult::Success) { if (verification.result == PlayerBotServiceVerificationResult::Rejected) { @@ -550,7 +542,7 @@ void PlayerBotController::processBank(Player* player, const Position& currentPos stop("bank_deposit_not_verified", currentPosition); return; } - npcSession.setStep(PlayerBotNpcConversationStep::Request); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Request); schedule(SCHEDULER_MINTICKS); return; } @@ -559,43 +551,40 @@ void PlayerBotController::processBank(Player* player, const Position& currentPos ",\"bank_after\":" + std::to_string(player->getBankBalance())); say(*player, "Deposited " + std::to_string(verification.before.money) + " gold. Bank: " + std::to_string(player->getBankBalance()) + '.'); - serviceSession.setBankDepositComplete(true); - npcSession.setStep(PlayerBotNpcConversationStep::Ready); - } - if (npcSession.step() == PlayerBotNpcConversationStep::Ready) { - if (!serviceSession.hasBankWithdrawal()) { - uint32_t amount = static_cast(std::min(carriedGoldReserve, player->getBankBalance())); - const uint32_t coinWeight = Item::items[ITEM_GOLD_COIN].weight; - if (coinWeight != 0) { - amount = std::min(amount, player->getFreeCapacity() / coinWeight); - } + serviceWorkflow.setBankDepositComplete(true); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Ready); + } + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Ready) { + if (!serviceWorkflow.hasBankWithdrawal()) { + const uint32_t amount = dispositionPolicy.bankWithdrawal( + {0, player->getFreeCapacity(), player->getMoney(), player->getBankBalance()}, Item::items[ITEM_GOLD_COIN].weight); if (amount == 0) { - serviceStage = ServiceStage::Complete; + serviceWorkflow.setStage(PlayerBotServiceStage::Complete); schedule(SCHEDULER_MINTICKS); return; } - serviceSession.beginBankWithdrawal(player->getBankBalance(), amount); + serviceWorkflow.beginBankWithdrawal(player->getBankBalance(), amount); } telemetry.recordActionAttempt(); - npc->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "withdraw " + std::to_string(serviceSession.bankTransaction().amount)); - npcSession.setStep(PlayerBotNpcConversationStep::Confirm); + npc->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "withdraw " + std::to_string(serviceWorkflow.bankTransaction().amount)); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Confirm); schedule(SCHEDULER_MINTICKS); return; } - if (npcSession.step() == PlayerBotNpcConversationStep::Confirm) { + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Confirm) { telemetry.recordActionAttempt(); npc->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "yes"); - npcSession.setStep(PlayerBotNpcConversationStep::Verify); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Verify); schedule(SCHEDULER_MINTICKS); return; } - const PlayerBotServiceVerification verification = serviceSession.verifyBankWithdrawal( + const PlayerBotServiceVerification verification = serviceWorkflow.verifyBankWithdrawal( player->getMoney(), player->getBankBalance(), maximumServiceAttempts); if (verification.result == PlayerBotServiceVerificationResult::Success) { emit("action_result", currentPosition, "\"action\":\"bank_withdraw\",\"result\":\"success\",\"count\":" + std::to_string(verification.before.amount) + ",\"bank_before\":" + std::to_string(verification.before.balance) + ",\"bank_after\":" + std::to_string(player->getBankBalance())); - serviceStage = ServiceStage::Complete; + serviceWorkflow.setStage(PlayerBotServiceStage::Complete); schedule(SCHEDULER_MINTICKS); return; } @@ -604,49 +593,49 @@ void PlayerBotController::processBank(Player* player, const Position& currentPos stop("bank_withdraw_not_verified", currentPosition); return; } - npcSession.setStep(PlayerBotNpcConversationStep::Ready); + serviceWorkflow.setNpcStep(PlayerBotNpcConversationStep::Ready); schedule(SCHEDULER_MINTICKS); } void PlayerBotController::processService(Player* player, const Position& currentPosition) { - if (serviceStage == ServiceStage::Discover) { - serviceSession.setBankDepositComplete(false); + if (serviceWorkflow.stage() == PlayerBotServiceStage::Discover) { + serviceWorkflow.setBankDepositComplete(false); discoverServices(currentPosition); schedule(SCHEDULER_MINTICKS); return; } - if (npcSession.step() == PlayerBotNpcConversationStep::Verify && serviceSession.hasShopTransaction() && - (serviceStage == ServiceStage::SellLoot || serviceStage == ServiceStage::BuyPotions)) { - auto service = std::find_if(serviceShops.begin(), serviceShops.end(), [this](const ServiceNpc& candidate) { - return candidate.id == npcSession.targetId(); + if (serviceWorkflow.npcStep() == PlayerBotNpcConversationStep::Verify && serviceWorkflow.hasShopTransaction() && + (serviceWorkflow.stage() == PlayerBotServiceStage::SellLoot || serviceWorkflow.stage() == PlayerBotServiceStage::BuyPotions)) { + auto service = std::find_if(serviceWorkflow.shops().begin(), serviceWorkflow.shops().end(), [this](const ServiceNpc& candidate) { + return candidate.id == serviceWorkflow.npcTargetId(); }); - if (service == serviceShops.end()) { + if (service == serviceWorkflow.shops().end()) { stop("shop_transaction_service_unavailable", currentPosition); return; } - const bool purchase = serviceStage != ServiceStage::SellLoot; - const char* action = serviceStage == ServiceStage::SellLoot ? "sell" : "buy_potions"; - const PlayerBotServiceTransaction& transaction = *serviceSession.shopTransaction(); + const bool purchase = serviceWorkflow.stage() != PlayerBotServiceStage::SellLoot; + const char* action = serviceWorkflow.stage() == PlayerBotServiceStage::SellLoot ? "sell" : "buy_potions"; + const PlayerBotServiceTransaction& transaction = *serviceWorkflow.shopTransaction(); processServiceShop(player, currentPosition, *service, action, transaction.itemId, transaction.amount, purchase); return; } - if (serviceStage == ServiceStage::SellLoot) { - if (pendingSlottedSaleItemId != 0 && - prepareSlottedSaleItem(player, pendingSlottedSaleItemId, currentPosition)) { + if (serviceWorkflow.stage() == PlayerBotServiceStage::SellLoot) { + if (const auto pending = serviceWorkflow.pendingSlottedSale(); pending && + prepareSlottedSaleItem(player, pending->itemId, currentPosition)) { return; } uint16_t itemId = 0; - ServiceNpc* seller = findLootSeller(player, currentPosition, itemId); + const ServiceNpc* seller = findLootSeller(player, currentPosition, itemId); if (!seller) { - serviceStage = ServiceStage::BuyPotions; + serviceWorkflow.setStage(PlayerBotServiceStage::BuyPotions); schedule(SCHEDULER_MINTICKS); return; } - if (!npcSession.targets(seller->id)) { - npcSession.reset(seller->id); - serviceApproachTarget = Position(); - serviceRejectedApproaches.clear(); + if (!serviceWorkflow.npcTargets(seller->id)) { + serviceWorkflow.resetNpc(seller->id); + serviceWorkflow.clearApproach(); + serviceWorkflow.clearRejectedApproaches(); clearNavigation(); } const uint32_t backpackSaleCount = inventoryPolicy.backpackSaleItemCount(*player, itemId); @@ -657,15 +646,15 @@ void PlayerBotController::processService(Player* player, const Position& current std::min(100, backpackSaleCount), false); return; } - if (serviceStage == ServiceStage::BuyPotions) { + if (serviceWorkflow.stage() == PlayerBotServiceStage::BuyPotions) { const uint16_t itemId = smallHealthPotionItemId; const uint32_t currentCount = inventoryPolicy.inventoryItemCount(*player, itemId); if (currentCount >= smallHealthPotionRestockTarget) { - serviceStage = ServiceStage::Bank; + serviceWorkflow.setStage(PlayerBotServiceStage::Bank); schedule(SCHEDULER_MINTICKS); return; } - ServiceNpc* seller = findShopFor(itemId, true, currentPosition); + const ServiceNpc* seller = findShopFor(itemId, true, currentPosition); if (!seller) { stop("required_shop_offer_unavailable", currentPosition); return; @@ -675,49 +664,38 @@ void PlayerBotController::processService(Player* player, const Position& current stop("required_shop_offer_unavailable", currentPosition); return; } - const uint32_t targetGap = smallHealthPotionRestockTarget - currentCount; - const uint64_t totalMoney = player->getMoney() + player->getBankBalance(); - const uint64_t reserve = inventoryPolicy.desiredCarriedGold(*player); - const uint32_t requiredGap = currentCount <= smallHealthPotionReturnThreshold ? - smallHealthPotionReturnThreshold + 1 - currentCount : 0; - if (totalMoney / offer->buyPrice < requiredGap) { + const PlayerBotEconomyRestockDecision restock = dispositionPolicy.restock( + {currentCount, player->getFreeCapacity(), player->getMoney(), player->getBankBalance()}, offer->buyPrice, + Item::items[itemId].weight); + if (restock.insufficientFunds) { stop("insufficient_potion_funds", currentPosition); return; } - uint32_t amount = totalMoney / offer->buyPrice >= targetGap ? targetGap : - static_cast(std::min(targetGap, - totalMoney > reserve ? (totalMoney - reserve) / offer->buyPrice : 0)); - if (currentCount <= smallHealthPotionReturnThreshold) { - amount = std::max(amount, static_cast(std::min(requiredGap, totalMoney / offer->buyPrice))); - } - const uint32_t itemWeight = Item::items[itemId].weight; - if (itemWeight != 0) { - amount = std::min(amount, player->getFreeCapacity() / itemWeight); - } + const uint32_t amount = restock.amount; if (amount == 0) { - serviceStage = ServiceStage::Bank; + serviceWorkflow.setStage(PlayerBotServiceStage::Bank); schedule(SCHEDULER_MINTICKS); return; } - if (!npcSession.targets(seller->id)) { - npcSession.reset(seller->id); - serviceApproachTarget = Position(); - serviceRejectedApproaches.clear(); + if (!serviceWorkflow.npcTargets(seller->id)) { + serviceWorkflow.resetNpc(seller->id); + serviceWorkflow.clearApproach(); + serviceWorkflow.clearRejectedApproaches(); clearNavigation(); } processServiceShop(player, currentPosition, *seller, "buy_potions", itemId, amount, true); return; } - if (serviceStage == ServiceStage::Bank) { - ServiceNpc* banker = findNearestService(serviceBankers, currentPosition); + if (serviceWorkflow.stage() == PlayerBotServiceStage::Bank) { + const ServiceNpc* banker = findNearestService(serviceWorkflow.bankers(), currentPosition); if (!banker) { stop("banker_unavailable", currentPosition); return; } - if (!npcSession.targets(banker->id)) { - npcSession.reset(banker->id); - serviceApproachTarget = Position(); - serviceRejectedApproaches.clear(); + if (!serviceWorkflow.npcTargets(banker->id)) { + serviceWorkflow.resetNpc(banker->id); + serviceWorkflow.clearApproach(); + serviceWorkflow.clearRejectedApproaches(); clearNavigation(); } processBank(player, currentPosition, *banker); @@ -770,40 +748,40 @@ bool PlayerBotController::findDepotLocker(const Position& position, uint16_t exp void PlayerBotController::clearDepotDiscovery() { - depotSession.clearDiscovery(); + depotWorkflow.clearDiscovery(); } bool PlayerBotController::discoverDepot(Player& player, const Position& currentPosition) { const auto now = std::chrono::steady_clock::now(); - depotSession.expireRejectedApproaches(now); - if (!depotSession.hasSelectedDepot() && depotSession.candidatesPrepared() && - depotSession.discoveryAnchor() != currentPosition) { + depotWorkflow.expireRejectedApproaches(now); + if (!depotWorkflow.hasSelectedDepot() && depotWorkflow.candidatesPrepared() && + depotWorkflow.discoveryAnchor() != currentPosition) { clearDepotDiscovery(); - depotSession.resetAttempts(); + depotWorkflow.resetAttempts(); } uint16_t lockerItemId = 0; - if (depotSession.hasSelectedDepot() && depotSession.approachPosition() != Position() && - playerbot::isInsideLocalPlanningArea(currentPosition, depotSession.lockerPosition()) && - findDepotLocker(depotSession.lockerPosition(), depotSession.depotId(), lockerItemId) && - lockerItemId == depotSession.lockerItemId()) { + if (depotWorkflow.hasSelectedDepot() && depotWorkflow.approachPosition() != Position() && + playerbot::isInsideLocalPlanningArea(currentPosition, depotWorkflow.lockerPosition()) && + findDepotLocker(depotWorkflow.lockerPosition(), depotWorkflow.depotId(), lockerItemId) && + lockerItemId == depotWorkflow.lockerItemId()) { return true; } - if (depotSession.hasSelectedDepot()) { + if (depotWorkflow.hasSelectedDepot()) { clearDepotDiscovery(); - depotSession.resetAttempts(); + depotWorkflow.resetAttempts(); } auto finishUnavailable = [&](const char* reason) { - const uint32_t attempts = depotSession.incrementAttempts(); + const uint32_t attempts = depotWorkflow.incrementAttempts(); const uint32_t retryDelay = std::min(depotRetryMaximumInterval, depotRetryInitialInterval << std::min(attempts - 1, 2)); if (shouldEmitRepeated(std::string("depot_discover:") + reason)) { emit("action_result", currentPosition, std::string("\"action\":\"depot_discover\",\"result\":\"unavailable\",\"reason\":") + jsonString(reason) + - ",\"indexed\":" + std::to_string(depotSession.indexedCandidateCount()) + - ",\"in_scope\":" + std::to_string(depotSession.inScopeCandidateCount()) + - ",\"standable\":" + std::to_string(depotSession.standableCandidateCount()) + - ",\"suppressed\":" + std::to_string(depotSession.suppressedApproachCount()) + + ",\"indexed\":" + std::to_string(depotWorkflow.indexedCandidateCount()) + + ",\"in_scope\":" + std::to_string(depotWorkflow.inScopeCandidateCount()) + + ",\"standable\":" + std::to_string(depotWorkflow.standableCandidateCount()) + + ",\"suppressed\":" + std::to_string(depotWorkflow.suppressedApproachCount()) + ",\"attempt\":" + std::to_string(attempts)); } clearDepotDiscovery(); @@ -815,12 +793,11 @@ bool PlayerBotController::discoverDepot(Player& player, const Position& currentP schedule(retryDelay); }; - if (!depotSession.candidatesPrepared()) { - depotSession.prepareCandidates(currentPosition); - auto& depotCandidates = depotSession.candidates(); + if (!depotWorkflow.candidatesPrepared()) { + depotWorkflow.beginDiscovery(currentPosition); for (const auto& entry : g_game.map.getDepotLockerPositions()) { for (const Position& lockerPosition : entry.second) { - ++depotSession.indexedCandidateCount(); + depotWorkflow.recordIndexedCandidate(); if (!playerbot::isInsideLocalPlanningArea(currentPosition, lockerPosition)) { continue; } @@ -828,7 +805,7 @@ bool PlayerBotController::discoverDepot(Player& player, const Position& currentP if (!findDepotLocker(lockerPosition, entry.first, indexedLockerItemId)) { continue; } - ++depotSession.inScopeCandidateCount(); + depotWorkflow.recordInScopeCandidate(); for (int32_t xOffset = -1; xOffset <= 1; ++xOffset) { for (int32_t yOffset = -1; yOffset <= 1; ++yOffset) { if (xOffset == 0 && yOffset == 0) { @@ -839,47 +816,44 @@ bool PlayerBotController::discoverDepot(Player& player, const Position& currentP if (!approachTile || approachTile->queryAdd(0, player, 1, FLAG_IGNOREBLOCKCREATURE) != RETURNVALUE_NOERROR) { continue; } - ++depotSession.standableCandidateCount(); - if (depotSession.isApproachRejected(approach)) { - ++depotSession.suppressedApproachCount(); + depotWorkflow.recordStandableCandidate(); + if (depotWorkflow.isApproachRejected(approach)) { + depotWorkflow.recordSuppressedApproach(); continue; } - depotCandidates.push_back({entry.first, indexedLockerItemId, lockerPosition, approach, - playerbot::localPlanningDistance(currentPosition, approach)}); + depotWorkflow.recordCandidate({entry.first, indexedLockerItemId, lockerPosition, approach, + playerbot::localPlanningDistance(currentPosition, approach)}); } } } } - std::sort(depotCandidates.begin(), depotCandidates.end(), [](const PlayerBotDepotCandidate& left, const PlayerBotDepotCandidate& right) { - return left.distance != right.distance ? left.distance < right.distance : - left.depotId != right.depotId ? left.depotId < right.depotId : - left.lockerPosition != right.lockerPosition ? left.lockerPosition < right.lockerPosition : - left.approachPosition < right.approachPosition; - }); + depotWorkflow.sortCandidates(); } - auto& depotCandidates = depotSession.candidates(); - if (depotCandidates.empty()) { - if (depotSession.suppressedApproachCount() != 0) { - auto earliestExpiry = depotSession.rejectedApproaches().begin()->second; - for (const auto& rejected : depotSession.rejectedApproaches()) { - earliestExpiry = std::min(earliestExpiry, rejected.second); + if (!depotWorkflow.hasCandidates()) { + if (depotWorkflow.suppressedApproachCount() != 0) { + const auto earliestExpiry = depotWorkflow.earliestRejectedApproachExpiry(); + if (!earliestExpiry) { + return false; } const uint32_t retryDelay = static_cast(std::max( - 1, std::chrono::duration_cast(earliestExpiry - now).count())); + 1, std::chrono::duration_cast(*earliestExpiry - now).count())); clearDepotDiscovery(); schedule(retryDelay); return false; } - finishUnavailable(depotSession.inScopeCandidateCount() == 0 ? "no_local_locker" : - depotSession.standableCandidateCount() == 0 ? "no_standable_approach" : "no_reachable_locker"); + finishUnavailable(depotWorkflow.inScopeCandidateCount() == 0 ? "no_local_locker" : + depotWorkflow.standableCandidateCount() == 0 ? "no_standable_approach" : "no_reachable_locker"); return false; } uint32_t routeValidations = 0; - while (depotSession.nextCandidate() < depotCandidates.size() && routeValidations < depotRouteValidationsPerDecision) { - const PlayerBotDepotCandidate candidate = depotCandidates[depotSession.nextCandidate()]; - depotSession.advanceCandidate(); + while (depotWorkflow.hasNextCandidate() && routeValidations < depotRouteValidationsPerDecision) { + const std::optional nextCandidate = depotWorkflow.takeNextCandidate(); + if (!nextCandidate) { + break; + } + const PlayerBotDepotCandidate candidate = *nextCandidate; uint16_t candidateLockerItemId = 0; Tile* approachTile = g_game.map.getTile(candidate.approachPosition); if (!playerbot::isInsideLocalPlanningArea(currentPosition, candidate.lockerPosition) || @@ -904,32 +878,32 @@ bool PlayerBotController::discoverDepot(Player& player, const Position& currentP (candidate.approachPosition == currentPosition || !steps.empty())); if (result != PlayerBotNavigationResult::Reached || (candidate.approachPosition != currentPosition && steps.empty())) { - depotSession.rejectApproach(candidate.approachPosition, now + depotApproachSuppression); + depotWorkflow.rejectApproach(candidate.approachPosition, now + depotApproachSuppression); continue; } - depotSession.select(candidate); + depotWorkflow.select(candidate); fixedTargetRouteFailureCount = 0; const size_t routeSteps = steps.size(); - adoptNavigationPlan(depotSession.approachPosition(), std::move(steps)); + adoptNavigationPlan(depotWorkflow.approachPosition(), std::move(steps)); std::ostringstream fields; - fields << "\"action\":\"depot_discover\",\"result\":\"success\",\"depot_id\":" << depotSession.depotId() - << ",\"locker_item_id\":" << depotSession.lockerItemId() << ",\"locker\":{\"x\":" << depotSession.lockerPosition().x - << ",\"y\":" << depotSession.lockerPosition().y << ",\"z\":" << static_cast(depotSession.lockerPosition().z) - << "},\"approach\":{\"x\":" << depotSession.approachPosition().x << ",\"y\":" << depotSession.approachPosition().y - << ",\"z\":" << static_cast(depotSession.approachPosition().z) << "},\"distance\":" << candidate.distance + fields << "\"action\":\"depot_discover\",\"result\":\"success\",\"depot_id\":" << depotWorkflow.depotId() + << ",\"locker_item_id\":" << depotWorkflow.lockerItemId() << ",\"locker\":{\"x\":" << depotWorkflow.lockerPosition().x + << ",\"y\":" << depotWorkflow.lockerPosition().y << ",\"z\":" << static_cast(depotWorkflow.lockerPosition().z) + << "},\"approach\":{\"x\":" << depotWorkflow.approachPosition().x << ",\"y\":" << depotWorkflow.approachPosition().y + << ",\"z\":" << static_cast(depotWorkflow.approachPosition().z) << "},\"distance\":" << candidate.distance << ",\"route_steps\":" << routeSteps << ",\"expanded_nodes\":" << expandedNodes - << ",\"indexed\":" << depotSession.indexedCandidateCount() << ",\"in_scope\":" << depotSession.inScopeCandidateCount() - << ",\"standable\":" << depotSession.standableCandidateCount(); + << ",\"indexed\":" << depotWorkflow.indexedCandidateCount() << ",\"in_scope\":" << depotWorkflow.inScopeCandidateCount() + << ",\"standable\":" << depotWorkflow.standableCandidateCount(); emit("action_result", currentPosition, fields.str()); return true; } - if (depotSession.nextCandidate() < depotCandidates.size()) { + if (depotWorkflow.hasNextCandidate()) { emit("action_result", currentPosition, std::string("\"action\":\"depot_discover\",\"result\":\"continuing\",\"reason\":\"route_validation_budget_exhausted\"") + - ",\"indexed\":" + std::to_string(depotSession.indexedCandidateCount()) + - ",\"in_scope\":" + std::to_string(depotSession.inScopeCandidateCount()) + - ",\"standable\":" + std::to_string(depotSession.standableCandidateCount()) + + ",\"indexed\":" + std::to_string(depotWorkflow.indexedCandidateCount()) + + ",\"in_scope\":" + std::to_string(depotWorkflow.inScopeCandidateCount()) + + ",\"standable\":" + std::to_string(depotWorkflow.standableCandidateCount()) + ",\"route_validations\":" + std::to_string(routeValidations)); schedule(blockedRouteRetryInterval); return false; @@ -982,12 +956,12 @@ bool PlayerBotController::openContainer(Player& player, Container& container, ui bool PlayerBotController::openDepotLocker(Player& player, const Position& currentPosition) { Container* opened = player.getContainerByID(depotLockerContainerId); - if (opened && opened->getDepotLocker() && opened->getDepotLocker()->getDepotId() == depotSession.depotId()) { - depotSession.resetAttempts(); - depotSession.setStage(PlayerBotDepotStage::OpenChest); + if (opened && opened->getDepotLocker() && opened->getDepotLocker()->getDepotId() == depotWorkflow.depotId()) { + depotWorkflow.resetAttempts(); + depotWorkflow.setStage(PlayerBotDepotStage::OpenChest); return true; } - Tile* tile = g_game.map.getTile(depotSession.lockerPosition()); + Tile* tile = g_game.map.getTile(depotWorkflow.lockerPosition()); TileItemVector* items = tile ? tile->getItemList() : nullptr; if (!items) { clearDepotDiscovery(); @@ -1001,25 +975,25 @@ bool PlayerBotController::openDepotLocker(Player& player, const Position& curren } for (Item* item : *items) { Container* container = item->getContainer(); - if (!container || !container->getDepotLocker() || container->getDepotLocker()->getDepotId() != depotSession.depotId()) { + if (!container || !container->getDepotLocker() || container->getDepotLocker()->getDepotId() != depotWorkflow.depotId()) { continue; } const int32_t stackPosition = tile->getThingIndex(item); if (stackPosition < 0 || stackPosition > UINT8_MAX) { break; } - if (depotSession.attempts() >= maximumDepotAttempts) { + if (depotWorkflow.attempts() >= maximumDepotAttempts) { logActionFailure("depot_open_locker", "open_not_verified", currentPosition); stop("depot_locker_open_failed", currentPosition); return false; } - const uint32_t attempts = depotSession.incrementAttempts(); + const uint32_t attempts = depotWorkflow.incrementAttempts(); player.closeContainer(depotLockerContainerId); telemetry.recordActionAttempt(); - g_game.playerUseItem(playerId, depotSession.lockerPosition(), static_cast(stackPosition), depotLockerContainerId, + g_game.playerUseItem(playerId, depotWorkflow.lockerPosition(), static_cast(stackPosition), depotLockerContainerId, item->getClientID()); emit("action_result", currentPosition, "\"action\":\"depot_open_locker\",\"result\":\"requested\",\"depot_id\":" + - std::to_string(depotSession.depotId()) + ",\"container_id\":" + std::to_string(depotLockerContainerId) + + std::to_string(depotWorkflow.depotId()) + ",\"container_id\":" + std::to_string(depotLockerContainerId) + ",\"attempt\":" + std::to_string(attempts)); if (pauseDepotFixtureForRestart(player, DepotRestartCheckpoint::Locker, currentPosition)) { return false; @@ -1038,12 +1012,12 @@ bool PlayerBotController::openDepotLocker(Player& player, const Position& curren bool PlayerBotController::openDepotChest(Player& player, const Position& currentPosition) { Container* locker = player.getContainerByID(depotLockerContainerId); - if (!locker || !locker->getDepotLocker() || locker->getDepotLocker()->getDepotId() != depotSession.depotId()) { - depotSession.setStage(PlayerBotDepotStage::OpenLocker); + if (!locker || !locker->getDepotLocker() || locker->getDepotLocker()->getDepotId() != depotWorkflow.depotId()) { + depotWorkflow.setStage(PlayerBotDepotStage::OpenLocker); schedule(blockedRouteRetryInterval); return false; } - DepotChest* chest = player.getDepotChest(depotSession.depotId(), false); + DepotChest* chest = player.getDepotChest(depotWorkflow.depotId(), false); if (!chest) { logActionFailure("depot_open_chest", "player_chest_missing", currentPosition); stop("depot_chest_missing", currentPosition); @@ -1053,8 +1027,8 @@ bool PlayerBotController::openDepotChest(Player& player, const Position& current chest->setMaxDepotItems(chest->getItemHoldingCount()); } if (player.getContainerByID(depotChestContainerId) == chest) { - depotSession.resetAttempts(); - depotSession.setStage(PlayerBotDepotStage::Deposit); + depotWorkflow.resetAttempts(); + depotWorkflow.setStage(PlayerBotDepotStage::Deposit); return true; } const int32_t index = locker->getThingIndex(chest); @@ -1067,18 +1041,18 @@ bool PlayerBotController::openDepotChest(Player& player, const Position& current schedule(navigationDecisionDelay(player)); return false; } - if (depotSession.attempts() >= maximumDepotAttempts) { + if (depotWorkflow.attempts() >= maximumDepotAttempts) { logActionFailure("depot_open_chest", "open_not_verified", currentPosition); stop("depot_chest_open_failed", currentPosition); return false; } - const uint32_t attempts = depotSession.incrementAttempts(); + const uint32_t attempts = depotWorkflow.incrementAttempts(); player.closeContainer(depotChestContainerId); telemetry.recordActionAttempt(); g_game.playerUseItem(playerId, Position(0xFFFF, 0x40 | depotLockerContainerId, static_cast(index)), static_cast(index), depotChestContainerId, chest->getClientID()); emit("action_result", currentPosition, "\"action\":\"depot_open_chest\",\"result\":\"requested\",\"depot_id\":" + - std::to_string(depotSession.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + + std::to_string(depotWorkflow.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + ",\"attempt\":" + std::to_string(attempts)); if (pauseDepotFixtureForRestart(player, DepotRestartCheckpoint::Chest, currentPosition)) { return false; @@ -1125,8 +1099,8 @@ void PlayerBotController::processFixtureDeposit(Player* player, const Position& stop("fake_depot_unavailable", currentPosition); return; } - if (depotSession.hasPendingMove()) { - const PlayerBotDepotMove pending = depotSession.move(); + if (depotWorkflow.hasPendingMove()) { + const PlayerBotDepotMove pending = depotWorkflow.move(); const uint32_t destinationCount = destination->getItemTypeCount(pending.itemId); if (destinationCount <= pending.destinationCount) { logActionFailure("deposit", "fixture_item_move_failed", currentPosition); @@ -1135,7 +1109,7 @@ void PlayerBotController::processFixtureDeposit(Player* player, const Position& } emit("action_result", currentPosition, "\"action\":\"deposit\",\"result\":\"success\",\"fixture\":true,\"item_id\":" + std::to_string(pending.itemId) + ",\"count\":" + std::to_string(destinationCount - pending.destinationCount)); - depotSession.clearMove(); + depotWorkflow.clearMove(); } if (player->getContainerByID(backpackContainerId) != backpack) { if (!player->canDoAction()) { @@ -1193,7 +1167,7 @@ void PlayerBotController::processFixtureDeposit(Player* player, const Position& return; } const uint8_t sourceIndex = static_cast(std::distance(sourceItems.begin(), sourceItem)); - depotSession.beginMove({depositItem->getID(), destination->getItemTypeCount(depositItem->getID()), + depotWorkflow.beginMove({depositItem->getID(), destination->getItemTypeCount(depositItem->getID()), inventoryPolicy.inventoryItemCount(*player, depositItem->getID()), static_cast(depositItem->getItemCount()), CONST_SLOT_WHEREEVER}); telemetry.recordActionAttempt(); @@ -1210,21 +1184,21 @@ void PlayerBotController::processDeposit(Player* player, const Position& current stop("depot_backpack_unavailable", currentPosition); return; } - if (depotSession.hasPendingMove()) { + if (depotWorkflow.hasPendingMove()) { Container* chest = player->getContainerByID(depotChestContainerId); if (!chest) { - depotSession.setStage(PlayerBotDepotStage::OpenChest); + depotWorkflow.setStage(PlayerBotDepotStage::OpenChest); schedule(navigationDecisionDelay(*player)); return; } - const PlayerBotDepotMoveVerification verification = depotSession.verifyMove( - inventoryPolicy.inventoryItemCount(*player, depotSession.move().itemId), - chest->getItemTypeCount(depotSession.move().itemId), maximumDepotAttempts); + const PlayerBotDepotMoveVerification verification = depotWorkflow.verifyMove( + inventoryPolicy.inventoryItemCount(*player, depotWorkflow.move().itemId), + chest->getItemTypeCount(depotWorkflow.move().itemId), maximumDepotAttempts); const PlayerBotDepotMove& move = verification.before; if (verification.result == PlayerBotDepotMoveResult::Moved) { std::ostringstream fields; fields << "\"action\":\"deposit\",\"result\":" << jsonString(verification.movedCount == move.requestedCount ? "success" : "partial") - << ",\"policy\":\"known_loot\",\"depot_id\":" << depotSession.depotId() << ",\"container_id\":" + << ",\"policy\":\"known_loot\",\"depot_id\":" << depotWorkflow.depotId() << ",\"container_id\":" << static_cast(depotChestContainerId) << ",\"item_id\":" << move.itemId << ",\"requested\":" << static_cast(move.requestedCount) << ",\"verified\":" << verification.movedCount << ",\"inventory_before\":" << move.inventoryCount @@ -1240,11 +1214,11 @@ void PlayerBotController::processDeposit(Player* player, const Position& current } else if (verification.result == PlayerBotDepotMoveResult::Deferred) { const uint16_t failedItemId = move.itemId; const slots_t failedSlot = move.sourceSlot; - unavailableSlottedSales[{failedItemId, failedSlot}] = - std::chrono::steady_clock::now() + unavailableDispositionCooldown; + serviceWorkflow.deferSlottedSale(failedItemId, failedSlot, + std::chrono::steady_clock::now() + unavailableDispositionCooldown); emit("action_result", currentPosition, "\"action\":\"deposit\",\"result\":\"deferred\",\"reason\":\"move_not_verified\",\"policy\":\"known_loot\",\"depot_id\":" + - std::to_string(depotSession.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + + std::to_string(depotWorkflow.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + ",\"item_id\":" + std::to_string(failedItemId) + ",\"source_slot\":" + std::to_string(failedSlot) + ",\"provider_available\":false,\"disposition\":\"deposit\",\"cooldown_ms\":" + std::to_string(std::chrono::duration_cast(unavailableDispositionCooldown).count())); @@ -1252,7 +1226,7 @@ void PlayerBotController::processDeposit(Player* player, const Position& current return; } else if (verification.result == PlayerBotDepotMoveResult::Rejected) { emit("action_result", currentPosition, "\"action\":\"deposit\",\"result\":\"failed\",\"reason\":\"no_slot_or_move_rejected\",\"policy\":\"known_loot\",\"depot_id\":" + - std::to_string(depotSession.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + + std::to_string(depotWorkflow.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + ",\"item_id\":" + std::to_string(move.itemId) + ",\"requested\":" + std::to_string(move.requestedCount) + ",\"verified\":0,\"inventory_before\":" + std::to_string(move.inventoryCount) + ",\"inventory_after\":" + std::to_string(verification.inventoryCount) + @@ -1262,7 +1236,7 @@ void PlayerBotController::processDeposit(Player* player, const Position& current return; } else { emit("action_result", currentPosition, "\"action\":\"deposit\",\"result\":\"retry\",\"reason\":\"not_verified\",\"policy\":\"known_loot\",\"depot_id\":" + - std::to_string(depotSession.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + + std::to_string(depotWorkflow.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + ",\"item_id\":" + std::to_string(move.itemId) + ",\"requested\":" + std::to_string(move.requestedCount) + ",\"verified\":0,\"inventory_before\":" + std::to_string(move.inventoryCount) + ",\"inventory_after\":" + std::to_string(verification.inventoryCount) + @@ -1273,18 +1247,18 @@ void PlayerBotController::processDeposit(Player* player, const Position& current } } - if (depotSession.stage() == PlayerBotDepotStage::Approach || depotSession.stage() == PlayerBotDepotStage::Discover) { - depotSession.setStage(PlayerBotDepotStage::OpenLocker); + if (depotWorkflow.stage() == PlayerBotDepotStage::Approach || depotWorkflow.stage() == PlayerBotDepotStage::Discover) { + depotWorkflow.setStage(PlayerBotDepotStage::OpenLocker); } - if (depotSession.stage() == PlayerBotDepotStage::OpenLocker && !openDepotLocker(*player, currentPosition)) { + if (depotWorkflow.stage() == PlayerBotDepotStage::OpenLocker && !openDepotLocker(*player, currentPosition)) { return; } - if (depotSession.stage() == PlayerBotDepotStage::OpenChest && !openDepotChest(*player, currentPosition)) { + if (depotWorkflow.stage() == PlayerBotDepotStage::OpenChest && !openDepotChest(*player, currentPosition)) { return; } Container* chest = player->getContainerByID(depotChestContainerId); - if (!chest || player->getDepotChest(depotSession.depotId(), false) != chest) { - depotSession.setStage(PlayerBotDepotStage::OpenChest); + if (!chest || player->getDepotChest(depotWorkflow.depotId(), false) != chest) { + depotWorkflow.setStage(PlayerBotDepotStage::OpenChest); schedule(blockedRouteRetryInterval); return; } @@ -1316,23 +1290,21 @@ void PlayerBotController::processDeposit(Player* player, const Position& current if (!depositItem || count == 0) { if (inventoryPolicy.effectiveFreeCapacity(*player) < returnCapacityThreshold) { const auto now = std::chrono::steady_clock::now(); - const auto deferred = std::find_if(unavailableSlottedSales.begin(), unavailableSlottedSales.end(), - [now](const auto& entry) { return entry.second > now; }); - if (deferred != unavailableSlottedSales.end()) { + if (const auto deferred = serviceWorkflow.nextDeferredSlottedSale(now)) { schedule(static_cast(std::chrono::duration_cast( - deferred->second - now).count())); + *deferred - now).count())); return; } stop("depot_capacity_not_recovered", currentPosition); return; } std::ostringstream fields; - fields << "\"action\":\"deposit\",\"result\":\"complete\",\"depot_id\":" << depotSession.depotId() + fields << "\"action\":\"deposit\",\"result\":\"complete\",\"depot_id\":" << depotWorkflow.depotId() << ",\"container_id\":" << static_cast(depotChestContainerId) << ",\"cycle\":" << completedCycles; emit("action_result", currentPosition, fields.str()); player->closeContainer(depotChestContainerId); player->closeContainer(depotLockerContainerId); - depotSession.setStage(PlayerBotDepotStage::Depart); + depotWorkflow.setStage(PlayerBotDepotStage::Depart); if (pauseDepotFixtureForRestart(*player, DepotRestartCheckpoint::Depart, currentPosition)) { return; } @@ -1378,7 +1350,7 @@ void PlayerBotController::processDeposit(Player* player, const Position& current return; } - depotSession.beginMove({depositItem->getID(), chest->getItemTypeCount(depositItem->getID()), + depotWorkflow.beginMove({depositItem->getID(), chest->getItemTypeCount(depositItem->getID()), inventoryPolicy.inventoryItemCount(*player, depositItem->getID()), count, sourceSlot}); const uint8_t submittedCount = fixtureRuntime.depotMoveFixture() == DepotMoveFixture::Partial && count > 1 ? count - 1 : count; telemetry.recordActionAttempt(); @@ -1386,10 +1358,10 @@ void PlayerBotController::processDeposit(Player* player, const Position& current Position(0xFFFF, 0x40 | depotChestContainerId, containerDestinationIndex(*chest, *depositItem)), submittedCount, depositItem, chest); emit("action_result", currentPosition, "\"action\":\"deposit\",\"result\":\"requested\",\"policy\":\"known_loot\",\"depot_id\":" + - std::to_string(depotSession.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + ",\"item_id\":" + - std::to_string(depotSession.move().itemId) + ",\"requested\":" + std::to_string(count) + ",\"submitted\":" + + std::to_string(depotWorkflow.depotId()) + ",\"container_id\":" + std::to_string(depotChestContainerId) + ",\"item_id\":" + + std::to_string(depotWorkflow.move().itemId) + ",\"requested\":" + std::to_string(count) + ",\"submitted\":" + std::to_string(submittedCount) + ",\"inventory_before\":" + - std::to_string(depotSession.move().inventoryCount) + ",\"depot_before\":" + std::to_string(depotSession.move().destinationCount) + + std::to_string(depotWorkflow.move().inventoryCount) + ",\"depot_before\":" + std::to_string(depotWorkflow.move().destinationCount) + ",\"source_slot\":" + (sourceSlot == CONST_SLOT_WHEREEVER ? "null" : std::to_string(sourceSlot)) + ",\"provider_available\":false,\"disposition\":\"deposit\""); if (pauseDepotFixtureForRestart(*player, DepotRestartCheckpoint::Deposit, currentPosition)) { diff --git a/server/src/playerbotserviceworkflow.cpp b/server/src/playerbotserviceworkflow.cpp new file mode 100644 index 0000000..901b094 --- /dev/null +++ b/server/src/playerbotserviceworkflow.cpp @@ -0,0 +1,153 @@ +#include "otpch.h" + +#include "playerbotserviceworkflow.h" + +void PlayerBotServiceWorkflow::reset() +{ + npcSession.reset(); + serviceSession.reset(); + serviceStage = PlayerBotServiceStage::Discover; + shopProviders.clear(); + bankProviders.clear(); + pendingApproach = Position(); + rejectedApproaches.clear(); + clearPendingSlottedSale(); +} + +bool PlayerBotServiceWorkflow::acceptNpcReply(uint32_t playerId, uint32_t replyingPlayerId, uint32_t npcId, uint8_t type) +{ + return npcSession.acceptReply(playerId, replyingPlayerId, npcId, type); +} + +PlayerBotNpcSessionOutcome PlayerBotServiceWorkflow::establishNpcFocus(Player& player, Npc& npc, uint32_t maximumRetries) +{ + return npcSession.establishFocus(player, npc, maximumRetries); +} + +PlayerBotNpcSessionOutcome PlayerBotServiceWorkflow::openNpcShop(Player& player, Npc& npc, uint32_t maximumRetries) +{ + return npcSession.openShop(player, npc, maximumRetries); +} + +PlayerBotServiceVerification PlayerBotServiceWorkflow::verifyShopTransaction(uint32_t itemCount, uint64_t money, + uint64_t balance, bool purchase, uint32_t unitPrice, uint32_t maximumRetries) +{ + return serviceSession.verifyShopTransaction(itemCount, money, balance, purchase, unitPrice, maximumRetries); +} + +PlayerBotServiceVerification PlayerBotServiceWorkflow::verifyBankDeposit(uint64_t money, uint64_t balance, + uint32_t maximumRetries) +{ + return serviceSession.verifyBankDeposit(money, balance, maximumRetries); +} + +PlayerBotServiceVerification PlayerBotServiceWorkflow::verifyBankWithdrawal(uint64_t money, uint64_t balance, + uint32_t maximumRetries) +{ + return serviceSession.verifyBankWithdrawal(money, balance, maximumRetries); +} + +void PlayerBotServiceWorkflow::setProviders(std::vector shops, + std::vector bankers) +{ + shopProviders = std::move(shops); + bankProviders = std::move(bankers); + std::sort(shopProviders.begin(), shopProviders.end(), [](const auto& left, const auto& right) { return left.id < right.id; }); + std::sort(bankProviders.begin(), bankProviders.end(), [](const auto& left, const auto& right) { return left.id < right.id; }); +} + +const PlayerBotEconomyProvider* PlayerBotServiceWorkflow::provider(uint32_t id, bool shop) const +{ + const auto& providers = shop ? shopProviders : bankProviders; + auto it = std::find_if(providers.begin(), providers.end(), [id](const auto& candidate) { return candidate.id == id; }); + return it == providers.end() ? nullptr : &*it; +} + +const PlayerBotEconomyProvider* PlayerBotServiceWorkflow::rankedProvider(const PlayerBotEconomyCatalog& catalog, + uint16_t itemId, bool purchase, const Position& position) const +{ + return catalog.rankedProvider(shopProviders, itemId, purchase, position); +} + +bool PlayerBotServiceWorkflow::isApproachRejected(const Position& position) const +{ + return rejectedApproaches.find(position) != rejectedApproaches.end(); +} + +void PlayerBotServiceWorkflow::rejectApproach(const Position& position) +{ + rejectedApproaches.insert(position); +} + +void PlayerBotServiceWorkflow::clearRejectedApproaches() +{ + rejectedApproaches.clear(); +} + +void PlayerBotServiceWorkflow::beginSlottedSale(uint16_t itemId, slots_t slot, uint32_t backpackCount) +{ + pendingSlottedItem = itemId; + pendingSlottedSlot = slot; + pendingSlottedBackpackItems = backpackCount; + ++slottedMoveAttempts; +} + +void PlayerBotServiceWorkflow::clearPendingSlottedSale() +{ + pendingSlottedItem = 0; + pendingSlottedSlot = CONST_SLOT_WHEREEVER; + pendingSlottedBackpackItems = 0; + slottedMoveAttempts = 0; +} + +std::optional PlayerBotServiceWorkflow::pendingSlottedSale() const +{ + if (!hasPendingSlottedSale()) { + return std::nullopt; + } + return PlayerBotSlottedSaleState{pendingSlottedItem, pendingSlottedSlot, pendingSlottedBackpackItems, slottedMoveAttempts}; +} + +PlayerBotSlottedSaleObservation PlayerBotServiceWorkflow::observeSlottedSale(bool moved, uint32_t maximumAttempts, + std::chrono::steady_clock::time_point now, std::chrono::steady_clock::duration cooldown) +{ + if (!hasPendingSlottedSale()) { + return PlayerBotSlottedSaleObservation::Pending; + } + if (moved) { + clearPendingSlottedSale(); + return PlayerBotSlottedSaleObservation::Moved; + } + const PlayerBotSlottedSaleState pending = *pendingSlottedSale(); + clearPendingSlottedSale(); + if (pending.attempts >= maximumAttempts) { + deferSlottedSale(pending.itemId, pending.sourceSlot, now + cooldown); + return PlayerBotSlottedSaleObservation::Deferred; + } + return PlayerBotSlottedSaleObservation::Retry; +} + +bool PlayerBotServiceWorkflow::slottedSaleUnavailable(uint16_t itemId, slots_t slot, + std::chrono::steady_clock::time_point now) const +{ + auto it = unavailableSlottedSales.find({itemId, slot}); + return it != unavailableSlottedSales.end() && it->second > now; +} + +void PlayerBotServiceWorkflow::deferSlottedSale(uint16_t itemId, slots_t slot, + std::chrono::steady_clock::time_point expires) +{ + unavailableSlottedSales[{itemId, slot}] = expires; +} + +std::optional PlayerBotServiceWorkflow::nextDeferredSlottedSale( + std::chrono::steady_clock::time_point now) const +{ + std::optional earliest; + for (const auto& entry : unavailableSlottedSales) { + if (entry.second > now && (!earliest || entry.second < *earliest)) { + earliest = entry.second; + } + } + return earliest; +} diff --git a/server/src/playerbotserviceworkflow.h b/server/src/playerbotserviceworkflow.h new file mode 100644 index 0000000..7550e22 --- /dev/null +++ b/server/src/playerbotserviceworkflow.h @@ -0,0 +1,113 @@ +/** Service orchestration. Engine interaction is deliberately supplied by the controller. */ +#ifndef FS_PLAYERBOTSERVICEWORKFLOW_H +#define FS_PLAYERBOTSERVICEWORKFLOW_H + +#include "playerboteconomy.h" +#include "playerbotnpcsession.h" +#include "playerbotservicesession.h" + +#include "creature.h" + +#include +#include + +enum class PlayerBotServiceStage : uint8_t { Discover, SellLoot, BuyPotions, Bank, Complete }; +enum class PlayerBotServiceCommandType : uint8_t { None, Sell, BuyPotions, DepositAll, Withdraw, Complete }; +enum class PlayerBotServiceOutcome : uint8_t { Pending, Success, Partial, Retry, Rejected, Unavailable }; + +struct PlayerBotServiceCommand { + PlayerBotServiceCommandType type = PlayerBotServiceCommandType::None; + uint32_t providerId = 0; + uint16_t itemId = 0; + uint32_t amount = 0; +}; + +enum class PlayerBotSlottedSaleObservation : uint8_t { Pending, Moved, Retry, Deferred }; + +struct PlayerBotSlottedSaleState { + uint16_t itemId = 0; + slots_t sourceSlot = CONST_SLOT_WHEREEVER; + uint32_t backpackCount = 0; + uint32_t attempts = 0; +}; + +class PlayerBotServiceWorkflow +{ + public: + void reset(); + PlayerBotServiceStage stage() const { return serviceStage; } + void setStage(PlayerBotServiceStage stage) { serviceStage = stage; } + void resetNpc(uint32_t targetId = 0) { npcSession.reset(targetId); } + bool npcTargets(uint32_t npcId) const { return npcSession.targets(npcId); } + uint32_t npcTargetId() const { return npcSession.targetId(); } + PlayerBotNpcConversationStep npcStep() const { return npcSession.step(); } + void setNpcStep(PlayerBotNpcConversationStep step) { npcSession.setStep(step); } + void resetNpcRetries() { npcSession.resetRetries(); } + void resetGreetingAcknowledgement() { npcSession.resetGreetingAcknowledgement(); } + bool isGreetingAcknowledged() const { return npcSession.isGreetingAcknowledged(); } + bool acceptNpcReply(uint32_t playerId, uint32_t replyingPlayerId, uint32_t npcId, uint8_t type); + PlayerBotNpcSessionOutcome establishNpcFocus(Player& player, Npc& npc, uint32_t maximumRetries); + PlayerBotNpcSessionOutcome openNpcShop(Player& player, Npc& npc, uint32_t maximumRetries); + uint32_t npcNextDelay() const { return npcSession.nextDelay(); } + + void resetTransactions() { serviceSession.reset(); } + bool hasShopTransaction() const { return serviceSession.hasShopTransaction(); } + const PlayerBotServiceTransaction* shopTransaction() const { return serviceSession.shopTransaction(); } + void beginShopTransaction(PlayerBotServiceTransaction transaction) { serviceSession.beginShopTransaction(transaction); } + PlayerBotServiceVerification verifyShopTransaction(uint32_t itemCount, uint64_t money, uint64_t balance, + bool purchase, uint32_t unitPrice, uint32_t maximumRetries); + bool bankDepositComplete() const { return serviceSession.bankDepositComplete(); } + void setBankDepositComplete(bool complete) { serviceSession.setBankDepositComplete(complete); } + bool hasBankDeposit() const { return serviceSession.hasBankDeposit(); } + void beginBankDeposit(uint64_t money, uint64_t balance) { serviceSession.beginBankDeposit(money, balance); } + PlayerBotServiceVerification verifyBankDeposit(uint64_t money, uint64_t balance, uint32_t maximumRetries); + bool hasBankWithdrawal() const { return serviceSession.hasBankWithdrawal(); } + const PlayerBotServiceTransaction& bankTransaction() const { return serviceSession.bankTransaction(); } + void beginBankWithdrawal(uint64_t balance, uint32_t amount) { serviceSession.beginBankWithdrawal(balance, amount); } + PlayerBotServiceVerification verifyBankWithdrawal(uint64_t money, uint64_t balance, uint32_t maximumRetries); + + void setProviders(std::vector shops, std::vector bankers); + const std::vector& shops() const { return shopProviders; } + const std::vector& bankers() const { return bankProviders; } + const PlayerBotEconomyProvider* provider(uint32_t id, bool shop) const; + const PlayerBotEconomyProvider* rankedProvider(const PlayerBotEconomyCatalog& catalog, uint16_t itemId, + bool purchase, const Position& position) const; + + const Position& approachTarget() const { return pendingApproach; } + void setApproachTarget(Position target) { pendingApproach = target; } + void clearApproach() { pendingApproach = Position(); } + bool isApproachRejected(const Position& position) const; + void rejectApproach(const Position& position); + void clearRejectedApproaches(); + + bool hasPendingSlottedSale() const { return pendingSlottedItem != 0; } + uint16_t pendingSlottedItemId() const { return pendingSlottedItem; } + slots_t pendingSlottedSourceSlot() const { return pendingSlottedSlot; } + uint32_t pendingSlottedBackpackCount() const { return pendingSlottedBackpackItems; } + uint32_t slottedSaleAttempts() const { return slottedMoveAttempts; } + std::optional pendingSlottedSale() const; + void beginSlottedSale(uint16_t itemId, slots_t slot, uint32_t backpackCount); + void clearPendingSlottedSale(); + PlayerBotSlottedSaleObservation observeSlottedSale(bool moved, uint32_t maximumAttempts, + std::chrono::steady_clock::time_point now, + std::chrono::steady_clock::duration cooldown); + bool slottedSaleUnavailable(uint16_t itemId, slots_t slot, std::chrono::steady_clock::time_point now) const; + void deferSlottedSale(uint16_t itemId, slots_t slot, std::chrono::steady_clock::time_point expires); + std::optional nextDeferredSlottedSale(std::chrono::steady_clock::time_point now) const; + + private: + PlayerBotNpcSession npcSession; + PlayerBotServiceSession serviceSession; + PlayerBotServiceStage serviceStage = PlayerBotServiceStage::Discover; + std::vector shopProviders; + std::vector bankProviders; + Position pendingApproach; + std::set rejectedApproaches; + uint16_t pendingSlottedItem = 0; + slots_t pendingSlottedSlot = CONST_SLOT_WHEREEVER; + uint32_t pendingSlottedBackpackItems = 0; + uint32_t slottedMoveAttempts = 0; + std::map, std::chrono::steady_clock::time_point> unavailableSlottedSales; +}; + +#endif diff --git a/server/src/playerbotspells.cpp b/server/src/playerbotspells.cpp index a415217..bbb26bb 100644 --- a/server/src/playerbotspells.cpp +++ b/server/src/playerbotspells.cpp @@ -596,7 +596,7 @@ void PlayerBotController::beginSpellTraining(Player& player, const Position& pos spellTrainingSession.begin(std::move(plan)); progressionSession.begin(PlayerBotProgressionProcedure::LearnSpell); const auto& training = spellTrainingSession.plan(); - npcSession.reset(training.npcId); + serviceWorkflow.resetNpc(training.npcId); navigationRuntime.adopt(training.approachPosition, std::move(steps)); emit("strategy_selection", position, "\"goal\":\"learn_spell\",\"npc_id\":" + std::to_string(training.npcId) + ",\"spell\":" + jsonString(training.spellName) + @@ -618,7 +618,7 @@ void PlayerBotController::finishSpellTraining(Player* player, const Position& po } progressionSession.reset(); spellTrainingSession.reset(); - npcSession.reset(); + serviceWorkflow.resetNpc(); clearNavigation(); goalArbiter.setCooldown(TopLevelGoal::LearnSpell, std::strcmp(result, "success") == 0 ? spellTrainingSuccessCooldown : spellTrainingFailureCooldown); @@ -649,7 +649,7 @@ void PlayerBotController::processSpellTraining(Player* player, const Position& c return; } if (spellTrainingSession.stage() == PlayerBotSpellTrainingStage::Greet) { - npcSession.resetGreetingAcknowledgement(); + serviceWorkflow.resetGreetingAcknowledgement(); telemetry.recordActionAttempt(); trainer->receiveSpeech(player, TALKTYPE_PRIVATE_PN, "hi"); spellTrainingSession.setStage(PlayerBotSpellTrainingStage::Request); @@ -657,7 +657,7 @@ void PlayerBotController::processSpellTraining(Player* player, const Position& c return; } if (spellTrainingSession.stage() == PlayerBotSpellTrainingStage::Request) { - if (!npcSession.isGreetingAcknowledged()) { + if (!serviceWorkflow.isGreetingAcknowledged()) { if (spellTrainingSession.incrementRetries() >= maximumProgressionAttempts) { finishSpellTraining(player, currentPosition, "failed", "trainer_focus_unconfirmed"); return; diff --git a/server/src/playerbottelemetry.cpp b/server/src/playerbottelemetry.cpp index febfdc8..4f360ef 100644 --- a/server/src/playerbottelemetry.cpp +++ b/server/src/playerbottelemetry.cpp @@ -102,11 +102,6 @@ uint64_t PlayerBotTelemetry::actionsAttempted() const return actionsAttemptedCount; } -uint64_t& PlayerBotTelemetry::actionsAttemptedForSession() -{ - return actionsAttemptedCount; -} - uint64_t PlayerBotTelemetry::pathfindingFailures() const { return pathfindingFailuresCount; diff --git a/server/src/playerbottelemetry.h b/server/src/playerbottelemetry.h index c0c19a1..5ca895c 100644 --- a/server/src/playerbottelemetry.h +++ b/server/src/playerbottelemetry.h @@ -59,7 +59,6 @@ namespace playerbot { void recordPathfindingAttempt(std::chrono::microseconds elapsed); void recordPathfinding(std::chrono::microseconds elapsed, bool found); uint64_t actionsAttempted() const; - uint64_t& actionsAttemptedForSession(); uint64_t pathfindingFailures() const; void maybeEmitSummary(const Position& position, const PlayerBotTelemetrySummary& summary); bool terminalLogged() const;