Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
175 lines
17 KiB
Python
175 lines
17 KiB
Python
"""Execute both production monitor bodies; native hint/camera boundaries are fakes."""
|
|
from policy_harness import ROOT, body, run
|
|
from pathlib import Path
|
|
import os
|
|
GUIDE=Path(os.environ.get('ERR_GUIDE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerGuide.c'))
|
|
OBSERVER=Path(os.environ.get('ERR_OBSERVER_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'))
|
|
PROBE=Path(os.environ.get('ERR_OBSERVER_PROBE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_UnconsciousCameraProbe.c'))
|
|
def cpp(text):
|
|
text=text.replace(' = null;', ' = nullptr;').replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;')
|
|
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','RplId'):
|
|
text=text.replace(name+'.',name+'::')
|
|
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','SCR_EditorManagerEntity','CameraManager'):
|
|
text=text.replace(typename+' ',typename+'* ')
|
|
for variable in ('mode','hints','resource','respawn','manager','editor'):
|
|
text=text.replace(variable+'.',variable+'->')
|
|
text=text.replace('GetCallqueue().','GetCallqueue()->').replace('Remove(ERR_UpdateGuide)','Remove(this)').replace('Remove(ERR_UpdateObserverClient)','Remove(this)').replace('CallLater(ERR_UpdateGuide, 1000, true)','CallLater(this, 1000, true)')
|
|
text=text.replace('GetGame().','GetGame()->').replace('EntitySpawnParams params = new EntitySpawnParams();','EntitySpawnParams params;')
|
|
return text
|
|
methods=cpp(body(GUIDE,'protected void ERR_StartGuide(')).replace('ERR_StartGuide()', 'StartActual()')+'\n'+cpp(body(GUIDE,'override void OnControlledEntityChanged('))+'\n'+cpp(body(GUIDE,'protected void ERR_UpdateGuide('))+'\n'+cpp(body(OBSERVER,'protected void ERR_UpdateObserverClient('))
|
|
methods+='\n'+cpp(body(PROBE,'string ERR_ProbeAcceptedRecoveryStatus('))
|
|
guidance=cpp(body(OBSERVER,'static string Message('))+'\n'+cpp(body(OBSERVER,'static bool Refresh('))
|
|
run(r'''#include <cassert>
|
|
#include <string>
|
|
#include <set>
|
|
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
|
|
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
|
|
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
|
|
struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*;
|
|
struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}static bool CanFight(Actor* a){return Alive(a)&&!a->unconscious;}};
|
|
struct ERR_GameMode {string recovery;string RecoveryStatus(){return recovery;}int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode;
|
|
ERR_GameMode* ERR_GameMode::current=&mode;
|
|
struct ERR_ObjectiveText {static string Description(ERR_GameMode* m,bool){return m->message;}static string Title(int){return "objective";}};
|
|
struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int){attempts++;if(!accepts)return false;shown++;last=text;return true;}} hints;
|
|
SCR_HintManagerComponent* SCR_HintManagerComponent::current=&hints;
|
|
struct ERR_ObserverRules {
|
|
'''+cpp(body(OBSERVER,'static bool View('))+r'''
|
|
};
|
|
struct ERR_ObserverGuidance {
|
|
'''+guidance+r'''
|
|
};
|
|
struct Vec {Vec operator+(const char*)const{return {};}};
|
|
struct EntitySpawnParams {int TransformMode=0;Vec Transform[4];};
|
|
namespace ETransformMode {enum {WORLD};}
|
|
struct Resource {bool valid=true;static Resource* current;static Resource* Load(const char*){return current;}bool IsValid(){return valid;}} resource;
|
|
Resource* Resource::current=&resource;
|
|
struct SCR_ManualCamera {static SCR_ManualCamera* Cast(SCR_ManualCamera* c){return c;}} camera;
|
|
struct SCR_EditorManagerEntity {bool opened=false;inline static SCR_EditorManagerEntity* current=nullptr;static SCR_EditorManagerEntity* GetInstance(){return current;}bool IsOpened(){return opened;}};
|
|
struct CameraManager {void SetCamera(SCR_ManualCamera* value){current=value;}SCR_ManualCamera* current=&camera;SCR_ManualCamera* CurrentCamera(){return current;}} manager;
|
|
struct SCR_RespawnSystemComponent {int destroys=0;static SCR_RespawnSystemComponent* current;static SCR_RespawnSystemComponent* GetInstance(){return current;}void DestroyLoadingPlaceholder(){destroys++;}} respawn;
|
|
SCR_RespawnSystemComponent* SCR_RespawnSystemComponent::current=&respawn;
|
|
struct RplId {static int Invalid(){return -1;}};
|
|
struct Queue {std::set<void*> scheduled;int added=0;void CallLater(void* owner,int,bool){added++;scheduled.insert(owner);}void Remove(void* owner){scheduled.erase(owner);}} queue;
|
|
struct Game {Queue* GetCallqueue(){return &queue;}void* local=nullptr;void* GetPlayerController(){return local;}int spawns=0;SCR_ManualCamera* SpawnEntityPrefabLocal(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
|
bool gameAvailable=true;Game* GetGame(){return gameAvailable?&game:nullptr;}
|
|
struct Controller {
|
|
Controller(){game.local=this;}
|
|
void ERR_UpdateRecoveryCamera(ERR_GameMode*){}
|
|
int nativeChanges=0,starts=0;bool m_ERRGuideStarted=false;void ERR_StartGuide(){starts++;StartActual();}
|
|
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;bool m_ERRObserverMonitor=false;Actor* m_ERRRecoveryBody=nullptr;float m_ERRRecoveryDeadline=-1;
|
|
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
|
|
SCR_ManualCamera* m_ERRObserverCamera=nullptr;Vec m_ERRObserverAnchor;
|
|
int m_ERRObservedId=1;bool m_ERRObserverReady=false;
|
|
string m_ERRShownRecoveryStatus;
|
|
int m_ERRShownObserverReason=0;bool m_ERRShownObserverAvailable=false;int closes=0;
|
|
Actor* GetControlledEntity(){return controlled;}int ERR_ObserverState(){return m_ERRObserverReason;}
|
|
void ERR_CloseObserverCamera(){closes++;m_ERRObserverReady=false;m_ERRShownRecoveryStatus="";m_ERRObserverCamera=nullptr;}
|
|
'''+methods+r'''
|
|
};
|
|
void reset(){queue=Queue();gameAvailable=true;mode=ERR_GameMode();ERR_GameMode::current=&mode;hints=SCR_HintManagerComponent();SCR_HintManagerComponent::current=&hints;actor=Actor();Resource::current=&resource;manager.current=&camera;}
|
|
int main(){
|
|
reset();Controller g;
|
|
SCR_HintManagerComponent::current=nullptr;g.ERR_UpdateGuide();assert(g.m_ERRGuidePhase==-1&&hints.attempts==0);
|
|
SCR_HintManagerComponent::current=&hints;hints.accepts=false;
|
|
g.ERR_UpdateGuide();g.ERR_UpdateGuide();assert(hints.attempts==2&&hints.shown==0&&g.m_ERRGuidePhase==-1&&g.m_ERRGuideMessage.empty());
|
|
hints.accepts=true;g.ERR_UpdateGuide();assert(hints.shown==1&&g.m_ERRGuidePhase==mode.phase&&g.m_ERRGuideMessage==mode.message);
|
|
g.ERR_UpdateGuide();assert(hints.attempts==3);
|
|
mode.message="";g.ERR_UpdateGuide();assert(hints.attempts==3&&g.m_ERRGuideMessage=="prison");
|
|
// Same-phase content updates are retryable and do not erase the delivered cache.
|
|
mode.message="transmission interrupted";hints.accepts=false;g.ERR_UpdateGuide();assert(g.m_ERRGuideMessage=="prison");
|
|
hints.accepts=true;g.ERR_UpdateGuide();assert(hints.shown==2&&g.m_ERRGuideMessage==mode.message);
|
|
mode.phase=ERR_Phase::RADIO_SEARCH;g.ERR_UpdateGuide();assert(hints.shown==3);
|
|
// Reacquiring a character in the same phase clears delivery once; native
|
|
// lifecycle and the existing monitor remain responsible for actual delivery.
|
|
Actor replacement;int shown=hints.shown;
|
|
g.OnControlledEntityChanged(&actor,&replacement);g.controlled=&replacement;
|
|
assert(g.nativeChanges==1&&g.starts==1&&g.m_ERRGuidePhase==-1);
|
|
hints.accepts=false;g.ERR_UpdateGuide();assert(hints.shown==shown&&g.m_ERRGuidePhase==-1);
|
|
hints.accepts=true;g.ERR_UpdateGuide();assert(hints.shown==shown+1);
|
|
g.ERR_UpdateGuide();assert(hints.shown==shown+1);
|
|
g.OnControlledEntityChanged(&replacement,&replacement);g.ERR_UpdateGuide();assert(hints.shown==shown+1);
|
|
g.OnControlledEntityChanged(&replacement,nullptr);g.controlled=nullptr;
|
|
g.ERR_UpdateGuide();assert(hints.shown==shown+1);
|
|
g.OnControlledEntityChanged(nullptr,&replacement);g.controlled=&replacement;
|
|
g.ERR_UpdateGuide();assert(hints.shown==shown+2&&g.nativeChanges==4&&g.starts==4);
|
|
// Explicit observation, death and absence suppress ordinary phase instructions.
|
|
for(int excluded=0;excluded<3;excluded++){
|
|
Controller c;if(excluded==0)c.m_ERRObserverReason=ERR_ObserverReason::SPENT_LIFE;
|
|
if(excluded==1){actor.alive=false;}if(excluded==2){c.controlled=nullptr;}
|
|
int attempts=hints.attempts;c.ERR_UpdateGuide();assert(hints.attempts==attempts&&c.m_ERRGuidePhase==-1);
|
|
actor.alive=true;c.controlled=&actor;c.m_ERRObserverReason=ERR_ObserverReason::NONE;c.ERR_UpdateGuide();assert(hints.attempts==attempts+1);
|
|
}
|
|
// Unconscious living prisoners retain recovery guidance. Preparing needs no body.
|
|
Controller unconscious;actor.unconscious=true;unconscious.ERR_UpdateGuide();assert(unconscious.m_ERRGuidePhase==mode.phase);actor.unconscious=false;
|
|
Controller preparing;preparing.controlled=nullptr;mode.phase=ERR_Phase::PREPARING;mode.message="preparing";preparing.ERR_UpdateGuide();assert(preparing.m_ERRGuideMessage=="preparing");
|
|
ERR_GameMode::current=nullptr;int attempts=hints.attempts;preparing.ERR_UpdateGuide();assert(hints.attempts==attempts);
|
|
// A prior local controller can remain alive until native teardown. Its
|
|
// recurring callback must not write global hints after ownership changed.
|
|
reset();Controller former;former.StartActual();former.ERR_UpdateGuide();Controller current;current.StartActual();
|
|
assert(queue.scheduled.count(&former)==1&&queue.scheduled.count(¤t)==1);
|
|
mode.phase=ERR_Phase::PREPARING;mode.message="new round preparing";
|
|
former.controlled=nullptr;int beforeStale=hints.attempts;
|
|
former.ERR_UpdateGuide();assert(hints.attempts==beforeStale);
|
|
assert(!former.m_ERRGuideStarted&&queue.scheduled.count(&former)==0&&queue.scheduled.count(¤t)==1);
|
|
assert(former.m_ERRGuidePhase==ERR_Phase::PRISON&&former.m_ERRGuideMessage=="prison");
|
|
current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1&¤t.m_ERRGuideMessage==mode.message);
|
|
current.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1);
|
|
mode.phase=ERR_Phase::RADIO_SEARCH;mode.message="new radio clue";former.controlled=&actor;
|
|
former.ERR_UpdateGuide();assert(hints.attempts==beforeStale+1);
|
|
game.local=&former;former.StartActual();assert(former.m_ERRGuideStarted&&queue.scheduled.count(&former)==1);int added=queue.added;former.StartActual();assert(queue.added==added);hints.accepts=false;former.ERR_UpdateGuide();assert(former.m_ERRGuideMessage=="prison");
|
|
hints.accepts=true;former.ERR_UpdateGuide();assert(former.m_ERRGuideMessage==mode.message);
|
|
int beforeNoGame=hints.attempts;gameAvailable=false;former.ERR_UpdateGuide();assert(hints.attempts==beforeNoGame);gameAvailable=true;
|
|
reset();Controller s;s.controlled=nullptr;s.m_ERRObserverReason=ERR_ObserverReason::SPENT_LIFE;
|
|
// An open editor owns its camera; ordinary observer setup waits for closure.
|
|
SCR_EditorManagerEntity editor;editor.opened=true;SCR_EditorManagerEntity::current=&editor;
|
|
int priorSpawns=game.spawns;s.ERR_UpdateObserverClient();assert(game.spawns==priorSpawns&&!s.m_ERRObserverCamera);
|
|
SCR_EditorManagerEntity::current=nullptr;
|
|
// Actual camera setup executes; missing manager cannot consume delivery state.
|
|
SCR_HintManagerComponent::current=nullptr;s.ERR_UpdateObserverClient();assert(s.m_ERRObserverCamera==&camera&&!s.m_ERRObserverReady);
|
|
SCR_HintManagerComponent::current=&hints;hints.accepts=false;s.ERR_UpdateObserverClient();s.ERR_UpdateObserverClient();assert(hints.attempts==2&&!s.m_ERRObserverReady&&s.m_ERRShownObserverReason==0);
|
|
hints.accepts=true;s.ERR_UpdateObserverClient();assert(hints.shown==1&&s.m_ERRObserverReady&&s.m_ERRShownObserverReason==ERR_ObserverReason::SPENT_LIFE&&s.m_ERRShownObserverAvailable);
|
|
s.ERR_UpdateObserverClient();assert(hints.attempts==3);
|
|
// Reason changes and a genuinely empty survivor set retry independently.
|
|
s.m_ERRObserverReason=ERR_ObserverReason::LATE_EXTRACTION;hints.accepts=false;s.ERR_UpdateObserverClient();assert(s.m_ERRShownObserverReason==ERR_ObserverReason::SPENT_LIFE);
|
|
hints.accepts=true;s.ERR_UpdateObserverClient();assert(s.m_ERRShownObserverReason==ERR_ObserverReason::LATE_EXTRACTION&&hints.shown==2);
|
|
s.m_ERRObservedId=RplId::Invalid();hints.accepts=false;s.ERR_UpdateObserverClient();assert(s.m_ERRShownObserverAvailable);
|
|
hints.accepts=true;s.ERR_UpdateObserverClient();assert(!s.m_ERRShownObserverAvailable&&hints.last.find("No teammate or body")!=string::npos);
|
|
s.ERR_UpdateObserverClient();assert(hints.shown==3);
|
|
// A living body closes observation without attempting a spectator hint.
|
|
s.controlled=&actor;attempts=hints.attempts;s.ERR_UpdateObserverClient();assert(s.closes==1&&!s.m_ERRObserverReady&&hints.attempts==attempts);
|
|
s.controlled=nullptr;s.ERR_UpdateObserverClient();assert(hints.shown==4&&s.m_ERRObserverReady);
|
|
// Unconscious observation preserves the original body, delivers recovery wording,
|
|
// and closes as soon as the local body wakes even before server metadata clears.
|
|
reset();Controller casualty;actor.unconscious=true;casualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;
|
|
casualty.ERR_UpdateObserverClient();assert(casualty.m_ERRObserverCamera==&camera&&casualty.controlled==&actor&&hints.last.find("You are unconscious")!=string::npos);
|
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==1);
|
|
// The phase-guide explicitly excludes this server-designated unconscious
|
|
// observer; the existing observer monitor must deliver recovery/defeat facts.
|
|
int attemptsBeforeGuide=hints.attempts;casualty.ERR_UpdateGuide();assert(hints.attempts==attemptsBeforeGuide);
|
|
mode.recovery="Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==2&&hints.last.find(mode.recovery)!=string::npos&&casualty.ERR_ProbeAcceptedRecoveryStatus()==mode.recovery);
|
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==2); // stable status never repeats
|
|
mode.recovery="Treatment or natural recovery is possible; the round continues.";hints.accepts=false;
|
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==2&&casualty.m_ERRShownRecoveryStatus.find("30 uninterrupted")!=string::npos);
|
|
hints.accepts=true;casualty.ERR_UpdateObserverClient();assert(hints.shown==3&&hints.last.find(mode.recovery)!=string::npos);
|
|
mode.recovery="A conscious prisoner's reconnection may provide help.";SCR_HintManagerComponent::current=nullptr;
|
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==3&&casualty.m_ERRShownRecoveryStatus.find("natural recovery")!=string::npos);
|
|
SCR_HintManagerComponent::current=&hints;casualty.ERR_UpdateObserverClient();assert(hints.shown==4&&hints.last.find(mode.recovery)!=string::npos);
|
|
mode.recovery="";casualty.ERR_UpdateObserverClient();assert(hints.shown==5&&casualty.m_ERRShownRecoveryStatus.empty()&&hints.last.find("reconnection")==string::npos);
|
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==5);
|
|
// Waking closes the view; a new status cannot keep a recovered observer hint alive.
|
|
mode.recovery="remaining casualty warning";actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==5&&casualty.ERR_ProbeAcceptedRecoveryStatus().empty());
|
|
// An old controller cannot consume the replacement's recovery-status update.
|
|
reset();Controller oldCasualty;actor.unconscious=true;oldCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;oldCasualty.ERR_UpdateObserverClient();
|
|
Controller newCasualty;newCasualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;mode.recovery="recovery warning after reconnect";int beforeOld=hints.shown;
|
|
oldCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld&&oldCasualty.m_ERRShownRecoveryStatus.empty());
|
|
newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&hints.last.find(mode.recovery)!=string::npos);
|
|
mode.active=false;mode.recovery="old round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+1&&!newCasualty.m_ERRObserverReady&&newCasualty.m_ERRShownRecoveryStatus.empty());
|
|
// A later active round's first observer hint contains only its current fact.
|
|
mode.active=true;mode.recovery="new round recovery";newCasualty.ERR_UpdateObserverClient();assert(hints.shown==beforeOld+2&&hints.last.find("new round recovery")!=string::npos&&hints.last.find("old round recovery")==string::npos);
|
|
}
|
|
''')
|
|
for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):
|
|
assert 'CallLater' not in body(path,signature), 'Delivery retry uses the existing monitor only'
|
|
print('PASS: actual guide ownership and scoped timer cancellation, current-controller preservation/reacquisition, hint retry/dedup and observer boundaries; native UI not exercised')
|