Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
109 lines
11 KiB
Python
109 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('))
|
|
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_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);
|
|
|
|
}
|
|
''')
|
|
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')
|