131 lines
8.2 KiB
Python
131 lines
8.2 KiB
Python
"""Execute actual DoSpawn_S ordering with native admission/geometry adapters.
|
|
No Enfusion scheduling, possession, collision or remote-client proof.
|
|
"""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
source = ROOT/'addons/core/Scripts/Game/ERR/ERR_SpawnLogic.c'
|
|
method = body(source, 'protected override void DoSpawn_S(')
|
|
method = method.replace('protected override ', '')
|
|
for old, new in [('ERR_GameMode', 'Mode'), ('SCR_PlayerController', 'Controller'),
|
|
('SCR_PlayerFactionAffiliationComponent', 'Affiliation'),
|
|
('SCR_RespawnComponent', 'Respawn'), ('ERR_PlayerLife', 'Life'),
|
|
('SCR_FreeSpawnData', 'Request'), ('ERR_ObserverReason', 'Reason')]:
|
|
method = method.replace(old, new)
|
|
for kind in ('Mode', 'Controller', 'Affiliation', 'Respawn', 'Faction', 'Life', 'Request'):
|
|
method = re.sub(r'\b'+kind+r' (\w+)\b', kind+r'* \1', method)
|
|
for var in ('controller', 'mode', 'affiliation', 'respawn', 'life'):
|
|
method = method.replace(var+'.', var+'->')
|
|
method = method.replace('Mode.Get()', 'Mode::Get()').replace('Controller.Cast(', 'Controller::Cast(')
|
|
method = method.replace('GetGame().GetPlayerManager().GetPlayerController(', 'GetGame()->GetPlayerManager()->GetPlayerController(')
|
|
method = method.replace('mode->Players().', 'mode->Players()->').replace('mode->Factions().', 'mode->Factions()->')
|
|
method = method.replace('ERR_MissionRules.Terminal(', 'Terminal(').replace('Reason.', 'Reason::')
|
|
method = method.replace('vector position;', 'int position=0;').replace('Reason reason =', 'int reason =')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <map>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
struct Text { bool empty=true; bool IsEmpty(){return empty;} };
|
|
struct Life {bool UsedLife=false;};
|
|
struct Roster {bool enter=true;Life life;bool CanEnter(int,int){return enter;}Life* Get(int){return &life;}};
|
|
struct Catalog {int PrisonerPrefab=1;};
|
|
struct Faction {} faction;
|
|
struct Controller;
|
|
struct Mode {int exhausted=0;void InitialAdmissionExhausted(int,Controller*){exhausted++;}bool active=true;int phase=1;Text error;Roster roster;Catalog catalog;Faction* factionPtr=&faction;
|
|
static Mode* current;static Mode* Get(){return current;}bool Active(){return active;}int Phase(){return phase;}
|
|
Text PreparationError(){return error;}Roster* Players(){return &roster;}Catalog* Factions(){return &catalog;}Faction* PlayerFaction(){return factionPtr;}};
|
|
Mode* Mode::current=nullptr;
|
|
struct Controller {bool controlled=false;static Controller* Cast(Controller* value){return value;}bool GetControlledEntity(){return controlled;}};
|
|
struct Manager {Controller* controller=nullptr;Controller* GetPlayerController(int){return controller;}};
|
|
struct Game {Manager manager;Manager* GetPlayerManager(){return &manager;}} game;
|
|
Game* GetGame(){return &game;}
|
|
struct Affiliation {int requests=0;void RequestFaction(Faction*){requests++;}};
|
|
struct Request {int prefab,position;Request(int a,int b):prefab(a),position(b){} };
|
|
struct Respawn {bool can=true,request=true;int requests=0,checks=0;std::vector<Request*> allocations;
|
|
bool CanSpawn(Request* value){allocations.push_back(value);checks++;return can;}
|
|
bool RequestSpawn(Request*){requests++;return request;}
|
|
~Respawn(){for(auto* value:allocations)delete value;}};
|
|
struct Values {std::map<int,int> values;bool Contains(int id){return values.count(id);}int Get(int id){return values.at(id);}void Set(int id,int value){values[id]=value;}};
|
|
struct IDs {std::set<int> values;bool Contains(int id){return values.count(id);}void Insert(int id){values.insert(id);}};
|
|
namespace Reason {enum {NONE,SPAWN_UNAVAILABLE,SPENT_LIFE,LATE_EXTRACTION};}
|
|
bool Terminal(int phase){return phase>=5;}
|
|
struct Logic {
|
|
Values m_Attempts,m_AuthorizedPositions;IDs m_Identities,m_Pending;bool m_AdmissionsStopped=false;
|
|
Affiliation affiliation;Respawn respawn;bool hasAffiliation=true,hasRespawn=true,context=true,clear=true;
|
|
int retries=0,observations=0,reason=Reason::NONE,placementChecks=0,observedBudget=-1;
|
|
Affiliation* GetPlayerFactionComponent_S(int){return hasAffiliation?&affiliation:nullptr;}
|
|
Respawn* GetPlayerRespawnComponent_S(int){return hasRespawn?&respawn:nullptr;}
|
|
bool CheckLocalAdmissionContext(){return context;}
|
|
bool SpawnPosition(Mode*,int& position){placementChecks++;position=42;return clear;}
|
|
void RetryLater(int){retries++;}
|
|
void Observe(int id,int why){observations++;reason=why;observedBudget=m_Attempts.Get(id);m_Attempts.values.erase(id);}
|
|
void OnPlayerSpawnFailed_S(int id){m_Pending.values.erase(id);m_AuthorizedPositions.values.erase(id);RetryLater(id);}
|
|
Logic(){m_Identities.Insert(1);m_Attempts.Set(1,0);}
|
|
'''+method+r'''
|
|
};
|
|
int main(){
|
|
Mode mode;Mode::current=&mode;Controller controller;game.manager.controller=&controller;
|
|
// Blocked geometry, missing affiliation, missing respawn and missing faction
|
|
// each consume exactly 30 eligible attempts, then expose the existing observer.
|
|
for(int fault=0;fault<5;fault++){
|
|
Logic logic;
|
|
if(fault==0)logic.clear=false;
|
|
if(fault==1)logic.hasAffiliation=false;
|
|
if(fault==2)logic.hasRespawn=false;
|
|
if(fault==3)mode.factionPtr=nullptr;
|
|
if(fault==4)logic.respawn.can=false;
|
|
for(int n=1;n<=30;n++){
|
|
logic.DoSpawn_S(1);assert(logic.m_Attempts.Get(1)==n);
|
|
assert(logic.retries==n&&logic.observations==0&&logic.respawn.requests==0);
|
|
assert(!logic.m_Pending.Contains(1)&&!logic.m_AuthorizedPositions.Contains(1));
|
|
assert(!mode.roster.life.UsedLife);
|
|
}
|
|
int previousExhausted=mode.exhausted;
|
|
logic.DoSpawn_S(1);
|
|
assert(mode.exhausted==previousExhausted+1);
|
|
assert(logic.observations==1&&logic.reason==Reason::SPAWN_UNAVAILABLE&&logic.retries==30&&logic.observedBudget==30);
|
|
logic.DoSpawn_S(1);assert(logic.observations==1); // removed attempt record
|
|
assert(!mode.roster.life.UsedLife);mode.factionPtr=&faction;
|
|
}
|
|
// Preparing may take arbitrarily many polls without consuming the budget.
|
|
{Logic logic;mode.active=false;for(int n=0;n<200;n++)logic.DoSpawn_S(1);
|
|
assert(logic.m_Attempts.Get(1)==0&&logic.retries==200&&logic.observations==0&&logic.placementChecks==0);
|
|
mode.active=true;logic.DoSpawn_S(1);assert(logic.m_Attempts.Get(1)==1&&logic.respawn.requests==1);}
|
|
// Temporarily blocked ground recovers normally; async pending calls are inert.
|
|
{Logic logic;logic.clear=false;for(int n=0;n<7;n++)logic.DoSpawn_S(1);
|
|
logic.clear=true;logic.DoSpawn_S(1);
|
|
assert(logic.m_Attempts.Get(1)==8&&logic.respawn.requests==1&&logic.m_Pending.Contains(1));
|
|
assert(logic.m_AuthorizedPositions.Get(1)==42&&!mode.roster.life.UsedLife);
|
|
for(int n=0;n<100;n++)logic.DoSpawn_S(1);
|
|
assert(logic.m_Attempts.Get(1)==8&&logic.respawn.requests==1&&logic.placementChecks==8&&logic.retries==7);}
|
|
// Request refusal does not double-charge; the next eligible attempt advances.
|
|
{Logic logic;logic.respawn.request=false;logic.DoSpawn_S(1);
|
|
assert(logic.m_Attempts.Get(1)==1&&logic.retries==1&&!logic.m_Pending.Contains(1));
|
|
logic.DoSpawn_S(1);assert(logic.m_Attempts.Get(1)==2&&logic.retries==2);}
|
|
// Admission refusal is before budget consumption and cannot spend a life.
|
|
for(bool spent: {false,true}){
|
|
Logic logic;mode.roster.enter=false;mode.roster.life.UsedLife=spent;
|
|
logic.DoSpawn_S(1);assert(logic.observations==1&&logic.placementChecks==0&&logic.retries==0&&logic.observedBudget==0);
|
|
assert(logic.reason==(spent?Reason::SPENT_LIFE:Reason::LATE_EXTRACTION));
|
|
assert(mode.roster.life.UsedLife==spent);mode.roster.enter=true;mode.roster.life.UsedLife=false;
|
|
}
|
|
// Existing body, invalid local context, stopped admission, terminal phase,
|
|
// preparation failure and missing controller cannot request or consume attempts.
|
|
for(int fault=0;fault<6;fault++){
|
|
Logic logic;
|
|
if(fault==0)controller.controlled=true;
|
|
if(fault==1)logic.context=false;
|
|
if(fault==2)logic.m_AdmissionsStopped=true;
|
|
if(fault==3)mode.phase=5;
|
|
if(fault==4)mode.error.empty=false;
|
|
if(fault==5)game.manager.controller=nullptr;
|
|
logic.DoSpawn_S(1);
|
|
assert(!logic.m_Attempts.Contains(1)||logic.m_Attempts.Get(1)==0);
|
|
assert(logic.placementChecks==0&&logic.respawn.requests==0&&logic.retries==0);
|
|
controller.controlled=false;mode.phase=1;mode.error.empty=true;game.manager.controller=&controller;
|
|
}
|
|
}
|
|
''')
|
|
print('PASS: actual admission attempt budget bounds blocked/missing prerequisites; preparation, spent life and async pending remain protected')
|