Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/playerbots.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ This design is intended to scale without one thread, graphical client, renderer,
| Loot | Ordinary; edge cases have focused evidence | Opens owned corpse containers through normal item use before inspecting contents, ranks known saleable loot by value and weight, and retries inaccessible corpses within bounds. Skinning and other secondary corpse actions are unsupported. |
| Navigation | Ordinary; transition and recovery scenarios have focused evidence | Routes across the loaded map using a shared coarse topology, then plans and executes local paths and supported transitions. Supports tools, doors, floor changes, teleports, and registered NPC travel. Detailed search and recovery have budgets; unsupported transitions and changing obstacles can prevent a journey. Static topology changes may require a supported reload or restart. |
| NPC service | Ordinary | Discovers loaded shops, bankers, spell trainers, and registered travel offers; buys supplies, sells known loot, banks, and verifies transactions. Custom dialogue or opaque travel conditions are excluded unless explicitly modeled. |
| Equipment and depots | Ordinary; restart and remote liquidation paths have focused evidence | Buys or equips supported upgrades, deposits retained loot, and can sell from local or remote depots. Two-handed loadout trade-offs are unsupported and those items remain protected. |
| Equipment and depots | Ordinary; restart and remote liquidation paths have focused evidence | Buys or equips supported upgrades, deposits retained loot, and can sell from local or remote depots. A Knight with an empty back slot can buy a standard backpack from a discovered shop. To upgrade a basic bag, it stages the complete bag in a discovered depot, buys with the empty back slot as the verified receipt destination, then retrieves the old bag into the new backpack. The bot stages only after its bank balance can pay the backpack price and preserve the configured reserve; carried coins inside the bag do not count for this preflight. Bounded failures restore the bag before purchase when possible. An unresolved depot recovery is stored on the player and tagged bag, stops the controller with an explicit failure, and resumes before normal goals after reload. If death equips a replacement basic bag, recovery deposits that complete bag before restoring the tagged original. Other containers and two-handed loadout trade-offs are unsupported and protected. |
| Spells | Ordinary for audited Knight spells; calibration and overflow training have focused evidence | Can learn and use selected healing, support, and offensive spells through normal speech. Loaded spell rules remain authoritative. Observations may rank legal casts but never weaken safety or legality. |
| Rewards and Oracle | Ordinary goal selection; claiming and departure have focused evidence | Claims supported shared container rewards and one legacy doublet case. The tagged Oracle departure path supports a level 8–10 character with no vocation. Scripted quests, levers, hazardous quest transit, and general dialogue reasoning are unsupported. |
| Death and restart | Ordinary; failure paths have focused evidence | Uses normal death, save, temple login, and bounded relog recovery. It reconstructs intent from persisted state rather than resuming an interrupted route, conversation, open container, or transaction in place. |
Expand Down
1 change: 1 addition & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ On Linux, the C++ contract checks require a C++17 compiler. Lua fixture checks r

