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