Files
Viano-Core/tests/clientless/test_final_shutdown.py
T
Nico 3aa780662e Complete AdventureBot navigation, instance travel and shutdown fixes
Finish capital waiting travel and adaptive flight entry up to 10,000 yards.
Preserve checked vertical climbs, recover blocked ground departures and find
validated dry landings. Retain group routes and Undercity arrival constraints.

Restore defense from native combat references, coordinate instance rendezvous
and browser reentry, finalize clientless sessions before map destruction and
transfer Halaa capture-point ownership safely.

Include the associated regression tests, investigation reports and reusable
diagnostic scripts. Keep generated runtime evidence and server binaries local
through scoped ignore rules. Validation/build/live evidence is recorded in the
session checkpoint and individual reports.
2026-11-05 10:35:34 +01:00

106 lines
6.9 KiB
Python

"""Execute production final CPC cleanup before actual Main teardown sequence."""
from pathlib import Path
import subprocess, tempfile
root = Path(__file__).resolve().parents[2]
world = (root/'src/server/game/World/World.cpp').read_text()
mgr = (root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text()
main = (root/'src/server/worldserver/Main.cpp').read_text()
def function(source, signature):
a=source.index(signature);i=source.index('{',a)+1;depth=1
while depth:
depth+=(source[i]=='{')-(source[i]=='}');i+=1
return source[a:i]
a=main.index('std::shared_ptr<void> mapManagementHandle(')
a=main.index('{',a)+1;b=main.index('\n });',a)
teardown=main[a:b]
processor=(root/'src/common/Utilities/AsyncCallbackProcessor.h').read_text()
processor='\n'.join(l for l in processor.splitlines() if not l.startswith('#include'))
code=r'''
#include <algorithm>
#include <cassert>
#include <chrono>
#include <functional>
#include <future>
#include <map>
#include <string>
#include <vector>
using uint32=unsigned;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_ERROR(...) ((void)0)
'''+processor+r'''
bool mapsAlive=true;unsigned players=0,pets=0,passengers=0,deleted=0,acks=0,notifications=0,delivered=0;
struct Player{unsigned guid;bool pet=false,transport=false,pending=false;Player(unsigned g):guid(g){++players;}~Player(){assert(mapsAlive);--players;}};
struct Callback{std::future<void>future;std::function<void()>done;bool InvokeIfReady(){if(future.wait_for(std::chrono::seconds(0))!=std::future_status::ready)return false;done();return true;}};
struct Session{Player*p=nullptr;bool clientless=true;AsyncCallbackProcessor<Callback>callbacks;
Player*GetPlayer(){return p;}bool IsClientless(){return clientless;}unsigned GetGUIDLow(){return p?p->guid:0;}unsigned GetAccountId(){return 8;}
void CompleteClientlessTeleport(){if(p&&p->pending){assert(mapsAlive);p->pending=false;++acks;}}
~Session(){if(p){assert(mapsAlive);if(p->pet)--pets;if(p->transport)--passengers;delete p;}++deleted;}
};
using WorldSession=Session;
enum class ClientlessPlayerState{Loading,Online,Removing};
struct World{using SessionMap=std::map<unsigned,Session*>;
bool m_clientlessSessionsFinalized=false;SessionMap m_clientlessSessions,m_sessions;
std::map<unsigned,ClientlessPlayerState>m_clientlessPlayerStates;
unsigned GetClientlessSessionCount(){return m_clientlessSessions.size();}
Session*FindClientlessSession(unsigned g){auto i=m_clientlessSessions.find(g);return i==m_clientlessSessions.end()?nullptr:i->second;}
bool AddClientlessSession(unsigned,Session*);void ShutdownClientlessSessions();
}world;auto sWorld=&world;
struct Controller{void OnPlayerLogout(Player*p){assert(mapsAlive&&p);assert(!p->pending);++notifications;
Session late;assert(!world.AddClientlessSession(999,&late)); // no reentrant login during teardown
}}controller;
struct Executor{unsigned clears=0;void Clear(unsigned){++clears;}}executor;auto sClientlessActionExecutorMgr=&executor;
struct Flight{unsigned clears=0;void Clear(unsigned){++clears;}}flight;auto sClientlessFlightMgr=&flight;
struct ClientlessPlayerMgr{
bool _shutdownDraining=false,_shutdownSnapshotDone=false;unsigned snapshots=0;
struct Record{struct Controller*Controller=nullptr;bool LogoutNotified=false;};
std::map<unsigned,Record>_records;std::map<unsigned,bool>_safeLogouts;
void BeginShutdown(){_shutdownDraining=true;}void WriteShutdownSnapshot(char const*event){assert(std::string(event)=="finalize");++snapshots;}
void FinalizeShutdown();
}manager;auto sClientlessPlayerMgr=&manager;
struct Factory{static void Shutdown(){assert(world.m_clientlessSessions.empty());}};using ClientlessCharacterFactory=Factory;
struct BG{void DeleteAllBattlegrounds(){assert(players==0&&pets==0&&passengers==0);}}bg;auto sBattlegroundMgr=&bg;
struct Saves{void Unload(){assert(players==0);}}saves;auto sInstanceSaveMgr=&saves;
struct Outdoor{void Die(){assert(players==0);}}outdoor;auto sOutdoorPvPMgr=&outdoor;
struct Maps{void UnloadAll(){assert(players==0&&pets==0&&passengers==0);mapsAlive=false;}}maps;auto sMapMgr=&maps;
struct Terrain{void UnloadAll(){assert(!mapsAlive);}}sTerrainMgr;
'''+function(world,'bool World::AddClientlessSession(')+function(world,'void World::ShutdownClientlessSessions(')+function(mgr,'void ClientlessPlayerMgr::FinalizeShutdown(')+r'''
void teardown(){
'''+teardown+r'''
}
int main(){
// Pet owner, transport passenger and a pending native teleport survive grace.
for(unsigned i=1;i<=3;++i){auto s=new Session;s->p=new Player(i);assert(world.AddClientlessSession(i,s));manager._records[i]={&controller,false};manager._safeLogouts[i]=true;}
world.FindClientlessSession(1)->p->pet=true;++pets;
world.FindClientlessSession(2)->p->transport=true;++passengers;
world.FindClientlessSession(3)->p->pending=true;
// Already notified Removing sessions must not notify a second time.
manager._records[2].LogoutNotified=true;
// Both ready and still-running login callbacks are discarded, not delivered.
std::promise<void>ready,slow;ready.set_value();
auto loading=new Session;loading->callbacks.AddCallback({ready.get_future(),[]{++delivered;}});
loading->callbacks.AddCallback({slow.get_future(),[]{++delivered;}});
assert(world.AddClientlessSession(4,loading));manager._records[4]={&controller,false};
// Low-level clientless sessions without a managed controller also log out.
auto unmanaged=new Session;unmanaged->p=new Player(5);assert(world.AddClientlessSession(5,unmanaged));
world.m_clientlessSessions[6]=nullptr;world.m_clientlessPlayerStates[6]=ClientlessPlayerState::Loading;
Session network;network.clientless=false;world.m_sessions[8]=&network;
teardown();slow.set_value();
assert(!mapsAlive&&players==0&&pets==0&&passengers==0&&acks==1&&notifications==2&&delivered==0);
assert(world.m_clientlessSessions.empty()&&world.m_clientlessPlayerStates.empty());
assert(manager._records.empty()&&manager._safeLogouts.empty()&&manager._shutdownDraining&&manager.snapshots==1);
assert(world.m_sessions.size()==1&&executor.clears==4&&flight.clears==4);
auto before=deleted;world.ShutdownClientlessSessions();assert(deleted==before&&manager.snapshots==1);
Session late;assert(!world.AddClientlessSession(6,&late));
// A normal shutdown that already wrote the deadline audit retains that audit.
manager.FinalizeShutdown();assert(manager.snapshots==1);
}
'''
# The native session destructor owns LogoutPlayer; no custom pet/passenger deletion.
session=(root/'src/server/game/Server/WorldSession.cpp').read_text()
assert 'LogoutPlayer(true)' in function(session,'WorldSession::~WorldSession(')
with tempfile.TemporaryDirectory(prefix='vcore-final-shutdown-') as directory:
p=Path(directory);(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('Final shutdown: real World/manager/Main sequence; pet/transport/teleport cleanup before maps, pending callbacks cancelled, no new sessions, exactly-once notifications, audit and idempotence PASS')