107 lines
8.7 KiB
Python
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')
|