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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@
# Local configuration and build output
.env
/server/build/
/artifacts/
142 changes: 140 additions & 2 deletions scripts/playerbot-gameplay/runtime.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ function Throw-WaitTimeout {
$status = & docker @composeArguments ps --all 2>&1
$logs = try { Get-ServerLogs } catch { "Server logs unavailable: $($_.Exception.Message)" }
$tail = (($logs -split "`r?`n") | Select-Object -Last 80) -join "`n"
throw "$Message`nScenario: $currentScenario`n--- compose status ---`n$($status -join "`n")`n--- server log tail ---`n$tail"
throw [System.TimeoutException]::new("$Message`nScenario: $currentScenario`n--- compose status ---`n$($status -join "`n")`n--- server log tail ---`n$tail")
}

function Wait-ForLog {
Expand Down Expand Up @@ -305,15 +305,153 @@ function Invoke-TimedStep {
}
}

function Add-ScenarioResult {
param(
[string]$Name,
[string]$Status,
[string]$ErrorMessage,
[string]$ArtifactPath
)

$duration = $timings[$Name]
$durationMilliseconds = if ($duration) { [int64]$duration.TotalMilliseconds } else { 0 }
[void]$script:scenarioResults.Add([pscustomobject][ordered]@{
Name = $Name
Status = $Status
DurationMilliseconds = $durationMilliseconds
Error = $ErrorMessage
ArtifactPath = $ArtifactPath
})
}

function Save-ScenarioFailureArtifacts {
param(
[string]$Name,
[string]$Status,
[System.Exception]$Exception,
[DateTime]$StartedAt
)

$safeName = $Name -replace '[^A-Za-z0-9_.-]', '_'
$directory = Join-Path (Join-Path $FailureArtifactsPath $scenarioRunId) $safeName
$collectionErrors = [System.Collections.Generic.List[string]]::new()
try {
[void][System.IO.Directory]::CreateDirectory($directory)
}
catch {
return "Artifact directory unavailable: $($_.Exception.Message)"
}

$serverLogs = ""
try {
Update-ServerLogs
$serverLogs = ((& docker @composeArguments logs --no-log-prefix server 2>&1) -join "`n")
if ($LASTEXITCODE -ne 0) {
throw "docker compose logs exited with code $LASTEXITCODE"
}
[System.IO.File]::WriteAllText((Join-Path $directory "server.log"), $serverLogs)
}
catch {
[void]$collectionErrors.Add("server.log: $($_.Exception.Message)")
try {
$serverLogs = Get-ServerLogs
[System.IO.File]::WriteAllText((Join-Path $directory "server.log"), $serverLogs)
}
catch {
[void]$collectionErrors.Add("server.log fallback: $($_.Exception.Message)")
}
}

try {
$composeStatus = (& docker @composeArguments ps --all 2>&1) -join "`n"
[System.IO.File]::WriteAllText((Join-Path $directory "compose-ps.txt"), $composeStatus)
}
catch {
[void]$collectionErrors.Add("compose-ps.txt: $($_.Exception.Message)")
}

try {
$eventLines = foreach ($event in ConvertFrom-PlayerbotLogs -Logs $serverLogs) {
$event | ConvertTo-Json -Compress -Depth 20
}
[System.IO.File]::WriteAllText((Join-Path $directory "playerbot-events.jsonl"), ($eventLines -join "`n"))
}
catch {
[void]$collectionErrors.Add("playerbot-events.jsonl: $($_.Exception.Message)")
}

try {
[System.IO.File]::WriteAllText((Join-Path $directory "failure.txt"), $Exception.ToString())
}
catch {
[void]$collectionErrors.Add("failure.txt: $($_.Exception.Message)")
}
$metadata = [pscustomobject][ordered]@{
Scenario = $Name
Status = $Status
StartedAtUtc = $StartedAt.ToUniversalTime().ToString("o")
FinishedAtUtc = [DateTime]::UtcNow.ToString("o")
TimeoutSeconds = $currentWaitTimeoutSeconds
ExceptionType = $Exception.GetType().FullName
ExceptionMessage = $Exception.Message
ContinueOnFailure = [bool]$ContinueOnFailure
KeepStack = [bool]$KeepStack
CollectionErrors = @($collectionErrors)
}
try {
[System.IO.File]::WriteAllText((Join-Path $directory "metadata.json"), ($metadata | ConvertTo-Json -Depth 10))
}
catch {
[void]$collectionErrors.Add("metadata.json: $($_.Exception.Message)")
}
return $directory
}

