Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
68 lines
7.1 KiB
Python
68 lines
7.1 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 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']:methods=methods.replace(obj+'.',obj+'->')
|
|
for kind in ['ERR_GameMode','ERR_ReconnectRules','SCR_PlayerIdentityUtils']: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;')
|
|
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;IEntity* Actor=nullptr;};
|
|
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;}};
|
|
struct ERR_GameMode {bool active=true;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;
|
|
reconnect.apply=false;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.m_mReconnectData.values.size()==1&&reconnect.data.m_ValidUntil.seconds==1000);
|
|
reconnect.apply=true;game.now.seconds=1001;assert(!reconnect.HandlePlayerReconnect(1));assert(reconnect.deleted==1&&reconnect.m_mReconnectData.values.empty());
|
|
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
|
|
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);
|
|
}
|
|
''')
|
|
print('PASS: original body/equipment retained past old timeout; expiry, death, canonical account/body, occupied controller, failed handoff and no replacement life')
|