Files
EscapistsReintroduced/tests/test_participant_selection.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

87 lines
8.0 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'):
s=s.replace(n+'.',n+'::')
for n in ('mode','manager','life','component','target'):
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('GetGame().','GetGame()->').replace('Players().','Players()->')
s=s.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('))
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>{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;}
namespace ERR_Actors{bool Alive(IEntity a){return a&&a->alive;}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;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;}Roster* Players(){return roster;}};ERR_GameMode* ERR_GameMode::current=nullptr;
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){prisonCalls++;p=0;return true;}};
struct Spawn{Placement m_Placement;int m_AuthorizedPositions=0;
'''+spawn+r'''
};
struct Observer{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;rejected();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);
// 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/dead bodies; canonical selection/cycling, unconscious following and prison/geometry fallback preserved; native boundaries adapted')