function Write-ScenarioSummary {
$passed = @($script:scenarioResults | Where-Object { $_.Status -eq "pass" }).Count
$failed = @($script:scenarioResults | Where-Object { $_.Status -eq "fail" }).Count
$timedOut = @($script:scenarioResults | Where-Object { $_.Status -eq "timeout" }).Count
$skipped = @($script:scenarioResults | Where-Object { $_.Status -eq "skipped" }).Count
"PLAYERBOT_GAMEPLAY_TEST SUMMARY pass=$passed fail=$failed timeout=$timedOut skipped=$skipped"
foreach ($result in $script:scenarioResults) {
$fields = "name=$($result.Name) status=$($result.Status) duration_ms=$($result.DurationMilliseconds)"
if ($result.ArtifactPath) {
$fields += " artifact=$($result.ArtifactPath)"
}
"PLAYERBOT_GAMEPLAY_TEST RESULT $fields"
}
if ($failed -gt 0 -or $timedOut -gt 0) {
"PLAYERBOT_GAMEPLAY_TEST FAIL"
} elseif ($passed -gt 0) {
"PLAYERBOT_GAMEPLAY_TEST PASS"
}
}

function Invoke-Scenario {
param(
[string]$Name,
[int]$DefaultTimeoutSeconds,
[scriptblock]$Body
)

if ($exactScenarioSelection -and -not $selectedScenarios.Contains($Name)) {
Add-ScenarioResult -Name $Name -Status "skipped"
return
}
$script:currentScenario = $Name
$script:currentWaitTimeoutSeconds = if ($timeoutOverridden) { $TimeoutSeconds } else { $DefaultTimeoutSeconds }
$script:currentScenarioDeadline = [DateTime]::UtcNow.AddSeconds($currentWaitTimeoutSeconds)
Invoke-TimedStep -Name $Name -Body $Body
$startedAt = [DateTime]::UtcNow
try {
Invoke-TimedStep -Name $Name -Body $Body
Add-ScenarioResult -Name $Name -Status "pass"
}
catch {
$status = if ($_.Exception -is [System.TimeoutException]) { "timeout" } else { "fail" }
$artifactPath = Save-ScenarioFailureArtifacts -Name $Name -Status $status -Exception $_.Exception -StartedAt $startedAt
Add-ScenarioResult -Name $Name -Status $status -ErrorMessage $_.Exception.Message -ArtifactPath $artifactPath
if (-not $ContinueOnFailure) {
throw
}
}
}
4 changes: 2 additions & 2 deletions scripts/playerbot-gameplay/scenarios-progression.ps1
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
if ($PickupProgression) {
Invoke-Scenario -Name "pickup_progression" -DefaultTimeoutSeconds 180 -Body {
Invoke-Scenario -Name "pickup_progression" -DefaultTimeoutSeconds 300 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "progression"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "900"
Expand Down Expand Up @@ -333,7 +333,7 @@
}

if ($MainlandRewards) {
Invoke-Scenario -Name "mainland_equipment_reward" -DefaultTimeoutSeconds 240 -Body {
Invoke-Scenario -Name "mainland_equipment_reward" -DefaultTimeoutSeconds 300 -Body {
Invoke-Compose down --volumes --remove-orphans
$env:PLAYERBOT_GAMEPLAY_MODE = "mainland_reward"
$env:PLAYERBOT_HUNT_DURATION_SECONDS = "900"
Expand Down
23 changes: 4 additions & 19 deletions scripts/playerbot-gameplay/scenarios-service.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,8 @@

Invoke-Compose stop server
Invoke-Compose up --detach server
$restartLogs = ""
for ($attempt = 0; $attempt -lt 300; $attempt++) {
Start-Sleep -Seconds 1
$restartLogs = Get-LatestServerGenerationLogs -Logs (Get-ServerLogs)
if ($restartLogs -match '"action":"deposit","result":"complete","depot_id":2' -and
$restartLogs -match '"action":"hunt_cycle","result":"started","cycle":1') { break }
}
Wait-ForLatestServerGenerationLog -Pattern '"action":"deposit","result":"complete","depot_id":2' | Out-Null
$restartLogs = Wait-ForLatestServerGenerationLog -Pattern '"action":"hunt_cycle","result":"started","cycle":1'
Assert-MainlandLoopEvents -Logs $restartLogs -MinimumCycles 1 -MinimumDeposits 1
}
}
Expand Down Expand Up @@ -72,12 +67,7 @@

Invoke-Compose stop server
Invoke-Compose up --detach server
$secondCycleLogs = ""
for ($attempt = 0; $attempt -lt 180; $attempt++) {
Start-Sleep -Seconds 1
$secondCycleLogs = Get-LatestServerGenerationLogs -Logs (Get-ServerLogs)
if ($secondCycleLogs -match '"action":"deposit","result":"complete"') { break }
}
$secondCycleLogs = Wait-ForLatestServerGenerationLog -Pattern '"action":"deposit","result":"complete"'
Assert-DepotEvents -Logs $secondCycleLogs -ExpectedDepositedCount 1 -ExpectedEquipmentDeposits 0
$sentinelCount = Invoke-DatabaseScalar -Query "SELECT COALESCE(SUM(count), 0) FROM player_depotitems JOIN players ON players.id = player_depotitems.player_id WHERE players.name = 'Rook Tester' AND pid = 2 AND itemtype = 2684"
if ($sentinelCount -ne 7) {
Expand All @@ -103,12 +93,7 @@
}
Invoke-Compose stop server
Invoke-Compose up --detach server
$recoveryLogs = ""
for ($attempt = 0; $attempt -lt 150; $attempt++) {
Start-Sleep -Seconds 1
$recoveryLogs = Get-LatestServerGenerationLogs -Logs (Get-ServerLogs)
if ($recoveryLogs -match '"action":"deposit","result":"complete"') { break }
}
$recoveryLogs = Wait-ForLatestServerGenerationLog -Pattern '"action":"deposit","result":"complete"'
Assert-DepotRecoveryEvents -Logs $recoveryLogs -Phase $phase
}
}
Expand Down
72 changes: 71 additions & 1 deletion scripts/test-playerbot-gameplay.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ fixtures, and removes the scenario stack unless -KeepStack is set.
.PARAMETER SlottedLoot
Runs seller, no-eligible-seller depot fallback, and interrupted-deposit restart
fixtures for policy-approved loot carried in an invalid equipment slot.

