Files
EscapistsReintroduced/tests/test_intel_discovery_feedback.py
T
2026-10-21 16:36:22 +02:00

115 lines
11 KiB
Python

"""Execute production feedback queue/delivery bodies with fake engine boundaries."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os
P=Path(os.environ.get('ERR_INTEL_FEEDBACK_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelDiscoveryFeedback.c'))
source=P.read_text()
def cpp(s):
s=s.replace('protected ', '').replace('static ', '')
for name in ('Replication','Math','ERR_Phase','ERR_LocationKind','ERR_GameMode','ERR_IntelDiscoveryText','SCR_Global','SCR_ChatComponent'):
s=s.replace(name+'.',name+'::')
s=s.replace('ERR_LocationComponent location','ERR_LocationComponent* location')
s=s.replace('location.', 'location->').replace('GetWorld().','GetWorld()->').replace('GetGame().','GetGame()->')
s=s.replace('int seen;','int seen=0;').replace('int identified;','int identified=0;').replace('int rough;','int rough=0;')
s=s.replace('foreach (ERR_LocationComponent* location, int knowledge : m_ERRIntelPending)','for (auto [location, knowledge] : m_ERRIntelPending.values)')
s=s.replace('Rpc(ERR_ReceiveDiscoveryFeedback, ', 'Rpc(')
s=s.replace('OnLocationDiscovered().Remove(ERR_QueueDiscoveryFeedback);','OnLocationDiscovered().Remove(0);')
s=s.replace('OnTick().Remove(ERR_FlushDiscoveryFeedback);','OnTick().Remove(0);')
s=s.replace('string.Format(', 'Format(')
return s
methods='\n'.join(cpp(body(P,s)) for s in ('protected void ERR_QueueDiscoveryFeedback(', 'protected void ERR_FlushDiscoveryFeedback(', 'protected void ERR_ReceiveDiscoveryFeedback(', 'protected void ERR_StopDiscoveryFeedback(', 'void ERR_IntelExhausted(', 'void ERR_RadioIntelWaiting('))
run(r'''
#include <cassert>
#include <string>
#include <map>
#include <vector>
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}bool Contains(const char* text)const{return find(text)!=npos;}};
string Format(string text,int identified,int rough){text.replace(text.find("%1"),2,std::to_string(identified));text.replace(text.find("%2"),2,std::to_string(rough));return text;}
struct Replication {static bool server;static bool IsServer(){return server;}};bool Replication::server=true;
struct SCR_Global {static bool edit;static bool IsEditMode(){return edit;}};bool SCR_Global::edit=false;
struct SCR_ChatComponent {static std::vector<string> messages;static void RadioProtocolMessage(string text){messages.push_back(text);}};std::vector<string> SCR_ChatComponent::messages;
struct Math {static float Max(float a,float b){return a>b?a:b;}};
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,EXTRACTION};}
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}string Id(){return "outpost-id";}};
template<class K,class V> struct Map {std::map<K,V> values;bool Find(K k,V& v){auto i=values.find(k);if(i==values.end())return false;v=i->second;return true;}bool Contains(K k){return values.count(k);}int Count(){return values.size();}bool IsEmpty(){return values.empty();}void Set(K k,V v){values[k]=v;}void Clear(){values.clear();}};
struct World {float time=0;float GetWorldTime(){return time;}} world;
struct Game {bool controller=true;bool GetPlayerController(){return controller;}} game;
Game* currentGame=&game;Game* GetGame(){return currentGame;}
struct Events {int removes=0;void Remove(int){removes++;}};
struct ERR_IntelDiscoveryText {static
'''+cpp(body(P,'static string Type('))+'static '+cpp(body(P,'static string Message('))+r'''
};
struct ERR_GameMode {
static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}
bool active=true;int phase=ERR_Phase::RADIO_SEARCH;bool Active(){return active;}int Phase(){return phase;}World* GetWorld(){return &world;}
Map<ERR_LocationComponent*,int> m_ERRIntelSeen,m_ERRIntelPending;
float m_ERRIntelMessageAt=0,m_ERRIntelExhaustedAt=0;bool m_ERRRadioIntelWaitingReported=false,m_ERRIntelFeedbackStopped=false,m_ERRIntelDuplicateReported=false,m_ERRIntelExhaustionReported=false;int m_ERRIntelMessageSequence=0,m_ERRIntelReceivedSequence=0;
Events discovered,tick;Events& OnLocationDiscovered(){return discovered;}Events& OnTick(){return tick;}
int rpcCount=0;void Rpc(int sequence,string message){rpcCount++;ERR_ReceiveDiscoveryFeedback(sequence,message);}
'''+methods+r'''
};
ERR_GameMode* ERR_GameMode::current=nullptr;
int main(){
ERR_GameMode m;ERR_GameMode::current=&m;ERR_LocationComponent site;
// Do not reveal startup sites, prison or automatic helicopter disclosure.
m.phase=ERR_Phase::PREPARING;m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());
m.phase=ERR_Phase::RADIO_SEARCH;for(int kind:{ERR_LocationKind::PRISON,ERR_LocationKind::EXTRACTION}){site.kind=kind;m.ERR_QueueDiscoveryFeedback(&site,0,1);}assert(m.m_ERRIntelPending.IsEmpty());
site.kind=ERR_LocationKind::OUTPOST;Replication::server=false;m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());Replication::server=true;
m.ERR_QueueDiscoveryFeedback(nullptr,0,1);m.ERR_QueueDiscoveryFeedback(&site,1,1);m.ERR_QueueDiscoveryFeedback(&site,0,2);m.ERR_QueueDiscoveryFeedback(&site,-1,1);assert(m.m_ERRIntelPending.IsEmpty());
// Rough clues do not disclose labels, kinds or coordinates. Listen callbacks dedupe.
m.ERR_QueueDiscoveryFeedback(&site,0,1);m.ERR_FlushDiscoveryFeedback(1499);assert(SCR_ChatComponent::messages.empty());
m.ERR_FlushDiscoveryFeedback(1500);assert(m.rpcCount==1&&SCR_ChatComponent::messages.size()==1);assert(SCR_ChatComponent::messages.back().find("Hidden")==string::npos);assert(SCR_ChatComponent::messages.back().find("New location marked. Type unknown.")!=string::npos);
m.ERR_QueueDiscoveryFeedback(&site,0,1);assert(m.m_ERRIntelPending.IsEmpty());
site.knowledge=2;world.time=2000;m.ERR_QueueDiscoveryFeedback(&site,1,2);m.ERR_FlushDiscoveryFeedback(6499);assert(m.rpcCount==1);
m.ERR_FlushDiscoveryFeedback(6500);assert(m.rpcCount==2&&SCR_ChatComponent::messages.back()=="Hey! A outpost was identified in the intel!");
// A same-batch rough→exact update becomes one final identified location.
ERR_GameMode batch;ERR_GameMode::current=&batch;ERR_LocationComponent a,b;world.time=0;
batch.ERR_QueueDiscoveryFeedback(&a,0,1);a.knowledge=2;batch.ERR_QueueDiscoveryFeedback(&a,1,2);batch.ERR_QueueDiscoveryFeedback(&b,0,1);batch.ERR_FlushDiscoveryFeedback(1500);
assert(SCR_ChatComponent::messages.back().find("Hey! A outpost was identified in the intel!")!=string::npos && SCR_ChatComponent::messages.back().find("New location marked. Type unknown.")!=string::npos);
// End/restart cancels pending output and removes both hooks, new mode is clean.
world.time=2000;b.knowledge=2;batch.ERR_QueueDiscoveryFeedback(&b,1,2);batch.ERR_StopDiscoveryFeedback();assert(batch.m_ERRIntelSeen.IsEmpty()&&batch.m_ERRIntelPending.IsEmpty()&&batch.discovered.removes==1&&batch.tick.removes==1);
int calls=batch.rpcCount;batch.ERR_FlushDiscoveryFeedback(99999);batch.ERR_QueueDiscoveryFeedback(&b,0,2);assert(batch.rpcCount==calls&&batch.m_ERRIntelPending.IsEmpty());
ERR_GameMode next;ERR_GameMode::current=&next;auto shown=SCR_ChatComponent::messages.size();batch.ERR_ReceiveDiscoveryFeedback(999,"old round");assert(SCR_ChatComponent::messages.size()==shown);
// Remote delivery gates old sequence, absent controller, terminal and editor state.
Replication::server=false;next.ERR_ReceiveDiscoveryFeedback(1,"remote");next.ERR_ReceiveDiscoveryFeedback(1,"duplicate");assert(SCR_ChatComponent::messages.size()==shown+1);
game.controller=false;next.ERR_ReceiveDiscoveryFeedback(2,"absent");game.controller=true;next.active=false;next.ERR_ReceiveDiscoveryFeedback(2,"ended");next.active=true;SCR_Global::edit=true;next.ERR_ReceiveDiscoveryFeedback(2,"editor");SCR_Global::edit=false;assert(next.m_ERRIntelReceivedSequence==1);
currentGame=nullptr;next.ERR_ReceiveDiscoveryFeedback(2,"no game");currentGame=&game;
next.ERR_ReceiveDiscoveryFeedback(2,"new");assert(SCR_ChatComponent::messages.size()==shown+2);
// Queue is bounded even for malformed/unusually large authored scenarios.
Replication::server=true;ERR_GameMode bounded;ERR_GameMode::current=&bounded;ERR_LocationComponent many[65];for(auto& item:many)bounded.ERR_QueueDiscoveryFeedback(&item,0,1);assert(bounded.m_ERRIntelSeen.Count()==64&&bounded.m_ERRIntelPending.Count()==64);
assert(ERR_IntelDiscoveryText::Type(ERR_LocationKind::MORTAR,"any")=="mortar site");
ERR_GameMode::current=&m;m.ERR_IntelExhausted();assert(SCR_ChatComponent::messages.back()=="Every known location has already been identified. This intel doesn't help us.");
assert(ERR_IntelDiscoveryText::Message(0,0,"secret").empty());assert(ERR_IntelDiscoveryText::Message(1,0,"")=="Hey! A location was identified in the intel!");
// Recovering a stack of spent-topic folders performs one exhaustion broadcast,
// not one chat line/RPC per physical document. Both variants share the budget.
ERR_GameMode surplus;ERR_GameMode::current=&surplus;world.time=10000;
for(int i=0;i<20;i++)surplus.ERR_IntelExhausted(i%2==0);
assert(surplus.rpcCount==1&&surplus.m_ERRIntelMessageSequence==1);
// Actual new discovery still follows its own coalescing window immediately.
world.time=10000;ERR_LocationComponent fresh;surplus.ERR_QueueDiscoveryFeedback(&fresh,0,1);
surplus.ERR_FlushDiscoveryFeedback(11500);assert(surplus.rpcCount==2);
assert(SCR_ChatComponent::messages.back()=="New location marked. Type unknown.");
world.time=14999;surplus.ERR_IntelExhausted();assert(surplus.rpcCount==2);
world.time=15000;surplus.ERR_IntelExhausted();assert(surplus.rpcCount==3&&surplus.m_ERRIntelMessageSequence==3);
// Later camps do not repeat either unhelpful-intel explanation, even hours later.
for(int i=0;i<100;i++){world.time=20000+i*60000;surplus.ERR_IntelExhausted(i%2==0);}
assert(surplus.rpcCount==3);
world.time=20000;Replication::server=false;surplus.ERR_IntelExhausted();assert(surplus.rpcCount==3);Replication::server=true;
surplus.ERR_StopDiscoveryFeedback();surplus.ERR_IntelExhausted();assert(surplus.rpcCount==3);
ERR_GameMode freshRound;ERR_GameMode::current=&freshRound;world.time=0;freshRound.ERR_IntelExhausted();assert(freshRound.rpcCount==1);
// Fixed-document waiting feedback is once per round, regardless of scan count.
ERR_GameMode held;ERR_GameMode::current=&held;
for(int i=0;i<100;i++)held.ERR_RadioIntelWaiting();
assert(held.rpcCount==1&&held.m_ERRIntelMessageSequence==1);
ERR_GameMode stopped;ERR_GameMode::current=&stopped;stopped.ERR_StopDiscoveryFeedback();stopped.ERR_RadioIntelWaiting();assert(stopped.rpcCount==0);
ERR_GameMode client;ERR_GameMode::current=&client;Replication::server=false;client.ERR_RadioIntelWaiting();assert(client.rpcCount==0);
}
''')
for required in ('OnLocationDiscovered().Insert(ERR_QueueDiscoveryFeedback)', 'OnTick().Insert(ERR_FlushDiscoveryFeedback)', 'OnStop().Insert(ERR_StopDiscoveryFeedback)', '[RplRpc(RplChannel.Reliable, RplRcver.Broadcast)]'):
assert required in source
for forbidden in ('HideHint(', 'CallLater(', 'Discover(', 'SetKnowledge('):
assert forbidden not in source
print('PASS: production Intel feedback coalescing, rough secrecy, rate limit, bounded dedupe, local/remote delivery and round cleanup; native UI/RPC not exercised')