Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
133 lines
13 KiB
Python
133 lines
13 KiB
Python
"""Run actual roster, late-join selection and spectator server-selection methods.
|
|
|
|
Engine enumeration/geometry/streaming are adapters, not native/MP coverage.
|
|
"""
|
|
import re
|
|
from policy_harness import ROOT,body,run
|
|
core=ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
|
def adapt(s):
|
|
s=s.replace('protected ','').replace('out vector position','vector& position').replace('null','nullptr')
|
|
s=re.sub(r'array<IEntity> (\w+) = \{\};',r'Array<IEntity> \1;',s)
|
|
s=s.replace('foreach (IEntity survivor : survivors)','for (IEntity survivor : survivors)')
|
|
for n in ('ERR_Phase','ERR_Actors','SCR_FactionManager','Replication','ERR_GameMode','ERR_ObserverReason','RplComponent','CompartmentAccessComponent','ERR_ObserverRules','ERR_ObserverTargets'):
|
|
s=s.replace(n+'.',n+'::')
|
|
for n in ('mode','manager','life','component','target','body'):
|
|
s=s.replace(n+'.',n+'->')
|
|
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('PlayerManager manager','PlayerManager* manager').replace('RplComponent component','RplComponent* component')
|
|
s=s.replace('life->Actor.', 'life->Actor->').replace('GetGame().','GetGame()->').replace('Players().','Players()->')
|
|
s=s.replace('ERR_PlayerLife life;', 'ERR_PlayerLife life=nullptr;').replace('IEntity target;','IEntity target=nullptr;').replace('FindComponent(RplComponent)','FindComponent()')
|
|
return s
|
|
roster=adapt(body(core/'ERR_PlayerRoster.c','ERR_PlayerLife CurrentActor('))
|
|
spawn=adapt(body(core/'ERR_SpawnLogic.c','protected bool SpawnPosition('))
|
|
observe=adapt(body(core/'ERR_Observer.c','void ERR_UpdateObserverServer('))
|
|
bodies=adapt(body(core/'ERR_ObserverTargets.c','static void Bodies(')).replace('array<IEntity> targets','Array<IEntity>& targets').replace('array<ref ERR_PlayerLife> lives = {};','Array<ERR_PlayerLife> lives;').replace('foreach (ERR_PlayerLife life : lives)', 'for (ERR_PlayerLife life : lives)')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <map>
|
|
#include <algorithm>
|
|
using vector=int;using Faction=int;using RplId=int;
|
|
template<class T>struct Array:std::vector<T>{bool Contains(T a){return Find(a)>=0;}void Insert(T a){this->push_back(a);}int Count(){return this->size();}bool IsEmpty(){return this->empty();}void RemoveOrdered(int i){this->erase(this->begin()+i);}int Find(T a){auto p=std::find(this->begin(),this->end(),a);return p==this->end()?-1:p-this->begin();}};
|
|
struct RplComponent{int id=1;int Id(){return id;}static RplComponent* Cast(void* p){return static_cast<RplComponent*>(p);}};
|
|
struct Actor{bool alive=true,conscious=true;int faction=1,origin=123;Actor* vehicle=nullptr;RplComponent replication;int GetOrigin(){return origin;}void* FindComponent(){return &replication;}};using IEntity=Actor*;
|
|
struct Identity{int id=1;bool IsNull(){return id==0;}};
|
|
struct Life{Identity AccountId;bool Connected=true,UsedLife=true;int ConnectionId=1;IEntity Actor=nullptr;};using ERR_PlayerLife=Life*;
|
|
struct PlayerManager{std::map<int,IEntity> controlled;int GetPlayerIdFromControlledEntity(IEntity a){for(auto& p:controlled)if(p.second==a)return p.first;return 0;}IEntity GetPlayerControlledEntity(int id){return controlled[id];}};
|
|
struct Game{PlayerManager* manager;PlayerManager* GetPlayerManager(){return manager;}};Game* game_;Game* GetGame(){return game_;}
|
|
namespace Replication{bool server=true;int bumps=0;bool IsServer(){return server;}void BumpMe(){bumps++;}}
|
|
namespace ERR_Phase{const int PRISON=1;}
|
|
namespace ERR_ObserverReason{const int NONE=0,SPENT_LIFE=1,UNCONSCIOUS=4;}
|
|
namespace ERR_ObserverRules{
|
|
'''+adapt(body(core/'ERR_Observer.c','static bool Casualty('))+r'''
|
|
}
|
|
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}bool CanFight(IEntity a){return Alive(a)&&a->conscious;}void Players(Array<IEntity>& out,bool conscious=false){out.clear();for(auto& p:GetGame()->manager->controlled){IEntity a=p.second;if(Alive(a)&&(!conscious||a->conscious))out.push_back(a);}}}
|
|
namespace SCR_FactionManager{int SGetFaction(IEntity a){return a?a->faction:0;}}
|
|
namespace CompartmentAccessComponent{IEntity GetVehicleIn(IEntity a){return a?a->vehicle:nullptr;}}
|
|
struct Roster{std::map<int,ERR_PlayerLife> lives;void Participants(Array<ERR_PlayerLife>& out){for(auto p:lives)if(p.second->UsedLife)out.push_back(p.second);}ERR_PlayerLife Get(int id){auto p=lives.find(id);return p==lives.end()?nullptr:p->second;}
|
|
'''+roster+r'''
|
|
};
|
|
struct ERR_GameMode{static ERR_GameMode* current;Roster* roster;int phase=2,faction=1;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}int PlayerFaction(){return faction;}int PrisonPosition(){return 0;}float PrisonHeading(){return 90;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
|
|
namespace ERR_ObserverTargets {
|
|
'''+bodies+r'''
|
|
}
|
|
struct Placement{int nearCalls=0,prisonCalls=0;IEntity last=nullptr;bool clear=true;bool Near(IEntity a,int,vector& p){nearCalls++;last=a;p=a->origin;return clear;}bool Prison(int,int,vector& p,float heading){assert(heading==90);prisonCalls++;p=0;return true;}};
|
|
struct Spawn{Placement m_Placement;int m_AuthorizedPositions=0;
|
|
'''+spawn+r'''
|
|
};
|
|
struct Observer{int playerId=0;int GetPlayerId(){return playerId;}int m_ERRObserverReason=1,m_ERRObserverAnchor=-1,m_ERRObservedId=-1,releases=0,streamRequests=0;IEntity m_ERRServerObserved=nullptr,m_ERRServerObservedVehicle=nullptr,own=nullptr;IEntity GetControlledEntity(){return own;}
|
|
void ERR_ReleaseObserved(){releases++;m_ERRServerObserved=nullptr;m_ERRServerObservedVehicle=nullptr;m_ERRObservedId=-1;}
|
|
void ERR_StreamObserved(IEntity,bool){streamRequests++;}
|
|
'''+observe+r'''
|
|
};
|
|
int main(){
|
|
PlayerManager manager;Game game{&manager};game_=&game;Roster roster;ERR_GameMode mode;mode.roster=&roster;ERR_GameMode::current=&mode;
|
|
Actor original,replacement,other;original.origin=100;replacement.origin=900;other.origin=200;
|
|
Life life;life.Actor=&original;roster.lives[1]=&life;manager.controlled[1]=&original;
|
|
auto accepted=[&](){Spawn s;vector p=-1;assert(s.SpawnPosition(&mode,p)&&p==original.origin&&s.m_Placement.last==&original&&s.m_Placement.prisonCalls==0);Observer o;o.ERR_UpdateObserverServer();assert(o.m_ERRServerObserved==&original&&o.m_ERRObserverAnchor==original.origin);};
|
|
auto rejected=[&](){Spawn s;vector p=-1;assert(s.SpawnPosition(&mode,p)&&p==0&&s.m_Placement.nearCalls==0&&s.m_Placement.prisonCalls==1);Observer o;o.m_ERRServerObserved=&original;o.ERR_UpdateObserverServer();assert(!o.m_ERRServerObserved&&o.releases==1);};
|
|
accepted();
|
|
roster.lives.clear();rejected();roster.lives[1]=&life; // unregistered controller
|
|
life.UsedLife=false;rejected();life.UsedLife=true; // pending initial assignment
|
|
life.Connected=false;rejected();life.Connected=true;
|
|
life.AccountId.id=0;rejected();life.AccountId.id=1;
|
|
life.ConnectionId=2;rejected();life.ConnectionId=1;
|
|
manager.controlled[1]=&replacement;rejected(); // recycled connection ID/controlled replacement cannot inherit original body record
|
|
manager.controlled[1]=&original;accepted(); // restored canonical handoff
|
|
original.alive=false;Observer corpse;corpse.ERR_UpdateObserverServer();assert(corpse.m_ERRServerObserved==&original);Spawn noCorpseSpawn;vector deadPos;noCorpseSpawn.SpawnPosition(&mode,deadPos);assert(noCorpseSpawn.m_Placement.nearCalls==0);original.alive=true;
|
|
original.faction=2;rejected();original.faction=1;
|
|
mode.faction=0;rejected();mode.faction=1;
|
|
mode.roster=nullptr;rejected();mode.roster=&roster;
|
|
original.conscious=false;Spawn unconscious;vector p=-1;assert(unconscious.SpawnPosition(&mode,p)&&p==0&&unconscious.m_Placement.nearCalls==0);
|
|
Observer following;following.ERR_UpdateObserverServer();assert(following.m_ERRServerObserved==&original);original.conscious=true; // living unconscious prisoners still followable
|
|
// An invalid earlier candidate cannot shadow a later canonical survivor.
|
|
manager.controlled[1]=&replacement;Life second;second.Actor=&other;second.ConnectionId=2;second.AccountId.id=2;manager.controlled[2]=&other;roster.lives[2]=&second;
|
|
Spawn mixed;assert(mixed.SpawnPosition(&mode,p)&&p==other.origin&&mixed.m_Placement.last==&other);
|
|
Observer mixedFollow;mixedFollow.m_ERRServerObserved=&replacement;mixedFollow.ERR_UpdateObserverServer();assert(mixedFollow.m_ERRServerObserved==&other&&mixedFollow.releases==1);
|
|
manager.controlled[1]=&original;Observer cycling;cycling.ERR_UpdateObserverServer();assert(cycling.m_ERRServerObserved==&original);cycling.ERR_UpdateObserverServer(1);assert(cycling.m_ERRServerObserved==&other);cycling.ERR_UpdateObserverServer(1);assert(cycling.m_ERRServerObserved==&original);cycling.ERR_UpdateObserverServer(-1);assert(cycling.m_ERRServerObserved==&other);
|
|
// Unconscious player keeps possession and follows only conscious canonical teammates.
|
|
Observer casualty;casualty.own=&original;casualty.m_ERRObserverReason=0;original.conscious=false;
|
|
casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==4&&casualty.m_ERRServerObserved==&other&&casualty.own==&original);
|
|
other.conscious=false;casualty.ERR_UpdateObserverServer();assert(!casualty.m_ERRServerObserved&&casualty.m_ERRObserverAnchor==original.origin);
|
|
other.conscious=true;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRServerObserved==&other);
|
|
original.conscious=true;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==0&&!casualty.m_ERRServerObserved&&casualty.own==&original);
|
|
original.conscious=false;life.Connected=false;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==0);
|
|
life.Connected=true;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==4);
|
|
mode.active=false;casualty.ERR_UpdateObserverServer();assert(casualty.m_ERRObserverReason==0&&!casualty.m_ERRServerObserved);
|
|
mode.active=true;original.conscious=true;
|
|
// A dead/disconnected teammate remains selectable while another is alive.
|
|
original.alive=false;life.Connected=false;manager.controlled.erase(1);
|
|
Observer mixedDead;mixedDead.ERR_UpdateObserverServer();assert(mixedDead.m_ERRServerObserved==&other);
|
|
mixedDead.ERR_UpdateObserverServer(1);assert(mixedDead.m_ERRServerObserved==&original);
|
|
mixedDead.ERR_UpdateObserverServer(0);assert(mixedDead.m_ERRServerObserved==&original); // polling retains selected corpse
|
|
mixedDead.ERR_UpdateObserverServer(-1);assert(mixedDead.m_ERRServerObserved==&other);
|
|
// Every participant dead, with native controllers already detached: cycle both bodies, including own.
|
|
original.alive=false;other.alive=false;manager.controlled.clear();life.Connected=false;second.Connected=false;
|
|
Observer allDead;allDead.own=&original;allDead.m_ERRObserverReason=4;
|
|
allDead.ERR_UpdateObserverServer();assert(allDead.m_ERRObserverReason==1&&allDead.m_ERRServerObserved==&original);
|
|
allDead.ERR_UpdateObserverServer(1);assert(allDead.m_ERRServerObserved==&other);
|
|
allDead.ERR_UpdateObserverServer(1);assert(allDead.m_ERRServerObserved==&original);
|
|
allDead.ERR_UpdateObserverServer(-1);assert(allDead.m_ERRServerObserved==&other);
|
|
second.Actor=nullptr;allDead.ERR_UpdateObserverServer();assert(allDead.m_ERRServerObserved==&original);second.Actor=&other;
|
|
other.faction=2;allDead.ERR_UpdateObserverServer(1);assert(allDead.m_ERRServerObserved==&original);
|
|
other.faction=0;allDead.ERR_UpdateObserverServer(1);assert(allDead.m_ERRServerObserved==&other);other.faction=1; // detached corpse without affiliation is still a canonical player body
|
|
original.alive=true;other.alive=true;life.Connected=true;second.Connected=true;manager.controlled[1]=&original;manager.controlled[2]=&other;
|
|
allDead.ERR_UpdateObserverServer();assert(allDead.m_ERRObserverReason==0); // healed body takes camera back
|
|
// Solo death without entity-lost callback still selects the controlled corpse.
|
|
roster.lives.erase(2);manager.controlled.erase(2);original.alive=false;
|
|
Observer solo;solo.playerId=1;solo.own=&original;solo.m_ERRObserverReason=0;
|
|
solo.ERR_UpdateObserverServer();assert(solo.m_ERRObserverReason==1&&solo.m_ERRServerObserved==&original&&solo.m_ERRObserverAnchor==original.origin);
|
|
// Native detachment before the callback is equally covered by the roster.
|
|
solo.own=nullptr;solo.m_ERRObserverReason=0;solo.ERR_UpdateObserverServer();assert(solo.m_ERRObserverReason==1&&solo.m_ERRServerObserved==&original);
|
|
mode.active=false;solo.ERR_UpdateObserverServer();assert(solo.m_ERRObserverReason==0);mode.active=true;
|
|
life.UsedLife=false;solo.ERR_UpdateObserverServer();assert(solo.m_ERRObserverReason==0);life.UsedLife=true;
|
|
// A new living controlled body must not inherit its predecessor's death camera.
|
|
solo.own=&replacement;solo.ERR_UpdateObserverServer();assert(solo.m_ERRObserverReason==0);
|
|
original.alive=true;roster.lives[2]=&second;manager.controlled[2]=&other;
|
|
// Existing terrain rejection and prison-phase placement stay delegated.
|
|
Spawn blocked;blocked.m_Placement.clear=false;assert(blocked.SpawnPosition(&mode,p)&&p==0&&blocked.m_Placement.nearCalls==2);
|
|
mode.phase=ERR_Phase::PRISON;Spawn prison;assert(prison.SpawnPosition(&mode,p)&&p==0&&prison.m_Placement.nearCalls==0);
|
|
Replication::server=false;Observer client;client.ERR_UpdateObserverServer();assert(!client.m_ERRServerObserved&&client.streamRequests==0);Replication::server=true;
|
|
}
|
|
''')
|
|
print('PASS: actual CurrentActor, SpawnPosition and observer-selection methods reject pending/unregistered/replaced/recycled/foreign bodies; canonical live selection and roster-corpse cycling, unconscious following and prison/geometry fallback preserved; native boundaries adapted')
|