Files
EscapistsReintroduced/tests/test_reconnect_recovery.py
2026-11-01 18:22:38 +01:00

96 lines
10 KiB
Python

"""Production reconnect reservation/handoff with opaque native engine boundaries."""
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerReconnect.c'
methods='\n'.join(body(p,k) for k in ['protected void PrepareReplacement(', 'protected override void HandleDataExpiery(', 'override int GetReconnectTimeout(', 'override bool HandlePlayerDisconnect(', 'override bool HandlePlayerReconnect('])
methods=methods.replace('protected ', '').replace('notnull ', '').replace('override ', '').replace('super.', 'Native::').replace('null','nullptr')
for kind in ['ERR_GameMode','ERR_PlayerLife','SCR_ReconnectData','IEntity']:
import re
methods=re.sub(r'\b'+kind+r' (\w+)\b',kind+r'* \1',methods)
for obj in ['mode','life','data','orphan']:methods=methods.replace(obj+'.',obj+'->')
for kind in ['ERR_GameMode','ERR_ReconnectRules','SCR_PlayerIdentityUtils','ERR_PlayerLifeRules','ERR_Actors']:methods=methods.replace(kind+'.',kind+'::')
methods=methods.replace('GetGame().','GetGame()->').replace('GetWorld().','GetWorld()->').replace('GetPlayerManager().','GetPlayerManager()->').replace('Players().','Players()->')
# Value-initialize locals as Enforce does.
methods=methods.replace('int owner;', 'int owner=0;')
roster_path=p.with_name('ERR_PlayerRoster.c')
replacement='\n'.join(body(roster_path,k) for k in ['bool AllowReconnectReplacement(', 'void Spawned(', 'void Disconnect('])
replacement=replacement.replace('IEntity actor','IEntity* actor').replace('Replication.IsServer()', 'true').replace('ERR_PlayerLife life', 'ERR_PlayerLife* life').replace('life.', 'life->').replace('ERR_PlayerLifeRules.', 'ERR_PlayerLifeRules::').replace('ERR_Actors.', 'ERR_Actors::').replace('null','nullptr').replace('m_OnChanged.Invoke(playerId);','').replace('m_Connections.Remove(playerId);','')
rule=body(p,'static int Timeout(').replace('Math.Max','std::max')
run(r'''
#include <cassert>
#include <algorithm>
#include <map>
struct UUID {int value=0;bool IsNull(){return value==0;}bool operator==(UUID b)const{return value==b.value;}bool operator!=(UUID b)const{return value!=b.value;}bool operator<(UUID b)const{return value<b.value;}};
struct Time {int seconds=0;Time PlusSeconds(int s){return {seconds+s};}};
struct IEntity {bool alive=true;int equipment=937,health=41,seat=3;};
struct SCR_ReconnectData {Time m_ValidUntil;IEntity* m_ReservedEntity=nullptr;};
struct ERR_PlayerLife {UUID AccountId{7};int ConnectionId=1;bool Connected=true,UsedLife=true,ReconnectPending=false;IEntity* Actor=nullptr;};
struct ERR_PlayerLifeRules {static bool CanEnter(bool used,int phase){return !used&&phase>=1&&phase<=3;}};
struct ERR_Actors {static bool Alive(IEntity* a){return a&&a->alive;}};
struct Roster {ERR_PlayerLife life;bool valid=true;ERR_PlayerLife* CurrentActor(IEntity* a){return valid&&a&&life.Actor==a&&life.Connected&&life.UsedLife?&life:nullptr;}ERR_PlayerLife* Get(int){return valid?&life:nullptr;}'''+replacement+r'''};
struct ERR_GameMode {bool active=true;int phase=1;int Phase(){return phase;}Roster roster;static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}bool Active(){return active;}Roster* Players(){return &roster;}};
ERR_GameMode* ERR_GameMode::current;
struct Game {IEntity* controlled=nullptr;int owner=0;Time now{100};Game* GetPlayerManager(){return this;}Game* GetWorld(){return this;}Time GetTimestamp(){return now;}
IEntity* GetPlayerControlledEntity(int){return controlled;}int GetPlayerIdFromControlledEntity(IEntity*){return owner;}} game;
Game* GetGame(){return &game;}
struct SCR_PlayerIdentityUtils {static UUID id;static UUID GetPlayerIdentityId(int){return id;}};UUID SCR_PlayerIdentityUtils::id{7};
struct ERR_ReconnectRules {'''+rule+r'''};
using KickCauseCode=int;
template<class... T>void PrintFormat(const char*,T...){}
struct Reservations {std::map<UUID,SCR_ReconnectData*> values;void Set(UUID id,SCR_ReconnectData* d){values[id]=d;}void Remove(UUID id){values.erase(id);}bool Find(UUID id,SCR_ReconnectData*& d){auto it=values.find(id);if(it==values.end())return false;d=it->second;return true;}};
struct Native {int configured=120,updates=0,deleted=0,applications=0;bool apply=true;SCR_ReconnectData data;Reservations m_mReconnectData;
int GetReconnectTimeout(){return configured;}bool HandlePlayerDisconnect(int,int){return false;}
bool HandlePlayerReconnect(int){SCR_ReconnectData* d;auto id=SCR_PlayerIdentityUtils::id;if(!m_mReconnectData.Find(id,d))return false;m_mReconnectData.Remove(id);applications++;if(apply)game.controlled=d->m_ReservedEntity;return game.controlled!=nullptr;}
SCR_ReconnectData* StoreData(int){data.m_ReservedEntity=game.controlled;data.m_ValidUntil={configured>0?game.now.seconds+configured:0};return &data;}
bool IsDataRelevant(SCR_ReconnectData* d){return d->m_ReservedEntity&&d->m_ReservedEntity->alive;}
bool IsDataExpired(SCR_ReconnectData* d,Time now){return d->m_ValidUntil.seconds>0&&d->m_ValidUntil.seconds<now.seconds;}
void UpdateExpieryCheck(){updates++;}void HandleDataExpiery(SCR_ReconnectData* d){if(IsDataRelevant(d)){deleted++;d->m_ReservedEntity->alive=false;}}
};
struct Reconnect:Native {'''+methods+r'''};
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;IEntity body,foreign;mode.roster.life.Actor=&body;game.controlled=&body;
Reconnect reconnect;assert(reconnect.GetReconnectTimeout()==900);assert(ERR_ReconnectRules::Timeout(1800)==1800);assert(ERR_ReconnectRules::Timeout(0)==0);
assert(reconnect.HandlePlayerDisconnect(1,0));assert(reconnect.data.m_ReservedEntity==&body&&reconnect.data.m_ValidUntil.seconds==1000);
game.controlled=nullptr;game.now.seconds=900; // longer than old 120-second window
assert(reconnect.HandlePlayerReconnect(1));assert(game.controlled==&body);assert(body.equipment==937&&body.health==41&&body.seat==3);assert(reconnect.m_mReconnectData.values.empty());
assert(!reconnect.HandlePlayerReconnect(1)); // consumed reservation, no second body
game.now.seconds=100;assert(reconnect.HandlePlayerDisconnect(1,99));game.controlled=nullptr;
SCR_PlayerIdentityUtils::id={9};assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1);SCR_PlayerIdentityUtils::id={7};
game.owner=2;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==0);game.owner=0; // never steal/delete another controlled body
game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=&foreign;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1);game.controlled=nullptr;
mode.roster.life.ReconnectPending=true;reconnect.apply=false;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1&&reconnect.data.m_ValidUntil.seconds==1000);assert(mode.roster.life.UsedLife&&mode.roster.life.ReconnectPending);
mode.roster.life.ReconnectPending=true;reconnect.apply=true;game.now.seconds=1001;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1&&reconnect.m_mReconnectData.values.empty());
assert(!mode.roster.life.UsedLife&&!mode.roster.life.ReconnectPending&&mode.roster.life.Actor==nullptr);
mode.roster.life.UsedLife=true;mode.roster.life.ReconnectPending=true;mode.roster.life.Actor=&body;
body.alive=true;game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=nullptr;body.alive=false;
assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1); // dead body left for corpse/loot systems, never revived
assert(!mode.roster.life.UsedLife&&!mode.roster.life.ReconnectPending);
mode.roster.life.UsedLife=true;mode.roster.life.Actor=&body;
body.alive=true;game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=nullptr;mode.roster.life.Actor=&foreign;
assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1); // stale round/body rejected
mode.roster.life.Actor=&body;game.controlled=&body;mode.roster.life.UsedLife=false;assert(!reconnect.HandlePlayerDisconnect(1,0));mode.roster.life.UsedLife=true;
assert(reconnect.HandlePlayerDisconnect(1,0));game.controlled=nullptr;mode.active=false;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.GetReconnectTimeout()==120);
mode.active=true;game.owner=1;int deleted=reconnect.deleted;reconnect.HandleDataExpiery(&reconnect.data);assert(reconnect.deleted==deleted&&body.alive);game.owner=0;
mode.active=true;reconnect.configured=0;game.controlled=&body;assert(reconnect.HandlePlayerDisconnect(1,0));assert(reconnect.data.m_ValidUntil.seconds==0);
// Production roster lifecycle arms only disconnected accounts, clears on assignment.
mode.roster.Spawned(1,&body);assert(!mode.roster.life.ReconnectPending);
mode.roster.Disconnect(1);assert(mode.roster.life.ReconnectPending&&!mode.roster.life.Connected);
assert(!mode.roster.AllowReconnectReplacement(1,1));mode.roster.life.Connected=true;
mode.roster.Spawned(1,&body);assert(!mode.roster.life.ReconnectPending);
// Missing reservation grants exactly one new admission, only on reconnect.
reconnect.m_mReconnectData.values.clear();game.controlled=nullptr;mode.roster.life.Actor=nullptr;
assert(!reconnect.HandlePlayerReconnect(1));assert(mode.roster.life.UsedLife); // duplicate audit after death
mode.roster.life.ReconnectPending=true;
mode.phase=4;assert(!reconnect.HandlePlayerReconnect(1));assert(mode.roster.life.UsedLife); // extraction
mode.phase=1;assert(!reconnect.HandlePlayerReconnect(1));assert(!mode.roster.life.UsedLife);
assert(!reconnect.HandlePlayerReconnect(1));assert(!mode.roster.life.ReconnectPending);
// A living body without a reservation is cleaned up before granting new admission.
mode.roster.life.UsedLife=true;mode.roster.life.ReconnectPending=true;mode.roster.life.Actor=&body;body.alive=true;
game.owner=2;assert(!reconnect.HandlePlayerReconnect(1));assert(body.alive&&mode.roster.life.UsedLife);
game.owner=0;deleted=reconnect.deleted;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==deleted+1&&!mode.roster.life.UsedLife);
// Lost entity inside an existing reservation is also eligible.
mode.roster.life.UsedLife=true;mode.roster.life.ReconnectPending=true;reconnect.data.m_ReservedEntity=nullptr;
reconnect.m_mReconnectData.Set({7},&reconnect.data);assert(!reconnect.HandlePlayerReconnect(1));assert(!mode.roster.life.UsedLife);
}
''')
print('PASS: original body/equipment retained past old timeout; expiry, death, canonical account/body, occupied controller, failed handoff, missing/dead/expired body replacement and extraction/duplicate-audit guards')