Files
Viano-Core/tests/clientless/test_world_shutdown.py
T
Nico d1aceaf583 Integrate safe bot shutdown and automatic login recovery
Drain clientless sessions during normal shutdown with a 60-second grace and recovery audit. Recover invalid bot login states while preserving real transports and taxis.

Handle bounded database reconnect failures with controlled service termination and clear OutdoorPvP references before shutdown destruction. Prevent obsolete transport actions from overriding newer movement.

Add regression coverage, live validation notes and the updated parallel -j10 build workflow in AGENTS.md. Full build, twelve regression tests and live login/shutdown/database-fault checks passed.
2026-11-04 13:00:26 +01:00

52 lines
4.3 KiB
Python

"""Exercise production World shutdown entrypoints and DB failure handoff."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/World.cpp').read_text()
def function(marker):
a=s.index(marker);i=s.index('{',a)+1;n=1
while n:n+=(s[i]=='{')-(s[i]=='}');i+=1
return s[a:i]
a=s.index('void World::Update(uint32 diff)');b=s.index(' time_t currentGameTime',a)
methods='\n'.join(function(m) for m in ['void World::_UpdateGameTime()', 'void World::ShutdownServ(', 'uint32 World::ShutdownCancel()'])+s[a:b]+'}\n'
code=r'''
#include <atomic>
#include <cassert>
#include <string>
#include <ctime>
using uint32=unsigned;using uint8=unsigned char;
constexpr unsigned SHUTDOWN_MASK_IDLE=2,SHUTDOWN_MASK_RESTART=1,SHUTDOWN_MASK_FORCE=4;
constexpr int SHUTDOWN_EXIT_CODE=0,ERROR_EXIT_CODE=1,SERVER_MSG_RESTART_CANCELLED=1,SERVER_MSG_SHUTDOWN_CANCELLED=2;
using ServerMessageType=int;using ShutdownExitCode=unsigned;using ShutdownMask=unsigned;
#define TC_LOG_DEBUG(...) ((void)0)
#define TC_LOG_ERROR(...) ((void)0)
namespace GameTime{time_t now=0,next=0;time_t GetGameTime(){return now;}void UpdateGameTimers(){now=next;}}
struct Mgr{bool draining=false,ready=false;int starts=0,cancels=0;void BeginShutdown(){if(!draining){draining=true;++starts;}}bool ShutdownReady(){return ready;}void CancelShutdown(){draining=false;++cancels;}}mgr;auto sClientlessPlayerMgr=&mgr;
struct Script{int begins=0,cancels=0;void OnShutdownInitiate(unsigned,unsigned){++begins;}void OnShutdownCancel(){++cancels;}}script;auto sScriptMgr=&script;
struct MySQLConnection{static bool failed;static bool HasFatalConnectionError(){return failed;}};bool MySQLConnection::failed=false;
struct World{
static bool m_stopEvent;static std::atomic<int>m_requestedShutdown;
unsigned m_ShutdownMask=0,m_ShutdownTimer=0,m_ExitCode=0,active=0;bool m_databaseShutdownRequested=false,closed=false;
static bool IsStopped(){return m_stopEvent;}unsigned GetActiveAndQueuedSessionCount(){return active;}
void SetClosed(bool v){closed=v;}void ShutdownMsg(bool=false,void* =nullptr,std::string const& ={}){}void SendServerMessage(int){}
void _UpdateGameTime();void ShutdownServ(unsigned,unsigned,unsigned char,std::string const& ={});unsigned ShutdownCancel();void Update(unsigned);
};bool World::m_stopEvent=false;std::atomic<int>World::m_requestedShutdown{-1};
void reset(){World::m_stopEvent=false;World::m_requestedShutdown=-1;mgr={};script={};GameTime::now=GameTime::next=0;MySQLConnection::failed=false;}
'''+methods+r'''
int main(){
reset();{World w;w.ShutdownServ(2,0,0);assert(mgr.draining&&!w.IsStopped());GameTime::next=2;w._UpdateGameTime();assert(w.m_ShutdownTimer==1&&!w.IsStopped());mgr.ready=true;GameTime::next=3;w._UpdateGameTime();assert(w.IsStopped());}
reset();{World w;mgr.ready=true;w.ShutdownServ(0,0,0);assert(w.IsStopped());}
reset();{World w;w.ShutdownServ(0,SHUTDOWN_MASK_FORCE,0);assert(w.IsStopped());}
reset();{World w;w.ShutdownServ(120,0,0);assert(!mgr.draining);GameTime::next=60;w._UpdateGameTime();w._UpdateGameTime();assert(mgr.draining&&!w.IsStopped());assert(w.ShutdownCancel()==60&&mgr.cancels==1&&!mgr.draining);}
reset();{World w;w.ShutdownServ(120,0,0);GameTime::next=120;w._UpdateGameTime();assert(mgr.draining&&!w.IsStopped());}
reset();{World w;w.active=1;mgr.ready=true;w.ShutdownServ(0,SHUTDOWN_MASK_IDLE,0);assert(!w.IsStopped());GameTime::next=1;w._UpdateGameTime();assert(!w.IsStopped());w.active=0;GameTime::next=2;w._UpdateGameTime();assert(w.IsStopped());}
reset();{World w;World::m_requestedShutdown=0;w.Update(1);assert(mgr.draining&&!w.IsStopped());mgr.ready=true;GameTime::next=1;w.Update(1);assert(w.IsStopped());}
reset();{World w;MySQLConnection::failed=true;w.Update(1);assert(w.closed&&w.m_databaseShutdownRequested&&w.m_ExitCode==1&&!w.IsStopped());assert(w.ShutdownCancel()==0);w.Update(1);assert(mgr.starts==1);mgr.ready=true;GameTime::next=1;w.Update(1);assert(w.IsStopped());}
}
'''
with tempfile.TemporaryDirectory(prefix='vcore-world-shutdown-')as folder:
p=Path(folder);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production World: normal/zero/forced/idle shutdown, final-minute drain, cancellation, signal request and terminal database handoff passed.')