.PARAMETER Scenario
Runs only the named scenarios. Names must match the gameplay scenario catalog.

.PARAMETER ContinueOnFailure
Captures diagnostics and continues with the next selected scenario after a failure.
#>
param(
[ValidateRange(30, 3600)]
Expand Down Expand Up @@ -43,6 +49,9 @@ param(
"magic_training_post_hunt", "magic_training_post_hunt_no_overflow", "magic_training_restart", "magic_training_hunt"
)]
[string]$MagicTrainingCase,
[string[]]$Scenario,
[switch]$ContinueOnFailure,
[string]$FailureArtifactsPath,
[switch]$Focused,
[switch]$SkipBuild,
[switch]$KeepStack
Expand All @@ -54,6 +63,54 @@ $projectRoot = Split-Path -Parent $PSScriptRoot
$composeFile = Join-Path $projectRoot "server\compose.yaml"
$gameplayComposeFile = Join-Path $projectRoot "server\compose.playerbot-gameplay.yaml"
$composeArguments = @("compose", "-f", $composeFile, "-f", $gameplayComposeFile)
$scenarioCatalog = @(
"cycle",
"mainland_loop", "slotted_loot_seller", "slotted_loot_no_seller", "slotted_loot_deposit_restart",
"real_depot", "real_depot_restart_approach", "real_depot_restart_locker", "real_depot_restart_chest",
"real_depot_restart_deposit", "real_depot_restart_depart", "real_depot_partial_move", "real_depot_rejected_move",
"pickup_progression", "pickup_progression_bundle", "pickup_progression_nested", "pickup_progression_resume",
"pickup_progression_nested_resume", "pickup_progression_space", "goal_arbitration", "goal_arbitration_interrupt",
"stamina_bonus_projection", "stamina_boundary_projection", "stamina_normal_projection", "hunt_region_planning",
"combat_readiness_ready", "combat_readiness_upgrade", "combat_readiness_missing_weapon", "combat_readiness_supplies",
"combat_readiness_no_food", "combat_readiness_low_wealth", "combat_readiness_food_capacity",
"combat_readiness_retention", "equipment_offer_shadow_upgrade", "equipment_offer_shadow_unaffordable",
"equipment_offer_shadow_no_upgrade", "equipment_purchase", "equipment_purchase_resume", "equipment_purchase_space",
"equipment_purchase_rejected", "adaptive_challenge", "mainland_equipment_reward", "oracle_departure",
"oracle_level_eight_interrupt", "oracle_level_eight_recovery",
"navigation", "navigation_recovery", "patrol_recovery", "target_pursuit", "target_pursuit_abandon",
"spell_training", "spell_use", "spell_calibration", "magic_training_haste", "magic_training_great_light",
"magic_training_light", "magic_training_refresh", "magic_training_reserve", "magic_training_exact_full",
"magic_training_pz", "magic_training_absent", "magic_training_expired", "magic_training_failed",
"magic_training_service", "magic_training_progression", "magic_training_post_hunt",
"magic_training_post_hunt_no_overflow", "magic_training_restart", "magic_training_hunt",
"corpse", "corpse_inaccessible", "death", "healing", "healing_resupply", "value"
)
$scenarioCatalogSet = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($scenarioName in $scenarioCatalog) {
if (-not $scenarioCatalogSet.Add($scenarioName)) {
throw "Duplicate gameplay scenario name: $scenarioName"
}
}
if ($scenarioCatalog.Count -ne 75) {
throw "The gameplay scenario catalog must contain 75 scenarios; found $($scenarioCatalog.Count)."
}
$requestedScenarioNames = @($Scenario | ForEach-Object { $_ -split ',' } | Where-Object { $_ })
$exactScenarioSelection = $requestedScenarioNames.Count -gt 0
$selectedScenarios = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($scenarioName in $requestedScenarioNames) {
if (-not $scenarioCatalogSet.Contains($scenarioName)) {
throw "Unknown gameplay scenario '$scenarioName'."
}
[void]$selectedScenarios.Add($scenarioName)
}
if ($exactScenarioSelection -and ($Focused -or $MagicTrainingCase)) {
throw "-Scenario cannot be combined with -Focused or -MagicTrainingCase."
}
if (-not $FailureArtifactsPath) {
$FailureArtifactsPath = Join-Path $projectRoot "artifacts\playerbot-gameplay"
}
$scenarioResults = [System.Collections.Generic.List[object]]::new()
$scenarioRunId = [DateTime]::UtcNow.ToString("yyyyMMddTHHmmssZ")
$previousDuration = $env:PLAYERBOT_HUNT_DURATION_SECONDS
$previousMode = $env:PLAYERBOT_GAMEPLAY_MODE
$previousRelogDelay = $env:PLAYERBOT_RELOG_DELAY_SECONDS
Expand Down Expand Up @@ -137,7 +194,19 @@ try {
. $PSScriptRoot/playerbot-gameplay/scenarios-spells.ps1

. $PSScriptRoot/playerbot-gameplay/scenarios-combat-loot.ps1
"PLAYERBOT_GAMEPLAY_TEST PASS"
if (-not $Focused) {
$resultNames = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
foreach ($result in $scenarioResults) {
[void]$resultNames.Add($result.Name)
}
if ($scenarioResults.Count -ne $scenarioCatalog.Count -or -not $resultNames.SetEquals($scenarioCatalogSet)) {
throw "The gameplay scenario catalog does not match the scenarios enumerated by the suite."
}
}
$failedScenarios = @($scenarioResults | Where-Object { $_.Status -in @("fail", "timeout") })
if ($failedScenarios.Count -gt 0) {
throw "$($failedScenarios.Count) gameplay scenario(s) failed."
}
}
finally {
try {
Expand All @@ -159,5 +228,6 @@ finally {
foreach ($timing in $timings.GetEnumerator()) {
"PLAYERBOT_GAMEPLAY_TIMING $($timing.Key)=$([Math]::Round($timing.Value.TotalSeconds, 2))s"
}
Write-ScenarioSummary
}
}
2 changes: 2 additions & 0 deletions server/.dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ build
vc14
vc16
db-volume
data/logs/*.log
data/logs/stats/*.log
1 change: 1 addition & 0 deletions server/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ RUN --mount=type=cache,target=/root/.cache/ccache \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_FLAGS=-fpch-preprocess \
-DDISABLE_STATS=1 \
-DSKIP_GIT=ON \
-DUSE_LUAJIT=ON \
&& cmake --build build --parallel 2 \
Expand Down
1 change: 1 addition & 0 deletions server/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ set(tfs_SRC
${CMAKE_CURRENT_LIST_DIR}/playerbotdeparture.cpp
${CMAKE_CURRENT_LIST_DIR}/playerbotequipment.cpp
${CMAKE_CURRENT_LIST_DIR}/playerbotequipmentpolicy.cpp
${CMAKE_CURRENT_LIST_DIR}/playerbotequipmentadapter.cpp
${CMAKE_CURRENT_LIST_DIR}/playerboteconomy.cpp
${CMAKE_CURRENT_LIST_DIR}/playerbotgoalarbiter.cpp
${CMAKE_CURRENT_LIST_DIR}/playerbotgoalplanner.cpp
Expand Down
Loading