81 lines
6.6 KiB
Python
81 lines
6.6 KiB
Python
"""Actual admission-exhaustion/tick rules; engine population and services substituted."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
MODE = ROOT/'addons/core/Scripts/Game/ERR/ERR_GameMode.c'
|
|
SPAWN = ROOT/'addons/core/Scripts/Game/ERR/ERR_SpawnLogic.c'
|
|
def cpp(s):
|
|
s=s.replace('protected ', '').replace('Replication.IsServer()', 'server').replace('Replication.BumpMe()', '(void)0')
|
|
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('connected.IsEmpty()', 'connected.empty()')
|
|
s=s.replace('foreach (int id : connected)', 'for (int id : connected)')
|
|
s=s.replace('ERR_PlayerLife life', 'Life* life').replace('SCR_PlayerController controller', 'Controller* controller').replace('PlayerController controller', 'Controller* controller')
|
|
s=s.replace('SCR_PlayerController.Cast(', 'Controller::Cast(').replace('ERR_Phase.DEFEAT','5')
|
|
for v in ('m_Roster','controller','life','m_Command','m_Extraction','m_RadioDefence','m_Recovery','GetGame()','GetWorld()','GetPlayerManager()'):
|
|
s=s.replace(v+'.', v+'->')
|
|
return s
|
|
methods='\n'.join(cpp(body(MODE,x)) for x in ('void InitialAdmissionStarted(', 'void InitialAdmissionExhausted(', 'protected bool InitialAdmissionsExhausted(', 'protected void ServerTick('))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <map>
|
|
#include <vector>
|
|
#include <string>
|
|
bool server=true;
|
|
struct Controller {void* actor=nullptr;void* GetControlledEntity(){return actor;}static Controller* Cast(Controller* c){return c;}void ERR_UpdateObserverServer(){}};
|
|
struct Life {bool Connected=true,UsedLife=false;void* Actor=nullptr;};
|
|
struct Roster {std::map<int,Life> lives;bool survivor=false;Life* Get(int id){auto i=lives.find(id);return i==lives.end()?nullptr:&i->second;}bool HasSurvivor(){return survivor;}};
|
|
struct Players {std::map<int,Controller*> connected;void GetPlayers(std::vector<int>& v){for(auto p:connected)v.push_back(p.first);}int GetPlayerCount(){return connected.size();}Controller* GetPlayerController(int id){auto i=connected.find(id);return i==connected.end()?nullptr:i->second;}};
|
|
struct Game {Players players;Players* GetPlayerManager(){return &players;}} game;Game* GetGame(){return &game;}
|
|
struct World {float now=0;float GetWorldTime(){return now;}} world;
|
|
struct Service {void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
|
|
struct Exhausted {std::map<int,Controller*> values;void Remove(int id){values.erase(id);}void Set(int id,Controller* c){values[id]=c;}bool Contains(int id){return values.count(id);}Controller* Get(int id){return values.at(id);}};
|
|
struct Mode {
|
|
bool m_StartedPlayers=false;int m_Phase=1;float m_NoSurvivorsSince=-1;std::string m_EndReason;
|
|
Exhausted m_ExhaustedInitialAdmissions;Roster roster;Roster* m_Roster=&roster;
|
|
Service service,m_OnTick;Service *m_Extraction=&service,*m_RadioDefence=&service,*m_Recovery=&service,*m_Command=&service;
|
|
World* GetWorld(){return &world;}bool Active(){return m_Phase>0&&m_Phase<5;}void Transition(int p){m_Phase=p;}bool RefreshRadioFeedback(){return false;}
|
|
'''+methods+r'''
|
|
};
|
|
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
|
|
int main(){
|
|
// Sole exhausted first admission reaches existing grace, without a life/body.
|
|
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
|
|
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
|
|
assert(f.m.m_EndReason=="No prisoners could be admitted to this round.");}
|
|
// Not-yet-audited arrivals block the timer before they have any roster entry.
|
|
{Fixture f;f.exhaust();f.tick(0);game.players.connected[2]=&f.second;f.tick(29000);
|
|
assert(f.m.m_NoSurvivorsSince==-1);f.m.roster.lives[2]=Life();f.tick(60000);assert(f.m.Active());
|
|
f.m.InitialAdmissionExhausted(2,&f.second);f.tick(61000);f.tick(90999);assert(f.m.Active());f.tick(91000);assert(!f.m.Active());}
|
|
// A retry/pending new attempt clears exhaustion immediately, not one tick later.
|
|
{Fixture f;f.exhaust();f.tick(0);f.m.InitialAdmissionStarted(1);assert(f.m.m_NoSurvivorsSince==-1);
|
|
f.tick(90000);assert(f.m.Active()&&!f.m.InitialAdmissionsExhausted());
|
|
f.exhaust();f.tick(91000);f.tick(120999);assert(f.m.Active());f.tick(121000);assert(!f.m.Active());}
|
|
// Empty population and disconnect cleanup erase the initial failure grace.
|
|
{Fixture f;f.exhaust();f.tick(0);game.players.connected.clear();f.tick(30000);assert(f.m.m_NoSurvivorsSince==-1&&f.m.Active());
|
|
f.m.InitialAdmissionStarted(1);game.players.connected[1]=&f.first;f.tick(60000);assert(f.m.Active());}
|
|
// Recycled connection/controller, live body and invalid roster never inherit it.
|
|
for(int fault=0;fault<5;fault++){Fixture f;f.exhaust();f.tick(0);
|
|
if(fault==0)game.players.connected[1]=&f.second;
|
|
if(fault==1)f.first.actor=&f.first;
|
|
if(fault==2)f.m.roster.lives[1].UsedLife=true;
|
|
if(fault==3)f.m.roster.lives[1].Actor=&f.first;
|
|
if(fault==4)f.m.roster.lives[1].Connected=false;
|
|
f.tick(30000);assert(f.m.Active()&&f.m.m_NoSurvivorsSince==-1);}
|
|
// Invalid phase, authority, prior participation or controller cannot record failure.
|
|
for(int fault=0;fault<6;fault++){Fixture f;
|
|
if(fault==0){server=false;}if(fault==1){f.m.m_Phase=0;}if(fault==2){f.m.m_Phase=5;}
|
|
if(fault==3){f.m.m_StartedPlayers=true;}if(fault==4){game.players.connected[1]=&f.second;}if(fault==5){f.m.roster.lives.clear();}
|
|
f.exhaust();assert(f.m.m_ExhaustedInitialAdmissions.values.empty());}
|
|
// Existing actual first spawn changes participation; normal death grace is unchanged.
|
|
{Fixture f;f.exhaust();f.m.InitialAdmissionStarted(1);f.m.m_StartedPlayers=true;f.m.roster.lives[1].UsedLife=true;f.m.roster.survivor=true;
|
|
f.tick(30000);assert(f.m.m_NoSurvivorsSince==-1&&f.m.Active());f.m.roster.survivor=false;f.tick(31000);f.tick(61000);
|
|
assert(f.m.m_Phase==5&&f.m.m_EndReason=="No surviving prisoners remain.");}
|
|
}
|
|
''')
|
|
# Tie the tested state seam to actual budget exhaustion and new/disconnected admission.
|
|
do_spawn=body(SPAWN,'protected override void DoSpawn_S(')
|
|
assert do_spawn.index('Observe(playerId, ERR_ObserverReason.SPAWN_UNAVAILABLE);\n mode.InitialAdmissionExhausted')>do_spawn.index('if (attempts >= 30)')
|
|
audit=body(SPAWN,'override void OnPlayerAuditSuccess_S(')
|
|
assert audit.index('mode.InitialAdmissionStarted(playerId);')<audit.index('m_Attempts.Set(playerId, 0);')
|
|
disconnect=body(SPAWN,'override void OnPlayerDisconnected_S(')
|
|
assert 'mode.InitialAdmissionStarted(playerId);' in disconnect
|
|
print('PASS: actual initial admission exhaustion and ServerTick grace; new/pending/empty/controller/life guards, normal death preserved')
|