Preserve stolen intel evidence and safely clean up spent folders
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-10-20 16:13:00 +02:00
parent ae2aedd131
commit 87d85ff02e
7 changed files with 221 additions and 23 deletions
+6
View File
@@ -2,6 +2,12 @@
Latest published Workshop version: **0.0.115**
## 2026-10-20 — Safe intel cleanup
- Removing used intel folders preserves enemies' ability to discover stolen documents.
- Used ordinary folders are cleaned up even when no new locations remain to reveal.
- Failed inventory cleanup retries safely without granting extra intel or deleting objective documents.
## 2026-10-20 — Hunter recovery and intel cleanup
- Hunters with a blocked driver seat or failed reboarding can abandon the vehicle and continue on foot.
@@ -1,9 +1,34 @@
// Base mission discovery: recovering physical documents works without companions.
// The Intel addon optionally assigns extra documents to enemy inventories.
class ERR_IntelDeleteCallback : ScriptedInventoryOperationCallback
{
bool Finished;
bool Succeeded;
protected void Finish(bool success) { if (!Finished) { Finished = true; Succeeded = success; } }
protected override void OnComplete() { Finish(true); }
protected override void OnFailed() { Finish(false); }
protected override void OnAllComplete() { Finish(true); }
protected override void OnAllFailed() { Finish(false); }
}
class ERR_SpentIntelCleanup
{
IEntity Folder;
ref ERR_IntelDeleteCallback Operation;
int Attempts;
float NextAttempt;
float Deadline;
bool TimeoutReported;
}
class ERR_DiscoveryService
{
protected ERR_GameMode m_Mode;
protected ref set<IEntity> m_Spent = new set<IEntity>();
protected ref array<ref ERR_SpentIntelCleanup> m_Cleanup = {};
protected int m_CleanupCursor;
protected float m_CleanupNow;
protected bool m_Stopped;
void ERR_DiscoveryService(ERR_GameMode mode) { m_Mode = mode; }
// Shared ledger for recovered and administratively retired documents.
bool IsSpent(IEntity folder) { return folder && m_Spent.Contains(folder); }
@@ -37,14 +62,74 @@ class ERR_DiscoveryService
void Tick(float now)
{
if (!Replication.IsServer()) return;
if (!Replication.IsServer() || m_Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
array<IEntity> players = {};
ERR_Actors.Players(players);
m_CleanupNow = now;
foreach (IEntity player : players) Recover(player);
// Pending callbacks are observed only; even a missing callback never
// permits a second operation against the same physical item.
int budget = Math.Min(8, m_Cleanup.Count());
while (budget-- > 0)
{
if (m_CleanupCursor >= m_Cleanup.Count()) m_CleanupCursor = 0;
if (ProcessCleanup(m_Cleanup[m_CleanupCursor], players, now)) m_Cleanup.Remove(m_CleanupCursor);
else m_CleanupCursor++;
}
}
protected void QueueCleanup(IEntity folder, float now)
{
if (!folder || folder.FindComponent(ERR_DocumentComponent)) return;
// One request per newly spent folder; admission shares the spent-ledger
// bound. Administrative retirement does not enter this queue.
ERR_SpentIntelCleanup request = new ERR_SpentIntelCleanup();
request.Folder = folder;
request.Deadline = now + 60000;
m_Cleanup.Insert(request);
}
protected bool ProcessCleanup(ERR_SpentIntelCleanup request, array<IEntity> players, float now)
{
if (!request.Folder) return true;
if (request.Operation)
{
if (!request.Operation.Finished)
{
if (now >= request.Deadline && !request.TimeoutReported)
{
request.TimeoutReported = true;
Print("Reintroduced [intel]: spent-folder deletion still pending; no duplicate request", LogLevel.WARNING);
}
return false;
}
if (request.Operation.Succeeded) return true;
request.Operation = null;
request.NextAttempt = now + 5000;
}
if (now >= request.Deadline || request.Attempts >= 3)
{
Print("Reintroduced [intel]: spent-folder cleanup expired; item retained without granting more intel", LogLevel.WARNING);
return true;
}
if (request.Folder.FindComponent(ERR_DocumentComponent)) return true;
if (now < request.NextAttempt) return false;
foreach (IEntity player : players)
{
if (!m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) continue;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) continue;
if (!ERR_InfantryAmmoTransfer.StoredUnder(request.Folder, player)) continue;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory || !inventory.Contains(request.Folder)) continue;
request.Attempts++;
request.NextAttempt = now + 5000;
request.Operation = new ERR_IntelDeleteCallback();
if (!inventory.TryDeleteItem(request.Folder, request.Operation)) request.Operation = null;
return false;
}
return false;
}
protected void Recover(IEntity player)
{
if (!Replication.IsServer() || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
if (!inventory) return;
@@ -72,7 +157,11 @@ class ERR_DiscoveryService
}
if (eligible.IsEmpty())
{
if (!document && !m_Mode.WorldPreparing() && MarkSpent(folder) && !mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
if (!document && !m_Mode.WorldPreparing() && MarkSpent(folder))
{
QueueCleanup(folder, m_CleanupNow);
if (!mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
}
continue;
}
ERR_LocationComponent chosen = eligible.GetRandomElement();
@@ -80,17 +169,11 @@ class ERR_DiscoveryService
m_Spent.Insert(folder);
PrintFormat("Reintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
// Delete intel folder :)
if (!document)
{
m_Mode.Evidence().Retire(folder);
SCR_InventoryStorageManagerComponent scrInventory = SCR_InventoryStorageManagerComponent.Cast(inventory);
bool accepted = scrInventory && scrInventory.TryDeleteItem(folder);
PrintFormat("Reintroduced [intel]: folder delete accepted=%1", accepted);
if (!accepted) SCR_EntityHelper.DeleteEntityAndChildren(folder);
}
// Preserve issued evidence: a later corpse inspection must still
// report this site's missing document after the physical item is gone.
if (!document) QueueCleanup(folder, m_CleanupNow);
}
}
void Stop() { m_Spent.Clear(); }
void Stop() { m_Stopped = true; m_Cleanup.Clear(); m_Spent.Clear(); }
}
+4
View File
@@ -63,6 +63,10 @@ int main(){
ERR_IssuedIntel a{&present,"radio-1"},b{&missing,"mortar-3"},c{nullptr,"depot-1"};ERR_BodyEvidence record{&body,{&a,&b,&c},false};Evidence service;service.record=&record;Array<string> result;
observer.position={3,0,0};assert(service.Inspect(&body,&observer,result));assert(result.size()==2&&result.Contains("mortar-3")&&result.Contains("depot-1")&&!result.Contains("radio-1"));
assert(!service.Inspect(&body,&observer,result));
// Engine deletion clears the weak entity reference, not the issued site.
// A consumed folder must remain reportable as missing after physical cleanup.
record.Searched=false;b.Folder=nullptr;
assert(service.Inspect(&body,&observer,result)&&result.Contains("mortar-3"));
Entity wreck,seat,unrelated;body.parent=&seat;seat.parent=&wreck;service.m_Roster={&record};
assert(service.RetainedWithin(&body)&&service.RetainedWithin(&seat)&&service.RetainedWithin(&wreck));
assert(!service.RetainedWithin(&unrelated)&&!service.RetainedWithin(nullptr));
+106
View File
@@ -0,0 +1,106 @@
"""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')
@@ -45,7 +45,7 @@ struct Mode{bool worldPreparing=false;bool WorldPreparing(){return worldPreparin
Roster*Players(){return &roster;}Faction*PlayerFaction(){return faction;}struct Evidence*Evidence(){return &evidence;}struct Settings*Settings(){return &settings;}struct MineIntel*MineIntel(){return haveMine?&mine:nullptr;}void Locations(Array<ERR_LocationComponent*>&out){out=locations;}void ERR_IntelExhausted(bool){exhausted++;}};
struct PlayerManager{int GetPlayerIdFromControlledEntity(IEntity){return 1;}};struct Game{PlayerManager manager;PlayerManager*GetPlayerManager(){return &manager;}}game;Game*GetGame(){return &game;}
template<class...T>void PrintFormat(T...){}
struct Discovery{Mode*m_Mode=nullptr;Array<IEntity>m_Spent;void Folders(InventoryStorageManagerComponent*i,Array<IEntity>&out){out=i->folders;}bool MarkSpent(IEntity e){if(!m_Spent.Contains(e))m_Spent.Insert(e);return true;}
struct Discovery{bool m_Stopped=false;float m_CleanupNow=0;Mode*m_Mode=nullptr;Array<IEntity>m_Spent,cleanup;void QueueCleanup(IEntity item,float){cleanup.Insert(item);}void Folders(InventoryStorageManagerComponent*i,Array<IEntity>&out){out=i->folders;}bool MarkSpent(IEntity e){if(!m_Spent.Contains(e))m_Spent.Insert(e);return true;}
'''+s+r'''
};
struct Fixture{Faction friendly,foreign;Mode mode;Discovery discovery;Entity player,second,folder;SCR_InventoryStorageManagerComponent firstInv,secondInv;ERR_DocumentComponent objective;ERR_LocationComponent radio,other;
@@ -56,19 +56,15 @@ int main(){
// advances ordinary discovery once across transfer between two valid players.
Fixture f;f.objective.target="radio";f.folder.document=&f.objective;
f.discovery.Recover(&f.player);assert(f.radio.knowledge==1&&f.other.knowledge==0&&f.discovery.m_Spent.Contains(&f.folder));
assert(f.firstInv.deleteCalls==0&&!f.folder.deleted&&!f.mode.evidence.retired); // objective stays physical
assert(f.discovery.cleanup.empty()&&!f.mode.evidence.retired); // objective stays physical
assert(f.mode.mine.calls==1&&f.mode.mine.lastPlayer==&f.player&&f.mode.mine.recovered[0]==&f.folder);
f.firstInv.folders.clear();f.secondInv.folders.Insert(&f.folder);f.discovery.Recover(&f.second);assert(f.radio.calls==1&&f.other.calls==0&&f.mode.mine.calls==1);
// Existing sealed generic-folder target survives the supplemental hook.
Fixture sealed;sealed.mode.evidence.sealed=&sealed.folder;sealed.mode.evidence.target="radio";sealed.discovery.Recover(&sealed.player);assert(sealed.radio.knowledge==1&&sealed.other.knowledge==0);
assert(sealed.firstInv.deleted==&sealed.folder&&sealed.mode.evidence.retired==&sealed.folder&&!sealed.folder.deleted);
sealed.discovery.Recover(&sealed.player);assert(sealed.firstInv.deleteCalls==1); // never consume twice
Fixture refusal;refusal.firstInv.acceptDelete=false;refusal.discovery.Recover(&refusal.player);
assert(refusal.firstInv.deleteCalls==1&&refusal.folder.deleted&&refusal.mode.evidence.retired==&refusal.folder);
Fixture generic;InventoryStorageManagerComponent bare;bare.folders.Insert(&generic.folder);generic.player.inventory=&bare;generic.discovery.Recover(&generic.player);
assert(generic.folder.deleted&&generic.mode.evidence.retired==&generic.folder); // missing scripted manager fallback
assert(sealed.discovery.cleanup.size()==1&&sealed.discovery.cleanup[0]==&sealed.folder&&!sealed.mode.evidence.retired);
sealed.discovery.Recover(&sealed.player);assert(sealed.discovery.cleanup.size()==1); // never consume/queue twice
Fixture failed;failed.other.allow=false;failed.discovery.Recover(&failed.player);
assert(failed.firstInv.deleteCalls==0&&!failed.folder.deleted&&!failed.mode.evidence.retired); // no clue, no deletion
assert(failed.discovery.cleanup.empty()&&!failed.mode.evidence.retired); // no clue, no deletion
// Unknown authored target stays unspent; a supplement cannot consume it first.
Fixture missing;missing.objective.target="absent";missing.folder.document=&missing.objective;missing.discovery.Recover(&missing.player);assert(missing.discovery.m_Spent.IsEmpty()&&missing.radio.calls==0&&missing.other.calls==0);
// Failed site discovery also retains the original retry semantics.
@@ -80,10 +76,12 @@ int main(){
assert(pending.radio.knowledge==1&&pending.discovery.m_Spent.Contains(&pending.folder));
// Generic exhausted clue retains the existing spent/exhaustion behavior.
Fixture exhausted;exhausted.radio.knowledge=2;exhausted.other.knowledge=2;exhausted.discovery.Recover(&exhausted.player);assert(exhausted.discovery.m_Spent.Contains(&exhausted.folder)&&exhausted.mode.exhausted==1);
assert(exhausted.discovery.cleanup.size()==1&&!exhausted.mode.evidence.retired);
// A newly published mine warning is useful even after site clues are exhausted.
// Spend the original generic folder once without the contradictory exhaustion notice.
Fixture mineOnly;mineOnly.mode.mine.newlyRecovered=true;mineOnly.radio.knowledge=2;mineOnly.other.knowledge=2;
mineOnly.discovery.Recover(&mineOnly.player);assert(mineOnly.mode.mine.calls==1&&mineOnly.discovery.m_Spent.Contains(&mineOnly.folder)&&mineOnly.mode.exhausted==0);
assert(mineOnly.discovery.cleanup.size()==1&&!mineOnly.mode.evidence.retired);
mineOnly.discovery.Recover(&mineOnly.player);assert(mineOnly.mode.mine.calls==1&&mineOnly.mode.exhausted==0);
// New warning supplements, never replaces, an available site clue.
Fixture both;both.mode.mine.newlyRecovered=true;both.discovery.Recover(&both.player);assert(both.other.knowledge==1&&both.discovery.m_Spent.Contains(&both.folder)&&both.mode.exhausted==0);
+1 -1
View File
@@ -30,7 +30,7 @@ struct Mode {
Point PrisonPosition(){return {0};}bool Transition(int phase){m_Phase=phase;transitions++;return true;}
'''+cpp(unlock)+'''
};
struct Discovery {Mode* m_Mode=nullptr;int inventoryReads=0;
struct Discovery {Mode* m_Mode=nullptr;int inventoryReads=0;bool m_Stopped=false;
'''+cpp(prefix)+'''
};
int main(){
+1
View File
@@ -25,6 +25,7 @@ def check():
subprocess.run([sys.executable,str(ROOT/'tests/test_hunter_fallback.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_hunter_tactics.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_hunter_recovery_wait.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_intel_cleanup.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_hunter_gun_team.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_hunter_coordination.py')],check=True)
subprocess.run([sys.executable,str(ROOT/'tests/test_attack_coordination.py')],check=True)