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.
82 lines
6.7 KiB
Python
82 lines
6.7 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/00_Core/ERR_GameMode.c'
|
|
SPAWN = ROOT/'addons/core/Scripts/Game/ERR/00_Core/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').replace('ERR_Phase.RADIO_SEARCH','2')
|
|
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 {
|
|
void TickForces(float){}
|
|
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')
|