Files
EscapistsReintroduced/tests/test_intel_cleanup.py
T
2026-10-20 16:13:00 +02:00

107 lines
8.7 KiB
Python

"""Execute production spent-folder cleanup against explicit native API doubles."""
import re
from policy_harness import ROOT, body, run
p = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c'
callback = body(p, 'class ERR_IntelDeleteCallback')
callback = callback.replace('class ERR_IntelDeleteCallback : ScriptedInventoryOperationCallback', 'struct ERR_IntelDeleteCallback')
callback = callback.replace('protected ', '').replace('override ', '').replace('bool Finished;', 'bool Finished=false;').replace('bool Succeeded;', 'bool Succeeded=false;')
code = '\n'.join(body(p, s) for s in ('protected void QueueCleanup(', 'protected bool ProcessCleanup(', 'void Tick(float now)', 'void Stop()'))
code = code.replace('protected ', '').replace('foreach (', 'for (').replace('null', 'nullptr').replace('array<IEntity>', 'Array<IEntity>')
for typ in ('ERR_SpentIntelCleanup', 'InventoryStorageManagerComponent'):
code = re.sub(r'\b' + typ + r' (\w+)', typ + r'* \1', code)
for ns in ('ERR_GameMode', 'Replication', 'ERR_Actors', 'SCR_FactionManager', 'ERR_InfantryAmmoTransfer', 'InventoryStorageManagerComponent', 'Math', 'LogLevel'):
code = code.replace(ns + '.', ns + '::')
for obj in ('request', 'player', 'folder', 'inventory', 'm_Mode'):
code = re.sub(r'\b' + obj + r'\.', obj + '->', code)
code = code.replace('Folder.', 'Folder->').replace('Operation.', 'Operation->').replace('Players().', 'Players()->')
code = re.sub(r'FindComponent\((\w+)\)', r'FindComponent("\1")', code)
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
#include <string>
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}int Count(){return this->size();}void Clear(){this->clear();}void Remove(int i){this->erase(this->begin()+i);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
''' + callback + r''';
struct Entity;using IEntity=Entity*;
struct InventoryStorageManagerComponent{
Array<IEntity>items;bool accept=true;int calls=0,immediate=0;ERR_IntelDeleteCallback*last=nullptr;
static InventoryStorageManagerComponent*Cast(void*p){return (InventoryStorageManagerComponent*)p;}
bool Contains(IEntity item){return items.Contains(item);}
bool TryDeleteItem(IEntity item,ERR_IntelDeleteCallback*cb){assert(Contains(item));assert(!last||last->Finished);calls++;if(!accept)return false;last=cb;if(immediate==1)cb->OnComplete();if(immediate==2)cb->OnFailed();return true;}
};
struct Entity{bool alive=true,canonical=true,objective=false;int faction=1;IEntity holder=nullptr;InventoryStorageManagerComponent*inventory=nullptr;void*FindComponent(std::string type){if(type=="ERR_DocumentComponent")return objective?this:nullptr;return inventory;}};
struct Roster{IEntity CurrentActor(IEntity p){return p&&p->canonical?p:nullptr;}};
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;Roster roster;bool active=true;int faction=1;static ERR_GameMode*Get(){return current;}bool Active(){return active;}Roster*Players(){return &roster;}int PlayerFaction(){return faction;}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace ERR_Actors{Array<IEntity>players;void Players(Array<IEntity>&out){out=players;}bool Alive(IEntity p){return p&&p->alive;}}
namespace SCR_FactionManager{int SGetFaction(IEntity p){return p->faction;}}
namespace ERR_InfantryAmmoTransfer{bool StoredUnder(IEntity item,IEntity player){return item->holder==player;}}
namespace Math{int Min(int a,int b){return std::min(a,b);}}
namespace LogLevel{int WARNING=1;}int warnings=0;void Print(std::string,int){warnings++;}
struct ERR_SpentIntelCleanup{IEntity Folder=nullptr;ERR_IntelDeleteCallback*Operation=nullptr;int Attempts=0;float NextAttempt=0,Deadline=0;bool TimeoutReported=false;};
struct Service{ERR_GameMode*m_Mode=nullptr;Array<ERR_SpentIntelCleanup*>m_Cleanup;Array<IEntity>m_Spent;int m_CleanupCursor=0,recovers=0;float m_CleanupNow=0;bool m_Stopped=false;void Recover(IEntity){recovers++;}
''' + code + r'''
};
struct Fixture{ERR_GameMode mode;Service service;Entity player,folder;InventoryStorageManagerComponent inventory;
Fixture(){Replication::server=true;ERR_GameMode::current=&mode;service.m_Mode=&mode;player.inventory=&inventory;folder.holder=&player;inventory.items.Insert(&folder);ERR_Actors::players={&player};service.QueueCleanup(&folder,1000);}
ERR_SpentIntelCleanup*job(){return service.m_Cleanup[0];}
};
int main(){
// Callback terminal state cannot be changed by duplicate/late notifications.
ERR_IntelDeleteCallback cb;cb.OnComplete();cb.OnAllFailed();assert(cb.Finished&&cb.Succeeded);
ERR_IntelDeleteCallback failed;failed.OnFailed();failed.OnAllComplete();assert(failed.Finished&&!failed.Succeeded);
{Fixture f;f.service.Tick(1000);auto cb=f.inventory.last;assert(f.inventory.calls==1&&f.job()->Attempts==1);
f.service.Tick(6000);assert(f.inventory.calls==1); // one outstanding native operation
cb->OnComplete();f.service.Tick(7000);assert(f.service.m_Cleanup.empty()&&f.inventory.calls==1);}
// Rejection is retried slowly and stops after three requests; no force deletion.
{Fixture f;f.inventory.accept=false;f.service.Tick(1000);f.service.Tick(5999);assert(f.inventory.calls==1);
f.service.Tick(6000);assert(f.inventory.calls==2);f.service.Tick(11000);assert(f.inventory.calls==3);
f.service.Tick(11001);assert(f.service.m_Cleanup.empty());}
// An accepted request can fail later; only a definitive callback permits retry.
{Fixture f;f.service.Tick(1000);f.inventory.last->OnFailed();f.service.Tick(2000);
f.service.Tick(6999);assert(f.inventory.calls==1);f.service.Tick(7000);assert(f.inventory.calls==2);
f.inventory.last->OnComplete();f.service.Tick(8000);assert(f.service.m_Cleanup.empty());}
// A missing callback stays quarantined after deadline and logs only once.
{Fixture f;f.service.Tick(1000);int before=warnings;f.service.Tick(61000);f.service.Tick(100000);
assert(f.inventory.calls==1&&warnings==before+1&&!f.service.m_Cleanup.empty());
f.inventory.last->OnFailed();f.service.Tick(101000);assert(f.service.m_Cleanup.empty()&&f.inventory.calls==1);}
// Deleted native entity completes without dereferencing a stale item.
{Fixture f;f.job()->Folder=nullptr;f.service.Tick(1000);assert(f.service.m_Cleanup.empty()&&f.inventory.calls==0);}
// Moving a folder never authorizes removal from a corpse, NPC, or ground.
for(int fault=0;fault<6;fault++){Fixture f;
if(fault==0)f.folder.holder=nullptr;
if(fault==1)f.player.alive=false;
if(fault==2)f.player.canonical=false;
if(fault==3)f.player.faction=2;
if(fault==4)f.inventory.items.Clear();
if(fault==5)f.player.inventory=nullptr;
f.service.Tick(1000);assert(f.inventory.calls==0);f.service.Tick(61000);assert(f.service.m_Cleanup.empty());}
{Fixture f;Entity next;InventoryStorageManagerComponent other;next.inventory=&other;other.items.Insert(&f.folder);
f.folder.holder=&next;f.inventory.items.Clear();ERR_Actors::players.Insert(&next);f.service.Tick(1000);
assert(f.inventory.calls==0&&other.calls==1);}
// Objective documents never enter the queue, even if a caller asks directly.
{Fixture f;Entity objective;objective.objective=true;f.service.QueueCleanup(&objective,1000);assert(f.service.m_Cleanup.size()==1);
f.folder.objective=true;f.service.Tick(1000);assert(f.inventory.calls==0&&f.service.m_Cleanup.empty());}
// Mode replacement, client side, shutdown and inactive rounds cannot mutate inventory.
for(int fault=0;fault<4;fault++){Fixture f;
if(fault==0)Replication::server=false;
if(fault==1)ERR_GameMode::current=nullptr;
if(fault==2)f.mode.active=false;
if(fault==3)f.service.Stop();
f.service.Tick(1000);assert(f.inventory.calls==0&&f.service.recovers==0);}
{Fixture f;f.service.Tick(1000);auto late=f.inventory.last;f.service.Stop();late->OnComplete();f.service.Tick(61000);
assert(f.inventory.calls==1&&f.service.m_Cleanup.empty());}
// Synchronous completion is accepted; the next tick retires the request.
{Fixture f;f.inventory.immediate=1;f.service.Tick(1000);f.service.Tick(2000);assert(f.service.m_Cleanup.empty()&&f.inventory.calls==1);}
// Round-robin tick budget stays bounded and does not starve later requests.
{Fixture f;Entity folders[9];for(auto& item:folders){item.holder=&f.player;f.inventory.items.Insert(&item);f.service.QueueCleanup(&item,1000);}
f.inventory.accept=false;f.service.Tick(1000);assert(f.inventory.calls==8);f.service.Tick(1001);assert(f.inventory.calls==10);}
}
''')
source = p.read_text()
assert 'Evidence().Retire(' not in source and 'DeleteEntityAndChildren' not in source
assert 'TryDeleteItem(request.Folder, request.Operation)' in source
print('PASS: native-callback cleanup, bounded retries and budget, pending quarantine, objective/ownership/lifecycle protection, no evidence retirement or forced entity deletion')