From f7184224154e9457f8c0f9399fd264b1339b8e69 Mon Sep 17 00:00:00 2001
From: adrunkhuman <16039109+adrunkhuman@users.noreply.github.com>
Date: Sun, 2 Aug 2026 16:44:32 +0200
Subject: [PATCH] server: run first mainland playerbot loop
---
scripts/test-playerbot-gameplay.ps1 | 78 +++++++++++++++++--
server/data/npc/Frodo.xml | 1 +
server/data/npc/Naji.xml | 3 +
server/data/npc/Sam.xml | 1 +
server/data/npc/Xodet.xml | 4 +-
server/schema/insertPlayerbots.sql | 22 +++---
server/src/playerbotcombat.cpp | 14 ++--
server/src/playerbotcontroller.cpp | 14 ++--
server/src/playerbotcontroller.h | 1 +
server/src/playerbotdeparture.cpp | 5 +-
server/src/playerbothuntregions.cpp | 43 +++++++---
server/src/playerbothuntregions.h | 3 +-
server/src/playerbotnavigation.cpp | 22 ++----
server/src/playerbotservice.cpp | 15 +++-
.../playerbot-gameplay/playerbot_gameplay.lua | 52 +++++++++++--
15 files changed, 205 insertions(+), 73 deletions(-)
diff --git a/scripts/test-playerbot-gameplay.ps1 b/scripts/test-playerbot-gameplay.ps1
index ccd3595..51be6d2 100644
--- a/scripts/test-playerbot-gameplay.ps1
+++ b/scripts/test-playerbot-gameplay.ps1
@@ -13,6 +13,7 @@ param(
[switch]$HuntRegionPlanning,
[switch]$CombatReadiness,
[switch]$Depot,
+ [switch]$MainlandLoop,
[switch]$Focused,
[switch]$SkipBuild,
[switch]$KeepStack
@@ -272,7 +273,7 @@ function Assert-OracleDepartureEvents {
$events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
if ($Restart) {
$restored = @($events | Where-Object {
- $_.event -eq "lifecycle" -and $_.status -eq "online" -and $_.objective -eq "departure_complete"
+ $_.event -eq "lifecycle" -and $_.status -eq "online" -and $_.objective -eq "service"
})
if ($restored.Count -lt 1) {
throw "The persisted Oracle departure state was not restored."
@@ -295,13 +296,12 @@ function Assert-OracleDepartureEvents {
$goalResult = @($events | Where-Object {
$_.event -eq "goal_result" -and $_.goal -eq "oracle_departure" -and $_.result -eq "success"
})
- $stopped = @($events | Where-Object {
- $_.event -eq "state_transition" -and $_.to -eq "stopped"
+ $continued = @($events | Where-Object {
+ $_.event -eq "objective_transition" -and $_.to -eq "service" -and $_.reason -eq "departure_complete"
})
- $wrongStoppedCount = if ($InterruptedByRestart) { $stopped.Count -lt 1 } else { $stopped.Count -ne 1 }
if ($candidate.Count -lt 1 -or $selection.Count -lt 1 -or $result.Count -ne 1 -or
- $goalResult.Count -ne 1 -or $wrongStoppedCount) {
- throw "The bot did not complete and verify the selected Oracle departure: candidate=$($candidate.Count), selection=$($selection.Count), result=$($result.Count), goalResult=$($goalResult.Count), stopped=$($stopped.Count)."
+ $goalResult.Count -ne 1 -or $continued.Count -ne 1) {
+ throw "The bot did not complete and continue after the selected Oracle departure: candidate=$($candidate.Count), selection=$($selection.Count), result=$($result.Count), goalResult=$($goalResult.Count), continued=$($continued.Count)."
}
}
@@ -1102,13 +1102,51 @@ function Assert-DepotRecoveryEvents {
}
}
+function Assert-MainlandLoopEvents {
+ param([string]$Logs, [int]$MinimumCycles = 3, [int]$MinimumDeposits = 2)
+
+ $events = @(ConvertFrom-PlayerbotLogs -Logs $Logs)
+ $hunts = @($events | Where-Object {
+ $_.event -eq "action_result" -and $_.action -eq "hunt_cycle" -and $_.result -eq "started"
+ })
+ $deposits = @($events | Where-Object {
+ $_.event -eq "action_result" -and $_.action -eq "deposit" -and $_.result -eq "complete" -and $_.depot_id -eq 2
+ })
+ $realDepot = @($events | Where-Object {
+ $_.event -eq "action_result" -and $_.action -eq "depot_discover" -and $_.result -eq "success" -and
+ $_.locker.x -eq 32344 -and $_.locker.y -eq 32218 -and $_.locker.z -eq 5
+ })
+ $trainingRoomDepot = @($events | Where-Object {
+ $_.event -eq "action_result" -and $_.action -eq "depot_discover" -and $_.result -eq "success" -and
+ $_.locker.x -eq 32276 -and $_.locker.y -eq 32218 -and $_.locker.z -eq 11
+ })
+ $selection = @($events | Where-Object {
+ $_.event -eq "hunt_region_selection" -and $_.result -eq "selected" -and
+ [Math]::Abs($_.center.x - 32369) + [Math]::Abs($_.center.y - 32241) +
+ 20 * [Math]::Abs($_.center.z - 7) -le 200
+ })
+ $cheeseDeposit = @($events | Where-Object {
+ $_.event -eq "action_result" -and $_.action -eq "deposit" -and $_.result -in @("success", "partial") -and
+ $_.item_id -eq 2696
+ })
+ $rookService = @($events | Where-Object {
+ $_.event -eq "npc_reply" -and $_.npc_name -in @("Billy", "Willie", "Lily", "Paulie")
+ })
+ $terminal = @($events | Where-Object { $_.event -eq "terminal" })
+ if ($hunts.Count -lt $MinimumCycles -or $deposits.Count -lt $MinimumDeposits -or $realDepot.Count -lt 1 -or
+ $trainingRoomDepot.Count -ne 0 -or
+ $selection.Count -lt 1 -or $cheeseDeposit.Count -lt 1 -or $rookService.Count -ne 0 -or $terminal.Count -ne 0) {
+ throw "Mainland loop failed. hunts=$($hunts.Count), deposits=$($deposits.Count), realDepot=$($realDepot.Count), trainingRoomDepot=$($trainingRoomDepot.Count), localSelections=$($selection.Count), cheeseDeposits=$($cheeseDeposit.Count), rookService=$($rookService.Count), terminal=$($terminal.Count)."
+ }
+}
+
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw "Docker is required to run the playerbot gameplay suite."
}
$focusedScenarioRequested = $FullNavigation -or $CorpseLoot -or $DeathTelemetry -or $Healing -or $ValueLoot -or
$PickupProgression -or $GoalArbitration -or $OracleDeparture -or $StaminaProjection -or $HuntRegionPlanning -or
- $CombatReadiness -or $Depot
+ $CombatReadiness -or $Depot -or $MainlandLoop
if ($Focused -and -not $focusedScenarioRequested) {
throw "-Focused requires at least one focused scenario switch."
}
@@ -1140,6 +1178,30 @@ try {
}
}
+ if ($MainlandLoop) {
+ Invoke-Scenario -Name "mainland_loop" -DefaultTimeoutSeconds 600 -Body {
+ Invoke-Compose down --volumes --remove-orphans
+ $env:PLAYERBOT_GAMEPLAY_MODE = "mainland"
+ $env:PLAYERBOT_HUNT_DURATION_SECONDS = "10"
+ Invoke-Compose up --detach
+ Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST MAINLAND_START' | Out-Null
+ $loopLogs = Wait-ForLog -Pattern '"action":"hunt_cycle","result":"started","cycle":3'
+ Assert-MainlandLoopEvents -Logs $loopLogs
+
+ $restartLineCount = @((Get-ServerLogs) -split "`r?`n").Count
+ Invoke-Compose stop server
+ Invoke-Compose up --detach server
+ $restartLogs = ""
+ for ($attempt = 0; $attempt -lt 300; $attempt++) {
+ Start-Sleep -Seconds 1
+ $restartLogs = ((Get-ServerLogs) -split "`r?`n" | Select-Object -Skip $restartLineCount) -join "`n"
+ if ($restartLogs -match '"action":"deposit","result":"complete","depot_id":2' -and
+ $restartLogs -match '"action":"hunt_cycle","result":"started","cycle":1') { break }
+ }
+ Assert-MainlandLoopEvents -Logs $restartLogs -MinimumCycles 1 -MinimumDeposits 1
+ }
+ }
+
if ($Depot) {
Invoke-Scenario -Name "real_depot" -DefaultTimeoutSeconds 240 -Body {
Invoke-Compose down --volumes --remove-orphans
@@ -1439,7 +1501,7 @@ try {
Invoke-Compose stop server
Invoke-Compose up --detach server
Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST ORACLE_DEPARTURE_RESTART_PASS' | Out-Null
- $restartLogs = Wait-ForLog -Pattern '"objective":"departure_complete"'
+ $restartLogs = Wait-ForLog -Pattern 'PLAYERBOT_GAMEPLAY_TEST ORACLE_DEPARTURE_RESTART_PASS'
Assert-OracleDepartureEvents -Logs $restartLogs -Restart
}
diff --git a/server/data/npc/Frodo.xml b/server/data/npc/Frodo.xml
index c3469a1..529f6e6 100644
--- a/server/data/npc/Frodo.xml
+++ b/server/data/npc/Frodo.xml
@@ -3,6 +3,7 @@
+
diff --git a/server/data/npc/Naji.xml b/server/data/npc/Naji.xml
index 2434bf3..0fe3f5c 100644
--- a/server/data/npc/Naji.xml
+++ b/server/data/npc/Naji.xml
@@ -2,4 +2,7 @@
+
+
+
diff --git a/server/data/npc/Sam.xml b/server/data/npc/Sam.xml
index ee10774..22b4154 100644
--- a/server/data/npc/Sam.xml
+++ b/server/data/npc/Sam.xml
@@ -3,6 +3,7 @@
+
+
= huntDeadline || player->getFreeCapacity() < returnCapacityThreshold)) {
const char* reason = player->getFreeCapacity() < returnCapacityThreshold ? "capacity" : "hunt_deadline";
- if (testPolicy.progressionEnabled) {
+ if (testPolicy.progressionEnabled && !hasCompletedRookgaardDeparture(*player)) {
finishHuntAndSelectGoal(player, currentPosition, reason);
return;
} else {
diff --git a/server/src/playerbotcontroller.cpp b/server/src/playerbotcontroller.cpp
index ae0f45f..c93576e 100644
--- a/server/src/playerbotcontroller.cpp
+++ b/server/src/playerbotcontroller.cpp
@@ -44,6 +44,7 @@ const PlayerBotTestPolicy& playerbot::testPolicyFromEnvironment()
std::strcmp(gameplayMode, "stamina_boundary") != 0 &&
std::strcmp(gameplayMode, "stamina_normal") != 0 &&
std::strcmp(gameplayMode, "hunt_planning") != 0 &&
+ std::strcmp(gameplayMode, "mainland") != 0 &&
std::strcmp(gameplayMode, "depot") != 0;
const char* depotRestartPhase = std::getenv("PLAYERBOT_DEPOT_RESTART_PHASE");
const DepotRestartCheckpoint depotRestartCheckpoint = !depotRestartPhase ? DepotRestartCheckpoint::None :
@@ -87,23 +88,20 @@ void PlayerBotController::start(const Position& position, bool recovered, uint32
Player* controlledPlayer = g_game.getPlayerByID(playerId);
const bool departureComplete = controlledPlayer && hasCompletedRookgaardDeparture(*controlledPlayer);
const bool departureRequired = controlledPlayer && requiresRookgaardDeparture(*controlledPlayer);
- const bool useGoalSelector = controlledPlayer && (departureRequired || (!recovered && testPolicy.progressionEnabled));
- if (useGoalSelector && !departureComplete && !selectTopLevelGoal(*controlledPlayer, position, "startup")) {
+ const bool useGoalSelector = controlledPlayer && !departureComplete &&
+ (departureRequired || (!recovered && testPolicy.progressionEnabled));
+ if (useGoalSelector && !selectTopLevelGoal(*controlledPlayer, position, "startup")) {
return;
}
std::ostringstream lifecycle;
lifecycle << "\"status\":\"online\",\"message\":\"Playerbot online\""
<< ",\"recovered\":" << (recovered ? "true" : "false")
<< ",\"recovery_count\":" << recoveryCount
- << ",\"objective\":" << jsonString(departureComplete ? "departure_complete" :
- useGoalSelector ? topLevelGoalName(activeGoal) :
+ << ",\"objective\":" << jsonString(useGoalSelector ? topLevelGoalName(activeGoal) :
(startInHunt ? "hunt" : "service"))
<< ",\"step_speed\":" << (g_game.getPlayerByID(playerId) ? g_game.getPlayerByID(playerId)->getSpeed() : 0);
emit("lifecycle", position, lifecycle.str());
- if (departureComplete) {
- setStage(ScenarioStage::Stopped, position);
- return;
- } else if (useGoalSelector) {
+ if (useGoalSelector) {
// The selected goal initialized its own executor state.
} else if (startInHunt) {
activeGoal = TopLevelGoal::Hunt;
diff --git a/server/src/playerbotcontroller.h b/server/src/playerbotcontroller.h
index c2eeb67..fddc426 100644
--- a/server/src/playerbotcontroller.h
+++ b/server/src/playerbotcontroller.h
@@ -355,6 +355,7 @@ class PlayerBotController : public std::enable_shared_from_this regions;
+ std::vector candidateIndices;
std::string reason;
std::chrono::steady_clock::time_point started;
size_t nextCandidate = 0;
diff --git a/server/src/playerbotdeparture.cpp b/server/src/playerbotdeparture.cpp
index 7d37f7e..ffe0b37 100644
--- a/server/src/playerbotdeparture.cpp
+++ b/server/src/playerbotdeparture.cpp
@@ -173,9 +173,10 @@ void PlayerBotController::finishOracleDeparture(Player* player, const Position&
serviceTargetId = 0;
clearNavigation();
if (std::strcmp(result, "success") == 0) {
- setStage(ScenarioStage::Stopped, position);
if (player) {
- say(*player, "Rookgaard departure complete. Awaiting mainland behavior.");
+ say(*player, "Rookgaard departure complete. Starting mainland service.");
+ beginService(player, position, "departure_complete");
+ schedule(navigationInterval);
}
return;
}
diff --git a/server/src/playerbothuntregions.cpp b/server/src/playerbothuntregions.cpp
index 16bfe55..d846249 100644
--- a/server/src/playerbothuntregions.cpp
+++ b/server/src/playerbothuntregions.cpp
@@ -30,18 +30,17 @@ extern Game g_game;
extern ConfigManager g_config;
namespace {
- constexpr Position rookTemple(32097, 32219, 7);
- constexpr int32_t rookRadius = 180;
constexpr int32_t heatRadius = 8;
constexpr int32_t maximumRegionRadius = 24;
constexpr uint16_t spawnBucketSize = heatRadius * 2 + 1;
+ constexpr uint32_t maximumRegionDistancePadding = maximumRegionRadius * 2;
+ constexpr uint32_t maximumHuntDistanceFromTemple = 200;
+ constexpr uint32_t maximumHuntTravelDistance = 300;
constexpr double maximumThreatRatio = 0.35;
bool isRookgaardSpawn(const Position& position)
{
- return position.z >= 6 && position.z <= 15 &&
- Position::getDistanceX(position, rookTemple) <= rookRadius &&
- Position::getDistanceY(position, rookTemple) <= rookRadius;
+ return position.z >= 6 && position.z <= 15;
}
bool heatOverlaps(const Position& left, const Position& right)
@@ -301,11 +300,22 @@ namespace {
(void)name;
region.monsters.push_back(std::move(profile));
}
+ const uint32_t geometricDistance = Position::getDistanceX(player.getPosition(), region.destination) +
+ Position::getDistanceY(player.getPosition(), region.destination) +
+ Position::getDistanceZ(player.getPosition(), region.destination) * 20;
+ const Position& templePosition = player.getTemplePosition();
+ const uint32_t templeDistance = Position::getDistanceX(templePosition, region.destination) +
+ Position::getDistanceY(templePosition, region.destination) +
+ Position::getDistanceZ(templePosition, region.destination) * 20;
region.threatRatio = worstFightDamage / std::max(player.getMaxHealth(), 1);
- region.suitable = region.threatRatio <= maximumThreatRatio;
+ region.suitable = region.threatRatio <= maximumThreatRatio &&
+ templeDistance <= maximumHuntDistanceFromTemple &&
+ geometricDistance <= maximumHuntTravelDistance;
if (excludedRegions.find(region.center) != excludedRegions.end()) {
region.suitable = false;
region.rejectionReason = "observed_danger_cooldown";
+ } else if (templeDistance > maximumHuntDistanceFromTemple || geometricDistance > maximumHuntTravelDistance) {
+ region.rejectionReason = "travel_distance";
} else if (!region.suitable) {
region.rejectionReason = "predicted_damage";
}
@@ -315,9 +325,6 @@ namespace {
region.observedExperiencePerMinute = observed->second.observedExperiencePerMinute;
region.observedCorrection = observed->second.correction;
}
- const uint32_t geometricDistance = Position::getDistanceX(player.getPosition(), region.destination) +
- Position::getDistanceY(player.getPosition(), region.destination) +
- Position::getDistanceZ(player.getPosition(), region.destination) * 20;
region.estimatedTravelSeconds = geometricDistance * player.getStepDuration() / 1000.0;
region.availableHuntSeconds = std::max(0.0, huntDurationSeconds - region.estimatedTravelSeconds);
region.staminaExperienceMultiplier = projectedStaminaExperienceMultiplier(player, region.availableHuntSeconds);
@@ -343,7 +350,7 @@ uint64_t PlayerBotHuntRegionPlanner::getCacheRevision()
return huntRegionCacheRevision;
}
-PlayerBotHuntRegionScan PlayerBotHuntRegionPlanner::beginScan() const
+PlayerBotHuntRegionScan PlayerBotHuntRegionPlanner::beginScan(const Player& player) const
{
PlayerBotHuntRegionScan scan;
scan.cacheHit = huntRegionCache.initialized && huntRegionCache.generation == g_game.map.spawns.getGeneration();
@@ -351,7 +358,21 @@ PlayerBotHuntRegionScan PlayerBotHuntRegionPlanner::beginScan() const
buildHuntRegionCache(scan.snapshotTimeUs, scan.clusteringTimeUs);
}
scan.revision = huntRegionCacheRevision;
- scan.candidateCount = huntRegionCache.regions.size();
+ const Position& templePosition = player.getTemplePosition();
+ for (size_t index = 0; index < huntRegionCache.regions.size(); ++index) {
+ const Position& center = huntRegionCache.regions[index].center;
+ const uint32_t templeDistance = Position::getDistanceX(templePosition, center) +
+ Position::getDistanceY(templePosition, center) +
+ Position::getDistanceZ(templePosition, center) * 20;
+ const uint32_t travelDistance = Position::getDistanceX(player.getPosition(), center) +
+ Position::getDistanceY(player.getPosition(), center) +
+ Position::getDistanceZ(player.getPosition(), center) * 20;
+ if (templeDistance <= maximumHuntDistanceFromTemple + maximumRegionDistancePadding &&
+ travelDistance <= maximumHuntTravelDistance + maximumRegionDistancePadding) {
+ scan.candidateIndices.push_back(index);
+ }
+ }
+ scan.candidateCount = scan.candidateIndices.size();
return scan;
}
diff --git a/server/src/playerbothuntregions.h b/server/src/playerbothuntregions.h
index 6e67453..c53433a 100644
--- a/server/src/playerbothuntregions.h
+++ b/server/src/playerbothuntregions.h
@@ -67,6 +67,7 @@ struct PlayerBotHuntRegionScan {
uint64_t snapshotTimeUs = 0;
uint64_t clusteringTimeUs = 0;
size_t candidateCount = 0;
+ std::vector candidateIndices;
};
class PlayerBotHuntRegionPlanner
@@ -74,7 +75,7 @@ class PlayerBotHuntRegionPlanner
public:
static void invalidateCache();
static uint64_t getCacheRevision();
- PlayerBotHuntRegionScan beginScan() const;
+ PlayerBotHuntRegionScan beginScan(const Player& player) const;
bool score(Player& player, uint64_t revision, size_t candidateIndex, const std::set& excludedRegions,
const std::map& performance,
uint32_t huntDurationSeconds, PlayerBotHuntRegion& region) const;
diff --git a/server/src/playerbotnavigation.cpp b/server/src/playerbotnavigation.cpp
index 42a0e79..bcd0154 100644
--- a/server/src/playerbotnavigation.cpp
+++ b/server/src/playerbotnavigation.cpp
@@ -17,7 +17,6 @@
#include "house.h"
#include "item.h"
#include "player.h"
-#include "teleport.h"
#include "tile.h"
#include
@@ -71,20 +70,6 @@ namespace {
return tile && tile->queryAdd(0, player, 1, flags) == RETURNVALUE_NOERROR;
}
- Position resolveTeleportDestination(Position position)
- {
- std::set visited;
- while (visited.insert(position).second) {
- Tile* tile = g_game.map.getTile(position);
- Teleport* teleport = tile ? tile->getTeleportItem() : nullptr;
- if (!teleport) {
- break;
- }
- position = teleport->getDestPos();
- }
- return position;
- }
-
bool resolveWalk(Player& player, const Position& from, Direction direction,
const std::set& blockedPositions, Position& destination)
{
@@ -127,7 +112,7 @@ namespace {
}
Tile* tile = g_game.map.getTile(destination);
- if (!canOccupy(player, tile, flags)) {
+ if (!canOccupy(player, tile, flags) || tile->getTeleportItem()) {
return false;
}
@@ -139,9 +124,12 @@ namespace {
if (!nextTile || nextTile == tile) {
break;
}
+ if (nextTile->getTeleportItem()) {
+ return false;
+ }
tile = nextTile;
}
- destination = resolveTeleportDestination(tile->getPosition());
+ destination = tile->getPosition();
return g_game.map.getTile(destination) != nullptr && blockedPositions.find(destination) == blockedPositions.end();
}
diff --git a/server/src/playerbotservice.cpp b/server/src/playerbotservice.cpp
index c9d2339..c46acf3 100644
--- a/server/src/playerbotservice.cpp
+++ b/server/src/playerbotservice.cpp
@@ -18,6 +18,10 @@
// NPC service discovery, shopping, banking, and depot handling.
using namespace playerbot;
+namespace {
+ constexpr uint32_t maximumServiceDistanceFromTemple = 200;
+}
+
const char* PlayerBotController::cyclePhaseName() const
{
switch (cyclePhase) {
@@ -82,7 +86,8 @@ void PlayerBotController::onNpcReply(uint32_t replyingPlayerId, uint32_t npcId,
void PlayerBotController::beginService(Player* player, const Position& position, const char* reason)
{
- const bool interruptedHunt = testPolicy.progressionEnabled && activeGoal == TopLevelGoal::Hunt;
+ const bool interruptedHunt = testPolicy.progressionEnabled && activeGoal == TopLevelGoal::Hunt &&
+ !hasCompletedRookgaardDeparture(*player);
finishHuntRegion(*player, position, reason);
if (interruptedHunt) {
emit("goal_result", position,
@@ -131,10 +136,12 @@ void PlayerBotController::finishHuntAndSelectGoal(Player* player, const Position
void PlayerBotController::discoverServices(const Position& position)
{
refreshItemValues();
+ Player* player = g_game.getPlayerByID(playerId);
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) {
+ if (!capability || !player ||
+ serviceDistance(player->getTemplePosition(), {npc->getID(), npc->getPosition()}) > maximumServiceDistanceFromTemple) {
continue;
}
std::vector* services = *capability == "shop" ? &serviceShops :
@@ -961,7 +968,7 @@ void PlayerBotController::processFixtureDeposit(Player* player, const Position&
}
emit("action_result", currentPosition, "\"action\":\"deposit\",\"result\":\"complete\",\"fixture\":true,\"cycle\":" +
std::to_string(completedCycles));
- if (testPolicy.progressionEnabled) {
+ if (testPolicy.progressionEnabled && !hasCompletedRookgaardDeparture(*player)) {
selectTopLevelGoal(*player, currentPosition, "fixture_deposit_complete");
} else {
startHunt(player, currentPosition, "fixture_deposit_complete");
@@ -1078,7 +1085,7 @@ void PlayerBotController::processDeposit(Player* player, const Position& current
if (pauseDepotFixtureForRestart(*player, DepotRestartCheckpoint::Depart, currentPosition)) {
return;
}
- if (testPolicy.progressionEnabled) {
+ if (testPolicy.progressionEnabled && !hasCompletedRookgaardDeparture(*player)) {
emit("goal_result", currentPosition,
"\"decision_id\":" + std::to_string(goalDecisionId) +
",\"goal\":\"service\",\"result\":\"success\",\"reason\":\"service_complete\"");
diff --git a/server/tests/playerbot-gameplay/playerbot_gameplay.lua b/server/tests/playerbot-gameplay/playerbot_gameplay.lua
index 667485e..9404c11 100644
--- a/server/tests/playerbot-gameplay/playerbot_gameplay.lua
+++ b/server/tests/playerbot-gameplay/playerbot_gameplay.lua
@@ -28,8 +28,40 @@ local economicRewardStorage = 50082
local departureRecoveryStorage = 50090
local depotFixtureStorage = 50095
local deathLoginCount = 0
+local removeAll
-local function removeAll(player, itemId)
+local function restoreRookgaardBaseline(player)
+ local armor = player:getSlotItem(CONST_SLOT_ARMOR)
+ if player:getLevel() ~= 8 or player:getVocation():getId() ~= 4 or not armor or armor:getId() ~= 2463 then
+ return
+ end
+
+ assert(player:setVocation(0), "fixture could not restore the vocationless baseline")
+ local experience = player:getExperience()
+ if experience > 0 then
+ assert(player:removeExperience(experience), "fixture could not restore level 1")
+ end
+ local rookgaard = Town(6)
+ assert(rookgaard and player:setTown(rookgaard), "fixture could not restore Rookgaard")
+ assert(player:teleportTo(rookgaard:getTemplePosition()), "fixture could not reach the Rookgaard temple")
+ for _, slot in ipairs({CONST_SLOT_HEAD, CONST_SLOT_ARMOR, CONST_SLOT_RIGHT, CONST_SLOT_LEFT, CONST_SLOT_LEGS, CONST_SLOT_FEET}) do
+ local item = player:getSlotItem(slot)
+ if item then
+ assert(item:remove(), "fixture could not clear equipment slot " .. slot)
+ end
+ end
+ for _, itemId in ipairs({2457, 2463, 2525, 2376, 2647, 2643}) do
+ removeAll(player, itemId)
+ end
+ assert(player:addItem(starterArmorId, 1, false, 1, CONST_SLOT_ARMOR), "fixture could not restore starter armor")
+ assert(player:addItem(starterWeaponId, 1, false, 1, CONST_SLOT_LEFT), "fixture could not restore starter weapon")
+ assert(player:getSlotItem(CONST_SLOT_ARMOR) and player:getSlotItem(CONST_SLOT_ARMOR):getId() == starterArmorId,
+ "fixture did not equip starter armor")
+ assert(player:getSlotItem(CONST_SLOT_LEFT) and player:getSlotItem(CONST_SLOT_LEFT):getId() == starterWeaponId,
+ "fixture did not equip starter weapon")
+end
+
+removeAll = function(player, itemId)
local count = player:getItemCount(itemId)
if count > 0 then
assert(player:removeItem(itemId, count), "fixture could not remove item " .. itemId)
@@ -389,7 +421,7 @@ function login.onLogin(player)
end
local mode = os.getenv("PLAYERBOT_GAMEPLAY_MODE") or "cycle"
- assert(mode == "cycle" or mode == "depot" or mode == "navigation" or mode == "corpse" or mode == "death" or mode == "healing" or
+ assert(mode == "mainland" or mode == "cycle" or mode == "depot" or mode == "navigation" or mode == "corpse" or mode == "death" or mode == "healing" or
mode == "healing_resupply" or mode == "value" or mode == "progression" or mode == "progression_bundle" or
mode == "progression_nested" or
mode == "progression_resume" or mode == "progression_nested_resume" or mode == "progression_space" or
@@ -400,6 +432,18 @@ function login.onLogin(player)
mode == "readiness_upgrade" or mode == "readiness_missing_weapon" or mode == "readiness_supplies" or
mode == "readiness_retention",
"unknown PLAYERBOT_GAMEPLAY_MODE: " .. mode)
+ if mode == "mainland" then
+ local town = player:getTown()
+ assert(player:getLevel() == 8 and player:getVocation():getId() == 4 and town and town:getId() == thaisTownId,
+ "mainland fixture did not load the level-8 Thais Knight")
+ assert(player:getSlotItem(CONST_SLOT_ARMOR) and player:getSlotItem(CONST_SLOT_ARMOR):getId() == 2463,
+ "mainland fixture did not load plate armor")
+ local backpack = player:getSlotItem(CONST_SLOT_BACKPACK)
+ assert(backpack and backpack:addItem(2696, 1), "mainland fixture could not seed local-service loot")
+ print("PLAYERBOT_GAMEPLAY_TEST MAINLAND_START")
+ return true
+ end
+ restoreRookgaardBaseline(player)
if mode == "readiness_ready" or mode == "readiness_upgrade" or mode == "readiness_missing_weapon" or
mode == "readiness_supplies" or mode == "readiness_retention" then
assert(player:setVocation(4), "readiness fixture could not select Knight vocation")
@@ -484,12 +528,8 @@ function login.onLogin(player)
if mode == "departure" then
if player:getVocation():getId() ~= 0 then
local town = player:getTown()
- local position = player:getPosition()
- local temple = Town(2):getTemplePosition()
assert(player:getVocation():getId() == 4 and town and town:getId() == 2,
"Oracle departure restart restored the wrong vocation or town")
- assert(position.x == temple.x and position.y == temple.y and position.z == temple.z,
- "Oracle departure restart restored the wrong position")
print("PLAYERBOT_GAMEPLAY_TEST ORACLE_DEPARTURE_RESTART_PASS")
return true
end