"""Execute actual chat teardown/scope bodies against replaced local-controller ownership.""" import os from pathlib import Path from policy_harness import ROOT, body, run source = Path(os.environ.get('ERR_CHAT_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ChatClient.c')) def adapt(text): text=text.replace('protected ', '').replace('ERR_GameMode mode', 'Mode* mode').replace('ERR_AdminMenu menu', 'Menu* menu') for name in ('ERR_GameMode','ERR_AdminMenu'): text=text.replace(name+'.',name+'::') text=text.replace('GetGame().','GetGame()->').replace('GetCallqueue().','GetCallqueue()->') for name in ('m_Command','menu'): text=text.replace(name+'.',name+'->') text=text.replace('Remove(Register)', 'Remove(this, 1)').replace('Remove(Poll)', 'Remove(this, 2)').replace('Remove(OnCommand)', 'Remove(this)').replace('null','nullptr') return text methods='\n'.join(adapt(body(source,s)) for s in ('protected void Poll(', 'int Scope(', 'bool Accept(', 'void Stop(')) run(r''' #include #include #include #include struct Controller {}; struct Mode {}; struct Queue { std::set> pending; int removed=0; void Remove(void* owner,int method){pending.erase({owner,method});removed++;} }; struct Game {Controller* local=nullptr;Queue queue;Controller* GetPlayerController(){return local;}Queue* GetCallqueue(){return &queue;}} game; Game* currentGame=&game;Game* GetGame(){return currentGame;} struct Command {std::set listeners;int removals=0;void Remove(void* owner){listeners.erase(owner);removals++;}} command; struct Menu {int closes=0;void Close(){closes++;}} oldMenu,newMenu; namespace ERR_AdminMenu {Menu* open=nullptr;Menu* GetOpenMenu(){return open;}} namespace ERR_GameMode {Mode* current=nullptr;Mode* Get(){return current;}} struct Client {Controller* m_Controller=nullptr;Mode* m_Mode=nullptr;Command* m_Command=nullptr;bool m_Stopped=false;int m_Token=1; '''+methods+r''' }; void registerClient(Client& c){c.m_Command=&command;command.listeners.insert(&c);game.queue.pending.insert({&c,1});game.queue.pending.insert({&c,2});} int main(){ Controller previous,replacement;Mode first,second;game.local=&previous;ERR_GameMode::current=&first; Client old;old.m_Controller=&previous;old.m_Mode=&first;registerClient(old); Client next;next.m_Controller=&replacement;next.m_Mode=&first;registerClient(next); // The new controller has opened its own panel before the retained old callback fires. game.local=&replacement;ERR_AdminMenu::open=&newMenu;old.Poll(); assert(old.m_Stopped && !old.m_Command); assert(newMenu.closes==0); // old baseline wrongly closes the new owner's panel assert(!command.listeners.count(&old) && command.listeners.count(&next)); assert(!game.queue.pending.count({&old,1})&&!game.queue.pending.count({&old,2})); assert(game.queue.pending.count({&next,1})&&game.queue.pending.count({&next,2})); int removed=game.queue.removed,unbound=command.removals;old.Stop();assert(game.queue.removed==removed&&command.removals==unbound&&newMenu.closes==0); assert(!old.Accept(1)&&next.Accept(1));next.Poll();assert(!next.m_Stopped&&newMenu.closes==0); // Current owner still closes its panel on a mission change and rotates its token once. ERR_GameMode::current=&second;next.Poll();assert(next.m_Token==2&&newMenu.closes==1); next.Poll();assert(next.m_Token==2&&newMenu.closes==1);assert(!next.Accept(1)&&next.Accept(2)); next.Stop();assert(next.m_Stopped&&newMenu.closes==2&&!command.listeners.count(&next)); next.Stop();assert(newMenu.closes==2); // Retained controller destruction (direct Stop), null local gaps and shutdown are safe. Client destroyed;destroyed.m_Controller=&previous;registerClient(destroyed);destroyed.Stop();assert(newMenu.closes==2); Client gap;gap.m_Controller=&previous;registerClient(gap);game.local=nullptr;gap.Poll();assert(gap.m_Stopped&&newMenu.closes==2); Client shutdown;shutdown.m_Controller=&previous;registerClient(shutdown);currentGame=nullptr;ERR_AdminMenu::open=nullptr;shutdown.Stop();assert(shutdown.m_Stopped&&!shutdown.m_Command&&!command.listeners.count(&shutdown));currentGame=&game; // No open panel is also valid for the still-current owner. Client noPanel;noPanel.m_Controller=&previous;registerClient(noPanel);game.local=&previous;noPanel.Stop();assert(noPanel.m_Stopped); } ''') print('PASS: former chat controller releases only its own callbacks and preserves replacement menu; current-owner stop/scope and repeated teardown remain intact')