Recover non-progressing cast loops and bound host model prompt cache
Core tests / core (push) Successful in 57s
Core tests / core (push) Successful in 57s
This commit is contained in:
+64
-15
@@ -1,6 +1,11 @@
|
|||||||
-- Conservative recovery for managed residents with fresh quest/explore intent.
|
-- Conservative recovery for managed residents with fresh quest/explore intent.
|
||||||
-- Native position/progress is sampled independently of dashboard summaries.
|
-- Native position/progress is sampled independently of dashboard summaries.
|
||||||
local intents, observations, pendingResults = {}, {}, {}
|
local intents, observations, pendingResults = {}, {}, {}
|
||||||
|
local progress={}
|
||||||
|
-- Successful gameplay, rather than merely starting a cast, resets the idle clock.
|
||||||
|
for _,event in ipairs({32,37,54}) do
|
||||||
|
RegisterPlayerEvent(event,function(_,p) if p:IsPlayerbot() then progress[p:GetGUIDLow()]=os.time() end end)
|
||||||
|
end
|
||||||
local LIMIT = 600
|
local LIMIT = 600
|
||||||
local COOLDOWN = 1800
|
local COOLDOWN = 1800
|
||||||
local function sqltext(s)
|
local function sqltext(s)
|
||||||
@@ -13,6 +18,22 @@ AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_recovery_log (
|
|||||||
AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_recovery_results (
|
AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_recovery_results (
|
||||||
attempt_id BIGINT UNSIGNED PRIMARY KEY,status VARCHAR(24) NOT NULL,
|
attempt_id BIGINT UNSIGNED PRIMARY KEY,status VARCHAR(24) NOT NULL,
|
||||||
checked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,detail VARCHAR(255) NOT NULL)]])
|
checked_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,detail VARCHAR(255) NOT NULL)]])
|
||||||
|
AuthDBExecute([[CREATE TABLE IF NOT EXISTS azerothai_recovery_leases (
|
||||||
|
guid INT UNSIGNED PRIMARY KEY,name VARCHAR(64),account_id INT UNSIGNED,
|
||||||
|
restore_at BIGINT UNSIGNED,gathering TINYINT,looting TINYINT)]])
|
||||||
|
local function restoreStrategies(now)
|
||||||
|
local q=AuthDBQuery('SELECT guid,name,account_id,gathering,looting FROM azerothai_recovery_leases WHERE restore_at<='..now)
|
||||||
|
if not q then return end
|
||||||
|
repeat
|
||||||
|
local guid,name,account=q:GetUInt32(0),q:GetString(1),q:GetUInt32(2)
|
||||||
|
local p=GetPlayerByName(name)
|
||||||
|
if p and p:IsPlayerbot() and p:GetGUIDLow()==guid and p:GetAccountId()==account and p:GetBotAI() and not p:GetBotAI():HasGameClientMaster() then
|
||||||
|
if q:GetUInt32(3)==1 then p:GetBotAI():ChangeStrategy('+gather',1) end
|
||||||
|
if q:GetUInt32(4)==1 then p:GetBotAI():ChangeStrategy('+loot',1) end
|
||||||
|
AuthDBExecute('DELETE FROM azerothai_recovery_leases WHERE guid='..guid)
|
||||||
|
end
|
||||||
|
until not q:NextRow()
|
||||||
|
end
|
||||||
local function checkResults(now)
|
local function checkResults(now)
|
||||||
for guid,r in pairs(pendingResults) do
|
for guid,r in pairs(pendingResults) do
|
||||||
local p=GetPlayerByName(r.name);local status,detail
|
local p=GetPlayerByName(r.name);local status,detail
|
||||||
@@ -39,17 +60,55 @@ function AzerothAISetRecoveryIntents(rows)
|
|||||||
return 'Recovery intentions refreshed'
|
return 'Recovery intentions refreshed'
|
||||||
end
|
end
|
||||||
local function safe(p,a)
|
local function safe(p,a)
|
||||||
if p:IsDead() or p:IsInCombat() or p:IsCasting() or p:IsTaxi() or a:HasGameClientMaster()
|
if p:IsDead() or p:IsInCombat() or p:IsTaxi() or a:HasGameClientMaster()
|
||||||
or p:IsRooted() or a:IsMovementImpaired() then return false end
|
or p:IsRooted() or a:IsMovementImpaired() then return false end
|
||||||
local g=p:GetGroup()
|
local g=p:GetGroup()
|
||||||
if g then
|
if g then
|
||||||
if #g:GetMembers()~=g:GetMembersCount() then return false end
|
if #g:GetMembers()~=g:GetMembersCount() then return false end
|
||||||
for _,m in pairs(g:GetMembers()) do if not m:IsPlayerbot() then return false end end
|
for _,m in pairs(g:GetMembers()) do if not m:IsPlayerbot() or not m:GetBotAI() or m:GetBotAI():HasGameClientMaster() then return false end end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
local function recover(p,a,r,guid,now,followup)
|
||||||
|
local x,y,z,map,level=p:GetX(),p:GetY(),p:GetZ(),p:GetMapId(),p:GetLevel()
|
||||||
|
local q=AuthDBQuery('SELECT guid FROM azerothai_lifecycle WHERE guid='..guid..' AND account_id='..r.account..' AND BINARY name='..sqltext(r.name).." AND state='active'")
|
||||||
|
local recent=AuthDBQuery('SELECT id FROM azerothai_recovery_log WHERE guid='..guid..' AND at>NOW()-INTERVAL '..COOLDOWN..' SECOND LIMIT 1')
|
||||||
|
if recent and followup then
|
||||||
|
local failed=AuthDBQuery("SELECT l.id FROM azerothai_recovery_log l JOIN azerothai_recovery_results r ON r.attempt_id=l.id WHERE l.guid="..guid.." AND r.status='no_progress' AND l.at>NOW()-INTERVAL 1800 SECOND AND (SELECT COUNT(*) FROM azerothai_recovery_log x WHERE x.guid=l.guid AND x.at>NOW()-INTERVAL 1800 SECOND)=1 LIMIT 1")
|
||||||
|
if failed then recent=nil end
|
||||||
|
end
|
||||||
|
if q and not recent then
|
||||||
|
local interrupted=p:IsCasting()
|
||||||
|
if interrupted then p:StopSpellCast() end
|
||||||
|
local staleFlight=p:GetMovementType()==7 and not p:IsTaxi()
|
||||||
|
if staleFlight then p:MoveExpire();p:MoveClear() end
|
||||||
|
a:Reset(true)
|
||||||
|
local gathering,looting=a:HasStrategy('gather',1),a:HasStrategy('loot',1)
|
||||||
|
if gathering or looting then
|
||||||
|
AuthDBExecute('INSERT INTO azerothai_recovery_leases VALUES ('..guid..','..sqltext(r.name)..','..r.account..','..(now+120)..','..(gathering and 1 or 0)..','..(looting and 1 or 0)..') ON DUPLICATE KEY UPDATE restore_at=VALUES(restore_at),gathering=GREATEST(gathering,VALUES(gathering)),looting=GREATEST(looting,VALUES(looting))')
|
||||||
|
a:ChangeStrategy('-gather',1);a:ChangeStrategy('-loot',1)
|
||||||
|
a:DoSpecificAction('release loot',true)
|
||||||
|
end
|
||||||
|
local moved=a:DoSpecificAction('move random',true)
|
||||||
|
local detail=(interrupted and 'Interrupted non-progressing cast; ' or '')..(staleFlight and 'Cleared stale flight; ' or '')..'AI replanned; gathering paused 120s; walking request='..tostring(moved)
|
||||||
|
AuthDBExecute('INSERT INTO azerothai_recovery_log(guid,name,detail) VALUES ('..guid..','..sqltext(r.name)..','..sqltext(detail)..')')
|
||||||
|
pendingResults[guid]={name=r.name,account=r.account,at=now,x=x,y=y,z=z,map=map,level=level}
|
||||||
|
PrintInfo('[AzerothAI recovery] '..r.name..': '..detail)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
-- Operator-only entry point for a stall already verified through repeated observations.
|
||||||
|
function AzerothAIRecoverObservedStall(name,guid,account)
|
||||||
|
local p=GetPlayerByName(name)
|
||||||
|
if not p or not p:IsPlayerbot() or p:GetGUIDLow()~=guid or p:GetAccountId()~=account then return 'Identity unavailable' end
|
||||||
|
local a=p:GetBotAI()
|
||||||
|
if not a or not safe(p,a) then return 'Unsafe to recover now' end
|
||||||
|
return recover(p,a,{name=name,account=account},guid,os.time(),true) and 'Recovery requested; awaiting observed progress' or 'Inactive identity or cooldown'
|
||||||
|
end
|
||||||
local function sample()
|
local function sample()
|
||||||
local now=os.time()
|
local now=os.time()
|
||||||
|
restoreStrategies(now)
|
||||||
checkResults(now)
|
checkResults(now)
|
||||||
for guid,r in pairs(intents) do
|
for guid,r in pairs(intents) do
|
||||||
local p=GetPlayerByName(r.name)
|
local p=GetPlayerByName(r.name)
|
||||||
@@ -62,22 +121,11 @@ local function sample()
|
|||||||
local x,y,z,map=p:GetX(),p:GetY(),p:GetZ(),p:GetMapId()
|
local x,y,z,map=p:GetX(),p:GetY(),p:GetZ(),p:GetMapId()
|
||||||
local level,hp=p:GetLevel(),p:GetHealth()
|
local level,hp=p:GetLevel(),p:GetHealth()
|
||||||
local old=observations[guid]
|
local old=observations[guid]
|
||||||
if not old or old.map~=map or old.level~=level or old.hp~=hp or (x-old.x)^2+(y-old.y)^2+(z-old.z)^2>25 then
|
if not old or old.map~=map or old.level~=level or old.hp~=hp or (progress[guid] or 0)>old.since or (x-old.x)^2+(y-old.y)^2+(z-old.z)^2>25 then
|
||||||
observations[guid]={x=x,y=y,z=z,map=map,level=level,hp=hp,since=now}
|
observations[guid]={x=x,y=y,z=z,map=map,level=level,hp=hp,since=now}
|
||||||
elseif now-old.since>=LIMIT then
|
elseif now-old.since>=LIMIT then
|
||||||
-- Persistent cooldown survives ALE reloads; exact identity must still be active.
|
-- Persistent cooldown survives ALE reloads; exact identity must still be active.
|
||||||
local q=AuthDBQuery('SELECT guid FROM azerothai_lifecycle WHERE guid='..guid..' AND account_id='..r.account..' AND BINARY name='..sqltext(r.name).." AND state='active'")
|
recover(p,a,r,guid,now)
|
||||||
local recent=AuthDBQuery('SELECT id FROM azerothai_recovery_log WHERE guid='..guid..' AND at>NOW()-INTERVAL '..COOLDOWN..' SECOND LIMIT 1')
|
|
||||||
if q and not recent then
|
|
||||||
local staleFlight=p:GetMovementType()==7 and not p:IsTaxi()
|
|
||||||
if staleFlight then p:MoveExpire();p:MoveClear() end
|
|
||||||
a:Reset(true)
|
|
||||||
local moved=a:DoSpecificAction('move random',true)
|
|
||||||
local detail=(staleFlight and 'Cleared stale flight; ' or '')..'AI replanned; walking request='..tostring(moved)
|
|
||||||
AuthDBExecute('INSERT INTO azerothai_recovery_log(guid,name,detail) VALUES ('..guid..','..sqltext(r.name)..','..sqltext(detail)..')')
|
|
||||||
pendingResults[guid]={name=r.name,account=r.account,at=now,x=x,y=y,z=z,map=map,level=level}
|
|
||||||
PrintInfo('[AzerothAI recovery] '..r.name..': '..detail)
|
|
||||||
end
|
|
||||||
old.since=now
|
old.since=now
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -85,6 +133,7 @@ local function sample()
|
|||||||
end
|
end
|
||||||
-- Forget residents whose intent changed to resting/socializing or who went offline.
|
-- Forget residents whose intent changed to resting/socializing or who went offline.
|
||||||
for guid in pairs(observations) do if not intents[guid] then observations[guid]=nil end end
|
for guid in pairs(observations) do if not intents[guid] then observations[guid]=nil end end
|
||||||
|
for guid,at in pairs(progress) do if now-at>3600 then progress[guid]=nil end end
|
||||||
end
|
end
|
||||||
CreateLuaEvent(function()local ok,err=pcall(sample);if not ok then PrintError('[AzerothAI recovery] '..tostring(err)) end end,30000,0)
|
CreateLuaEvent(function()local ok,err=pcall(sample);if not ok then PrintError('[AzerothAI recovery] '..tostring(err)) end end,30000,0)
|
||||||
PrintInfo('[AzerothAI] Conservative stuck recovery enabled: 10 minutes idle, 30 minute cooldown')
|
PrintInfo('[AzerothAI] Conservative stuck recovery enabled: 10 minutes idle, 30 minute cooldown')
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Bound the bundled llama-server prompt cache; preserve model and context size.
|
||||||
|
param([switch]$Restart)
|
||||||
|
$ErrorActionPreference='Stop'
|
||||||
|
$settings=@{LLAMA_ARG_CACHE_RAM='512';LLAMA_ARG_CTX_CHECKPOINTS='4'}
|
||||||
|
foreach($key in $settings.Keys){[Environment]::SetEnvironmentVariable($key,$settings[$key],'User');[Environment]::SetEnvironmentVariable($key,$settings[$key],'Process')}
|
||||||
|
$root=Join-Path $env:USERPROFILE 'Tools\AzerothAI'
|
||||||
|
$supervisor=Join-Path $root 'recovery.ps1'
|
||||||
|
if(Test-Path $supervisor){
|
||||||
|
$text=Get-Content -Raw $supervisor
|
||||||
|
if($text -notmatch 'LLAMA_ARG_CACHE_RAM'){
|
||||||
|
Copy-Item $supervisor "$supervisor.before-cache-limit" -Force
|
||||||
|
$prefix='$env:LLAMA_ARG_CACHE_RAM="512"; $env:LLAMA_ARG_CTX_CHECKPOINTS="4"'
|
||||||
|
Set-Content $supervisor ($prefix+"`r`n"+$text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($Restart){
|
||||||
|
$task=Get-ScheduledTask -TaskName 'AzerothAI-Recovery' -ErrorAction SilentlyContinue
|
||||||
|
if($task){Stop-ScheduledTask -InputObject $task}
|
||||||
|
try{Invoke-RestMethod -Method Post -Uri http://127.0.0.1:11434/api/generate -ContentType 'application/json' -Body '{"model":"qwen3.5:4b","keep_alive":0}' -TimeoutSec 45 | Out-Null}catch{Write-Warning $_.Exception.Message}
|
||||||
|
$binary=Join-Path $env:USERPROFILE 'Tools\Ollama\ollama.exe'
|
||||||
|
Get-Process ollama -ErrorAction SilentlyContinue | Where-Object {$_.Path -eq $binary} | Stop-Process
|
||||||
|
$logs=Split-Path $binary
|
||||||
|
foreach($file in @('server.out.log','server.err.log')){if(Test-Path "$logs\$file"){Copy-Item "$logs\$file" "$logs\$file.before-cache-limit" -Force}}
|
||||||
|
Start-Process $binary -ArgumentList 'serve' -WindowStyle Hidden -RedirectStandardOutput "$logs\server.out.log" -RedirectStandardError "$logs\server.err.log"
|
||||||
|
if($task){Start-ScheduledTask -InputObject $task}
|
||||||
|
}
|
||||||
|
Write-Output 'Prompt cache capped at 512 MiB; at most four context checkpoints per slot.'
|
||||||
@@ -28,3 +28,8 @@ The native guard now samples every two seconds. Retreat is requested below 50% h
|
|||||||
Healthy followers can regroup over 12–200 yards on the same map/phase. Healthy noncombat leaders pause for at most 20 seconds per minute when a noncombat companion is 25–200 yards behind. Recovery requests now extend to 80% health after recent danger. No teleport, resurrection, population or stat changes are made.
|
Healthy followers can regroup over 12–200 yards on the same map/phase. Healthy noncombat leaders pause for at most 20 seconds per minute when a noncombat companion is 25–200 yards behind. Recovery requests now extend to 80% health after recent danger. No teleport, resurrection, population or stat changes are made.
|
||||||
|
|
||||||
The auth table `azerothai_survival_audit` records request acceptance and observed outcomes: out of combat, movement while still in combat, no movement after 30 seconds, or death during observation. Movement means at least five yards from the starting position (or a map change); it does not prove successful escape. Audit records expire after seven days in bounded hourly batches. Snapshots continue to expose the latest action and timestamp.
|
The auth table `azerothai_survival_audit` records request acceptance and observed outcomes: out of combat, movement while still in combat, no movement after 30 seconds, or death during observation. Movement means at least five yards from the starting position (or a map change); it does not prove successful escape. Audit records expire after seven days in bounded hourly batches. Snapshots continue to expose the latest action and timestamp.
|
||||||
|
|
||||||
|
|
||||||
|
Stall recovery keeps its ten-minute idle clock across casts. Real movement, level/health changes, successful loot or quest completion reset the clock. It can interrupt a noncombat cast only after that threshold; combat, taxi, human control, movement restrictions, fresh intentions and the persistent thirty-minute recovery cooldown remain enforced. Run `deploy/pc-lab/limit-model-memory.ps1 -Restart` on the Windows model host to cap prompt cache at 512 MiB and checkpoints at four. This trades cache reuse for host memory without changing model or context size.
|
||||||
|
|
||||||
|
Recovery briefly disables existing gather/loot strategies for two minutes after releasing loot, so an invalid gathering target cannot immediately cancel the walking request. A durable lease restores only previously enabled strategies, including after a Lua reload. An operator may make one follow-up attempt within the cooldown only when the sole recent attempt has a verified no-progress result; automatic attempts retain their thirty-minute cooldown.
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ code=(Path(__file__).resolve().parents[1]/'ale/azerothai_recovery.lua').read_tex
|
|||||||
setup='''
|
setup='''
|
||||||
now=1000;os.time=function()return now end;attempts=0;expired=0;last=nil;active=true;player={x=0};ai={};captured={}
|
now=1000;os.time=function()return now end;attempts=0;expired=0;last=nil;active=true;player={x=0};ai={};captured={}
|
||||||
function AuthDBExecute(sql) table.insert(captured,sql);if string.find(sql,'INSERT INTO azerothai_recovery_log',1,true) then last=now end end
|
function AuthDBExecute(sql) table.insert(captured,sql);if string.find(sql,'INSERT INTO azerothai_recovery_log',1,true) then last=now end end
|
||||||
function AuthDBQuery(sql) if string.find(sql,'recovery_log',1,true) then return last and now-last<1800 and {} or nil end;return active and {} or nil end
|
function AuthDBQuery(sql) if string.find(sql,'recovery_leases',1,true) then return nil end;if string.find(sql,'recovery_log',1,true) then return last and now-last<1800 and {} or nil end;return active and {} or nil end
|
||||||
function GetPlayerByName(n) return n=='Test' and player or nil end
|
function GetPlayerByName(n) return n=='Test' and player or nil end
|
||||||
|
hooks={};function RegisterPlayerEvent(n,fn)hooks[n]=fn end
|
||||||
function CreateLuaEvent(fn) tick=fn end
|
function CreateLuaEvent(fn) tick=fn end
|
||||||
function PrintInfo(s) end
|
function PrintInfo(s) end
|
||||||
function PrintError(s) error(s) end
|
function PrintError(s) error(s) end
|
||||||
@@ -16,6 +17,7 @@ function player:GetBotAI()return ai end
|
|||||||
function player:IsDead()return self.dead end
|
function player:IsDead()return self.dead end
|
||||||
function player:IsInCombat()return self.combat end
|
function player:IsInCombat()return self.combat end
|
||||||
function player:IsCasting()return self.casting end
|
function player:IsCasting()return self.casting end
|
||||||
|
function player:StopSpellCast()self.casting=false;self.interrupted=true end
|
||||||
function player:IsTaxi()return self.taxi end
|
function player:IsTaxi()return self.taxi end
|
||||||
function player:IsRooted()return self.root end
|
function player:IsRooted()return self.root end
|
||||||
function player:GetGroup()return self.group end
|
function player:GetGroup()return self.group end
|
||||||
@@ -31,7 +33,9 @@ function player:MoveClear()end
|
|||||||
function ai:HasGameClientMaster()return player.master end
|
function ai:HasGameClientMaster()return player.master end
|
||||||
function ai:IsMovementImpaired()return player.impaired end
|
function ai:IsMovementImpaired()return player.impaired end
|
||||||
function ai:Reset(full)attempts=attempts+1 end
|
function ai:Reset(full)attempts=attempts+1 end
|
||||||
function ai:DoSpecificAction(action,silent)assert(action=='move random');return true end
|
function ai:HasStrategy(s,state)return false end
|
||||||
|
function ai:ChangeStrategy()end
|
||||||
|
function ai:DoSpecificAction(action,silent)assert(action=='move random' or action=='release loot');return true end
|
||||||
function step(seconds,action)
|
function step(seconds,action)
|
||||||
now=now+seconds
|
now=now+seconds
|
||||||
AzerothAISetRecoveryIntents({{guid=9,account=2,name='Test',action=action or 'quest'}})
|
AzerothAISetRecoveryIntents({{guid=9,account=2,name='Test',action=action or 'quest'}})
|
||||||
@@ -42,7 +46,7 @@ def new():
|
|||||||
l=LuaRuntime();l.execute(setup);l.execute(code);return l
|
l=LuaRuntime();l.execute(setup);l.execute(code);return l
|
||||||
l=new();l.execute('step(0);step(599);assert(attempts==0);step(1);assert(attempts==1);step(600);assert(attempts==1);step(1200);assert(attempts==2)')
|
l=new();l.execute('step(0);step(599);assert(attempts==0);step(1);assert(attempts==1);step(600);assert(attempts==1);step(1200);assert(attempts==2)')
|
||||||
print('Threshold and persistent cooldown passed')
|
print('Threshold and persistent cooldown passed')
|
||||||
for field in ['dead','combat','casting','taxi','root','master','impaired']:
|
for field in ['dead','combat','taxi','root','master','impaired']:
|
||||||
l=new();l.execute(f'player.{field}=true;step(0);step(601);assert(attempts==0)')
|
l=new();l.execute(f'player.{field}=true;step(0);step(601);assert(attempts==0)')
|
||||||
print('Death, combat, cast, taxi, root, master and impaired exclusions passed')
|
print('Death, combat, cast, taxi, root, master and impaired exclusions passed')
|
||||||
for action in ['rest','socialize','trade']:
|
for action in ['rest','socialize','trade']:
|
||||||
@@ -60,3 +64,13 @@ l=new();l.execute("step(0);step(600);player.x=8;step(30);assert(string.find(capt
|
|||||||
l=new();l.execute("step(0);step(600);step(180);assert(string.find(captured[#captured], '6E6F5F70726F6772657373'))")
|
l=new();l.execute("step(0);step(600);step(180);assert(string.find(captured[#captured], '6E6F5F70726F6772657373'))")
|
||||||
l=new();l.execute("step(0);step(600);player.dead=true;step(30);assert(string.find(captured[#captured], '756E7665726966696564'))")
|
l=new();l.execute("step(0);step(600);player.dead=true;step(30);assert(string.find(captured[#captured], '756E7665726966696564'))")
|
||||||
print('Recovery outcomes distinguish native progress, no progress, and unavailable residents')
|
print('Recovery outcomes distinguish native progress, no progress, and unavailable residents')
|
||||||
|
|
||||||
|
l=new();l.execute('player.casting=true;step(0);step(599);assert(attempts==0);step(1);assert(attempts==1 and player.interrupted)')
|
||||||
|
l=new();l.execute('player.casting=true;step(0);now=now+500;hooks[32](32,player);step(0);step(101);assert(attempts==0)')
|
||||||
|
l=new();l.execute('player.casting=true;step(0);player.combat=true;step(601);assert(attempts==0 and not player.interrupted)')
|
||||||
|
print('Repeated casts cannot reset idle evidence; productive casts and combat stay protected')
|
||||||
|
|
||||||
|
l=new();l.execute("function ai:HasStrategy()return true end;local changes={};function ai:ChangeStrategy(s)table.insert(changes,s)end;step(0);step(600);assert(changes[1]=='-gather' and changes[2]=='-loot');assert(attempts==1)")
|
||||||
|
print('Recovery pauses gathering before walking, with a durable restoration lease')
|
||||||
|
l=new();l.execute("local query=AuthDBQuery;local changes={};function ai:ChangeStrategy(s)table.insert(changes,s)end;function AuthDBQuery(sql)if string.find(sql,'SELECT guid,name,account_id,gathering,looting',1,true) then return {GetUInt32=function(_,i)return ({[0]=9,[2]=2,[3]=1,[4]=0})[i] end,GetString=function()return 'Test' end,NextRow=function()return false end} end;return query(sql)end;step(0);assert(changes[1]=='+gather' and #changes==1)")
|
||||||
|
print('Lease restoration restores only originally enabled strategies')
|
||||||
|
|||||||
Reference in New Issue
Block a user