```sh
sh server/tests/playerbot_contracts.sh
sh server/tests/playerbot_equipment_purchase_contracts.sh
lua scripts/test-playerbot-fixture-isolation.lua
lua scripts/test-playerbot-hunt-fixture.lua
lua scripts/test-playerbot-depot-fixture.lua
Expand Down
2 changes: 2 additions & 0 deletions server/src/playerbotcombat.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ bool PlayerBotController::handleHealing(Player* player, const Position& currentP
if (command.type == PlayerBotSurvivalCommandType::CastSpell) return dispatchSpellCommand(*player, currentPosition, command);
if (command.type == PlayerBotSurvivalCommandType::Wait) return true;
if (command.type == PlayerBotSurvivalCommandType::InterruptForService) {
if (progressionRuntime.equipmentBackpackRecoveryActive()) return false;
std::ostringstream fields;
fields << "\"action\":\"heal\",\"result\":\"skipped\",\"reason\":\"missing_supply\""
<< ",\"method\":" << jsonString(snapshot.potionItemId == smallHealthPotionItemId ? "small_health_potion" : "health_potion")
Expand All @@ -281,6 +282,7 @@ bool PlayerBotController::handleHealing(Player* player, const Position& currentP
<< ",\"resource_before\":0,\"resource_after\":0";
emit("action_result", currentPosition, fields.str());
if (progressionRuntime.session().active() != PlayerBotProgressionProcedure::None) {
if (interruptBackpackUpgradeForService(*player, currentPosition, "healing_supply_missing")) return true;
if (progressionRuntime.session().active(PlayerBotProgressionProcedure::OracleDeparture)) {
finishOracleDeparture(player, currentPosition, "interrupted", "healing_supply_missing");
} else {
Expand Down
12 changes: 9 additions & 3 deletions server/src/playerbotcontroller.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,11 @@ void PlayerBotController::start(const Position& position, bool recovered, uint32
refreshItemValues();
const bool startInHunt = !recovered && fixtureDriver.startInHunt();
Player* controlledPlayer = g_game.getPlayerByID(playerId);
const bool backpackUpgradeResumed = controlledPlayer && resumeBackpackUpgrade(*controlledPlayer, position);
if (!turnRouter.running()) return;
const bool departureComplete = controlledPlayer && departurePlanner.hasCompleted(departureSnapshot(*controlledPlayer));
const bool departureRequired = controlledPlayer && departurePlanner.required(departureSnapshot(*controlledPlayer));
const bool useGoalSelector = controlledPlayer && !startInHunt &&
const bool useGoalSelector = controlledPlayer && !backpackUpgradeResumed && !startInHunt &&
(departureRequired || (!recovered && fixtureDriver.startWithGoalSelection()));
if (!fixtureDriver.magicTrainingScenario() && !fixtureDriver.deferInitialization() && useGoalSelector &&
!selectTopLevelGoal(*controlledPlayer, position, "startup")) {
Expand Down Expand Up @@ -231,7 +233,9 @@ void PlayerBotController::start(const Position& position, bool recovered, uint32
lifecycle << "\"status\":\"online\",\"message\":\"Playerbot online\""
<< ",\"recovered\":" << (recovered ? "true" : "false")
<< ",\"recovery_count\":" << recoveryCount
<< ",\"objective\":" << jsonString((fixtureDriver.magicTrainingScenario() || fixtureDriver.deferInitialization()) ? "fixture_pending" : useGoalSelector ? PlayerBotGoalArbiter::goalName(progressionRuntime.activeGoal()) :
<< ",\"objective\":" << jsonString(backpackUpgradeResumed ? "buy_equipment" :
(fixtureDriver.magicTrainingScenario() || fixtureDriver.deferInitialization()) ? "fixture_pending" :
useGoalSelector ? PlayerBotGoalArbiter::goalName(progressionRuntime.activeGoal()) :
(startInHunt ? "hunt" : "service"))
<< ",\"step_speed\":" << (g_game.getPlayerByID(playerId) ? g_game.getPlayerByID(playerId)->getSpeed() : 0)
<< ",\"spell_calibration_profiles\":" << survivalRuntime.calibrationSize()
Expand All @@ -247,7 +251,9 @@ void PlayerBotController::start(const Position& position, bool recovered, uint32
emitFixtureEvents(fixtureDriver.runAdaptiveChallenge(*controlledPlayer), position);
emitFixtureEvents(fixtureDriver.runDepotRiskFallbackContract(), position);
}
if (fixtureDriver.magicTrainingScenario() || fixtureDriver.deferInitialization()) {
if (backpackUpgradeResumed) {
// Durable backpack recovery owns the normal progression command until resolved.
} else if (fixtureDriver.magicTrainingScenario() || fixtureDriver.deferInitialization()) {
fixtureDriver.beginDelayedInitialization();
} else if (useGoalSelector) {
// The selected goal initialized its own executor state.
Expand Down
8 changes: 8 additions & 0 deletions server/src/playerbotcontroller.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,11 @@ class PlayerBotController : public std::enable_shared_from_this<PlayerBotControl
void beginEquipmentPurchase(Player& player, const Position& position, EquipmentOfferEvaluation evaluation);
void processEquipmentPurchase(Player* player, const Position& position);
void finishEquipmentPurchase(Player* player, const Position& position, const char* result, const char* reason);
bool resumeBackpackUpgrade(Player& player, const Position& position);
bool interruptBackpackUpgradeForService(Player& player, const Position& position, const char* reason);
Item* taggedBackpackUpgradeBag(Player& player, uint32_t* matches = nullptr) const;
Item* taggedBackpackUpgradeReplacementBag(Player& player, uint32_t* matches = nullptr) const;
void clearBackpackUpgradePersistence(Player& player, bool clearItemMarker);

std::string rewardInspectionItemsJson(const RewardInspection& inspection) const;

Expand Down Expand Up @@ -496,6 +501,9 @@ class PlayerBotController : public std::enable_shared_from_this<PlayerBotControl
std::string pendingHuntCompletionReason;
PlayerBotRewardPlanner rewardPlanner;
PlayerBotEquipmentProviderPlanner equipmentProviderPlanner;
std::optional<PlayerBotDepotCandidate> backpackUpgradeDepot;
int32_t backpackUpgradeToken = 0;
uint16_t backpackUpgradeDepotId = 0;
size_t equipmentProviderScanOffset = 0;
size_t equipmentOfferScanOffset = 0;
PlayerBotSpellTrainingPlanner spellTrainingPlanner;
Expand Down
Loading