Files
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

291 lines
21 KiB
Plaintext

--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -779,6 +779,7 @@
}
protected void ReleaseUnit(ERR_HQUnit unit, float now)
{
+ if (unit.Assignment) RememberCompletedHunt(unit.Task, now);
FinishWatch(unit.Task, now); FinishWatch(unit.PendingTask, now);
unit.Release();
}
@@ -937,22 +938,50 @@
if (task.BeliefId != beliefId || !(task.ExpiresAt > now)) return now + 15000;
return task.ExpiresAt + 180000;
}
+ // Completion is historical: later loss of another squad must not erase this
+ // belief's completed-search quiet interval after the original task is cleared.
+ protected void RememberCompletedHunt(ERR_HQTask task, float now)
+ {
+ if (!task || task.ReleaseOnly || !task.SecuritySite.IsEmpty() || task.Investigation()) return;
+ if (!(task.ExpiresAt <= now)) return;
+ ERR_HQHuntWindow hunt = Hunt(task.BeliefId);
+ if (hunt) hunt.CompletedUntil = Math.Max(hunt.CompletedUntil, task.ExpiresAt + 180000);
+ }
+ // Releasing lost physical work and losing a radio order use the same finite
+ // surviving-commitment horizon. This never extends an already shorter hold.
+ protected void ReconsiderHunt(ERR_HQTask task, float now)
+ {
+ if (!task || task.ReleaseOnly || !task.SecuritySite.IsEmpty() || task.Investigation()) return;
+ float nextHunt = now + 15000;
+ foreach (ERR_HQUnit other : m_Units)
+ {
+ if (other.Assignment)
+ {
+ if (other.Task && other.Task.BeliefId == task.BeliefId) RememberCompletedHunt(other.Task, now);
+ nextHunt = Math.Max(nextHunt, HuntCommitment(other.Task, task.BeliefId, now));
+ }
+ if (other.PendingOrder) nextHunt = Math.Max(nextHunt, HuntCommitment(other.PendingTask, task.BeliefId, now));
+ }
+ // Keep both active and pending survivors' deadlines, or the short retry delay.
+ ERR_HQHuntWindow hunt = Hunt(task.BeliefId);
+ if (hunt) hunt.NextHunt = Math.Min(hunt.NextHunt, Math.Max(nextHunt, hunt.CompletedUntil));
+ }
+ // The original physical group is no longer available to fulfil its order.
+ protected void ReleaseLostForce(ERR_HQUnit unit, float now)
+ {
+ ref ERR_HQTask lostActive = unit.Task;
+ ref ERR_HQTask lostPending = unit.PendingTask;
+ ReleaseUnit(unit, now);
+ if (lostActive && lostActive.ExpiresAt > now) ReconsiderHunt(lostActive, now);
+ ReconsiderHunt(lostPending, now);
+ }
protected void LostOrder(ERR_HQUnit unit, float now)
{
ref ERR_HQTask task = unit.PendingTask;
unit.CancelPending();
if (!task || task.ReleaseOnly || !task.SecuritySite.IsEmpty()) return;
if (task.Investigation()) { FinishWatch(task, now); return; }
- float nextHunt = now + 15000;
- foreach (ERR_HQUnit other : m_Units)
- {
- if (other.Assignment) nextHunt = Math.Max(nextHunt, HuntCommitment(other.Task, task.BeliefId, now));
- if (other.PendingOrder) nextHunt = Math.Max(nextHunt, HuntCommitment(other.PendingTask, task.BeliefId, now));
- }
- // A lost long-distance retask cannot prolong an older delivered hunt. Keep
- // both active and pending survivors' own deadlines, or the short retry delay.
- ERR_HQHuntWindow hunt = Hunt(task.BeliefId);
- if (hunt) hunt.NextHunt = Math.Min(hunt.NextHunt, nextHunt);
+ ReconsiderHunt(task, now);
}
// Several squads may hear the same alarm. Reserve only one mobile search
// across all alarm beliefs, including orders still travelling over radio.
@@ -1006,6 +1035,7 @@
if (task.RoadIntercept || !task.SecuritySite.IsEmpty()) waypointResource = ERR_Resources.DefendWaypoint();
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(waypointResource, ERR_Actors.Ground(task.Destination)));
if (!waypoint) { LostOrder(unit, now); m_Journal.Write("ORDER FAILED", "waypoint spawn failed", order.MessageId); return; }
+ if (unit.Assignment) RememberCompletedHunt(unit.Task, now);
unit.Release();
unit.Task = task;
unit.Waypoint = waypoint;
@@ -1345,8 +1375,13 @@
foreach (ERR_HQUnit unit : m_Units)
{
if (!unit.Assignment && !unit.PendingOrder) continue;
- if (!unit.PlanEligible || assigned > globalLimit) { FinishWatch(unit.Task, now); FinishWatch(unit.PendingTask, now); unit.Release(); assigned--; continue; }
- if (unit.Task && now >= unit.Task.ExpiresAt) { FinishWatch(unit.Task, now); unit.ReleaseAssignment(); }
+ if (!unit.PlanEligible || assigned > globalLimit)
+ {
+ if (!unit.PlanEligible) ReleaseLostForce(unit, now);
+ else ReleaseUnit(unit, now); // Capacity trimming preserves the ordinary hold.
+ assigned--; continue;
+ }
+ if (unit.Task && now >= unit.Task.ExpiresAt) { if (unit.Assignment) RememberCompletedHunt(unit.Task, now); FinishWatch(unit.Task, now); unit.ReleaseAssignment(); }
if (unit.PendingTask && now >= unit.PendingTask.ExpiresAt) LostOrder(unit, now);
if (unit.Task && unit.Task.SecuritySite.IsEmpty() && !unit.Task.Investigation() && !m_Memory.GetById(unit.Task.BeliefId)) unit.ReleaseAssignment();
if (unit.PendingTask && !unit.PendingTask.ReleaseOnly && unit.PendingTask.SecuritySite.IsEmpty() && !unit.PendingTask.Investigation() && !m_Memory.GetById(unit.PendingTask.BeliefId)) unit.CancelPending();
@@ -1508,7 +1543,7 @@
}
else if (delivered < 0) LostOrder(unit, now);
}
- if (!unit.Group && !unit.Uplink) { ReleaseUnit(unit, now); m_Units.Remove(i); continue; }
+ if (!unit.Group && !unit.Uplink) { ReleaseLostForce(unit, now); m_Units.Remove(i); continue; }
QueueReports(unit, now);
}
if (now >= m_NextAssignment) { PlanConvoy(now); PlanResupply(now); Assign(now); m_NextAssignment = now + 5000; }
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQTask.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQTask.c
@@ -38,4 +38,5 @@
bool Investigation() { return !WatchSourceId.IsEmpty(); }
int BeliefId;
float NextHunt;
+ float CompletedUntil;
}
--- a/tests/test_hq_delivered_task.py
+++ b/tests/test_hq_delivered_task.py
@@ -13,7 +13,7 @@
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_GameMode','ERR_HQReport','ERR_HQBelief','ERR_ForceRecord','ERR_HQHuntWindow','AIWaypoint'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
- for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','m_Mode'):
+ for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','m_Mode','lostActive','lostPending'):
s=s.replace(name+'.',name+'->')
s=s.replace('ResourceName ', 'int ').replace('.IsEmpty()', '.empty()')
s=s.replace('PendingOrder.', 'PendingOrder->').replace('Assignment.', 'Assignment->')
@@ -23,6 +23,10 @@
units='\n'.join(cpp(body(P,x)) for x in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool AlarmCommitted(', 'protected void FinishWatch(', 'protected void ReleaseUnit(', 'protected void LostOrder(', 'protected void DeliverOrder('))
if 'protected float HuntCommitment(' in P.read_text(): methods=cpp(body(P,'protected float HuntCommitment('))+'\n'+methods
+if 'protected void ReconsiderHunt(' in P.read_text(): methods=cpp(body(P,'protected void ReconsiderHunt('))+'\n'+methods
+if 'protected void RememberCompletedHunt(' in P.read_text(): methods=cpp(body(P,'protected void RememberCompletedHunt('))+'\n'+methods
+if 'protected void ReleaseLostForce(' in P.read_text(): methods=cpp(body(P,'protected void ReleaseLostForce('))+'\n'+methods
+missing=cpp(body(P,'if (!unit.Group && !unit.Uplink)')).replace('m_Units.Remove(i)', 'm_Units.erase(m_Units.begin()+i)')
cleanup=cpp(body(P,' foreach (ERR_HQUnit unit : m_Units)\n {\n if (!unit.Assignment && !unit.PendingOrder) continue;\n if (!unit.PlanEligible || assigned > globalLimit)'))
R=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
rules='\n'.join(cpp(body(R,x)) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role('))
@@ -59,7 +63,7 @@
struct MineJob {int stops=0;void Stop(){stops++;}};
struct ERR_HQUnit{MineJob* EngineerMine=nullptr;int Id=1;string ForceSourceId="original";SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;
- ERR_HQRoute* Downlink=nullptr;void* PendingConvoyOrder=nullptr;bool HomeGuard=false,PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
+ ERR_HQRoute* Downlink=nullptr;ERR_HQRoute* Uplink=nullptr;void* PendingConvoyOrder=nullptr;bool HomeGuard=false,PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
bool Eligible(ERR_GameMode*){return eligible&&Group;}
'''+units+r'''
};
@@ -70,10 +74,10 @@
struct Memory{ERR_HQBelief* belief=nullptr;ERR_HQBelief* GetById(int id){return belief&&belief->BeliefId==id?belief:nullptr;}};
struct Watch{bool empty=false;int finished=0;bool ArmedSearchBlocked(string,float){return empty;}bool FinishInvestigation(string,int,int,float){finished++;return true;}};
struct Journal{int writes=0;void Write(string,string,int=0){writes++;}};
-struct ERR_HQHuntWindow{float NextHunt=200000;};
-struct HQ{ERR_GameMode* m_Mode;Memory m_Memory;Watch m_Watch;Journal m_Journal;std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow hunt;bool watchValid=true;
- bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int){return &hunt;}
-'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}'+r'''
+struct ERR_HQHuntWindow{float CompletedUntil=0;float NextHunt=200000;};
+struct HQ{ERR_GameMode* m_Mode;Memory m_Memory;Watch m_Watch;Journal m_Journal;std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow hunt,otherHunt;bool watchValid=true;
+ bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int id){return id==2?&otherHunt:&hunt;}
+'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}\nvoid RemoveMissing(float now){for(int i=int(m_Units.size())-1;i>=0;i--){ERR_HQUnit* unit=m_Units[i];'+missing+'}}'+r'''
};
struct Fixture{ERR_GameMode mode;HQ hq;ERR_HQUnit unit;SCR_AIGroup group;AIWaypoint previous;ERR_HQTask active,next;ERR_HQReport oldReport,newReport;ERR_HQRoute route;ERR_HQBelief belief;
Fixture(){ERR_GameMode::current=&mode;ERR_Actors::refuse=false;ERR_Actors::spawns=0;hq.m_Mode=&mode;hq.m_Units={&unit};hq.m_Memory.belief=&belief;
@@ -117,6 +121,54 @@
assert(f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);f.hq.Cleanup(1001,1,4);assert(f.previous.deletes==1&&f.unit.PendingTask==&f.next);}
{Fixture f;f.active.BeliefId=1;f.next.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);f.Retained();}
{Fixture f;f.unit.PlanEligible=false;f.hq.Cleanup(1000,1,4);assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
+ // Loss of the only physical pursuit squad releases its unfulfilled long hold.
+ {Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
+ f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==25000);assert(!f.unit.Task&&!f.unit.Assignment&&f.previous.deletes==1);}
+ // Native group deletion takes the earlier Tick cleanup path, before Assign.
+ {Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.Group=nullptr;
+ f.hq.RemoveMissing(10000);assert(f.hq.m_Units.empty()&&f.hq.hunt.NextHunt==25000&&!f.unit.Task&&f.previous.deletes==1);}
+ // Finishing an ordinary delivered search retains its completed-hunt cooldown.
+ {Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=190000;
+ f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Assignment);}
+ // Simultaneous expiry and unavailable-force detection cannot erase a completed
+ // hunt's quiet interval, including after loss of a pending same-belief retask.
+ for(int removed=0;removed<2;removed++)for(int pending=0;pending<3;pending++)for(int shorter=0;shorter<2;shorter++){
+ Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=shorter?12000:190000;f.hq.otherHunt.NextHunt=780000;
+ if(pending==0)f.unit.CancelPending();else {f.next.BeliefId=pending==1?1:2;f.next.ExpiresAt=600000;}
+ f.unit.PlanEligible=false;if(removed){f.unit.Group=nullptr;f.hq.RemoveMissing(10000);}else f.hq.Cleanup(10000,1,4);
+ assert(f.hq.hunt.NextHunt==(shorter?12000:190000));assert(f.hq.otherHunt.NextHunt==(pending==2?25000:780000));
+ assert(!f.unit.Task&&!f.unit.PendingTask&&f.previous.deletes==1);
+ }
+ // A completed squad's floor survives a different lost squad in either cleanup
+ // order, whether the completed squad expires normally or is also unavailable.
+ for(int reverse=0;reverse<2;reverse++)for(int completedLost=0;completedLost<2;completedLost++){
+ Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.unit.PlanEligible=!completedLost;f.hq.hunt.NextHunt=780000;
+ ERR_HQUnit lost;SCR_AIGroup lostGroup;ERR_HQTask lostTask;ERR_HQReport lostReport;lost.Group=&lostGroup;lost.Task=&lostTask;lost.Assignment=&lostReport;lost.PlanEligible=false;lostTask.BeliefId=1;lostTask.ExpiresAt=600000;
+ f.hq.m_Units.push_back(&lost);if(reverse)std::reverse(f.hq.m_Units.begin(),f.hq.m_Units.end());f.hq.Cleanup(10000,2,4);assert(f.hq.hunt.NextHunt==190000);
+ }
+ // A replacement can arrive before Assign processes the original expiry.
+ // Native commit must preserve completion history before clearing old ownership.
+ {Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
+ f.hq.DeliverOrder(&f.unit,10000);assert(f.unit.Task==&f.next&&f.hq.hunt.CompletedUntil==190000);
+ f.unit.PlanEligible=false;f.hq.Cleanup(11000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Task);}
+ // Only expired delivered pursuit work creates completion history.
+ for(int kind=0;kind<5;kind++){Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;
+ if(kind==0){f.active.ExpiresAt=10001;}if(kind==1){f.active.ReleaseOnly=true;}if(kind==2){f.active.SecuritySite="security";}if(kind==3){f.active.WatchSourceId="watch";}if(kind==4){f.unit.Assignment=nullptr;}
+ f.hq.ReleaseUnit(&f.unit,10000);assert(f.hq.hunt.CompletedUntil==0&&f.hq.otherHunt.CompletedUntil==0);}
+ // Capacity trimming is deliberately unchanged: it is not evidence of force loss.
+ {Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
+ f.hq.Cleanup(10000,2,1);assert(f.hq.hunt.NextHunt==780000&&!f.unit.Assignment);}
+ // Surviving active and pending same-belief commitments both retain their own
+ // horizon; foreign replacements cannot hide active work or prolong this hunt.
+ for(int scenario=0;scenario<5;scenario++){Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
+ ERR_HQUnit peer;SCR_AIGroup peerGroup;ERR_HQTask surviving,replacement;ERR_HQReport activeReport,pendingReport;peer.Group=&peerGroup;peer.Task=&surviving;peer.Assignment=&activeReport;peer.PendingTask=&replacement;peer.PendingOrder=&pendingReport;
+ surviving.BeliefId=1;surviving.ExpiresAt=120000;replacement.BeliefId=1;replacement.ExpiresAt=180000;
+ if(scenario==1)replacement.BeliefId=2;
+ if(scenario==2){surviving.SecuritySite="outpost";replacement.ReleaseOnly=true;}
+ if(scenario==3){peer.Assignment=nullptr;peer.PendingOrder=nullptr;}
+ if(scenario==4)f.hq.hunt.NextHunt=12000;
+ f.hq.m_Units.push_back(&peer);f.hq.Cleanup(10000,2,4);float expected=scenario==0?360000:scenario==1?300000:scenario==4?12000:25000;
+ assert(f.hq.hunt.NextHunt==expected);assert(peer.Task==&surviving);}
// Expired undelivered retasks use the same bounded rollback as lost radio.
// Both original and replacement expire here; no unfulfilled hunt owns 780s.
{Fixture f;f.active.ExpiresAt=120000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
--- a/tests/test_hq_assignment_lifecycle.py
+++ b/tests/test_hq_assignment_lifecycle.py
@@ -6,6 +6,8 @@
unit='\n'.join(body(p,s) for s in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
lost='\n'.join(body(p,s) for s in ('protected void FinishWatch(', 'protected void LostOrder(')).replace('protected ', '')
if 'protected float HuntCommitment(' in p.read_text(): lost=body(p,'protected float HuntCommitment(').replace('protected ', '')+'\n'+lost
+if 'protected void ReconsiderHunt(' in p.read_text(): lost=body(p,'protected void ReconsiderHunt(').replace('protected ', '')+'\n'+lost
+if 'protected void RememberCompletedHunt(' in p.read_text(): lost=body(p,'protected void RememberCompletedHunt(').replace('protected ', '')+'\n'+lost
retask=body(p,'protected void Retask(')
queue=retask[retask.index(' ERR_HQHuntWindow hunt = Hunt(belief.BeliefId);'):]
queue=queue[:queue.index(';',queue.index('if (hunt)'))+1]
@@ -22,7 +24,7 @@
code=code.replace('foreach (ERR_HQUnit other : m_Units)', 'for (ERR_HQUnit* other : m_Units)')
code=re.sub(r'\b(mode|record|Group|unit|other|task|planned|hunt)\.', r'\1->', code)
code=code.replace('mode->Forces().FindGroup', 'mode->Forces()->FindGroup')
- return code
+ return code.replace('other->Task.', 'other->Task->')
run(r'''
#include <cassert>
#include <vector>
@@ -41,7 +43,7 @@
static ERR_GameMode* Get(){return current;} Ledger* Forces(){return &ledger;}};
ERR_GameMode* ERR_GameMode::current=nullptr;
struct SCR_EntityHelper {static void DeleteEntityAndChildren(AIWaypoint* p){p->deleted++;}};
-struct ERR_HQHuntWindow {float NextHunt=0;};
+struct ERR_HQHuntWindow {float CompletedUntil=0;float NextHunt=0;};
struct MineJob {int stops=0;void Stop(){stops++;}};
struct ERR_HQUnit {MineJob* EngineerMine=nullptr;
SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
@@ -112,7 +114,8 @@
// An unrelated pending replacement cannot conceal its original same-track task.
replacement.BeliefId=8;owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
h.LostOrder(&owner,30000);assert(h.window.NextHunt==580000);
- // Orphan task pointers, expired tasks and non-pursuit orders cannot hold a hunt.
+ // Orphan pointers and non-pursuit orders cannot hold a hunt. A still-owned
+ // expired active search preserves its completed interval.
owner.Task=nullptr;owner.Assignment=nullptr;peer.Assignment=nullptr;peer.PendingOrder=nullptr;
owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
h.LostOrder(&owner,30000);assert(h.window.NextHunt==45000);
@@ -120,7 +123,7 @@
peer.Assignment=&r;active.ExpiresAt=400000;active.ReleaseOnly=false;active.SecuritySite="";active.WatchSourceId="";
if(kind==0){active.ReleaseOnly=true;}if(kind==1){active.SecuritySite="site";}if(kind==2){active.WatchSourceId="patrol";}if(kind==3){active.ExpiresAt=30000;}
owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=1000000;
- h.LostOrder(&owner,30000);assert(h.window.NextHunt==45000);}
+ h.LostOrder(&owner,30000);assert(h.window.NextHunt==(kind==3?210000:45000));}
// Clamping only: even a surviving long commitment must not raise earlier pacing.
active.ExpiresAt=400000;owner.PendingTask=&pending;owner.PendingOrder=&r;h.window.NextHunt=40000;
h.LostOrder(&owner,30000);assert(h.window.NextHunt==40000);
--- a/tests/test_hq_task_reservations.py
+++ b/tests/test_hq_task_reservations.py
@@ -28,7 +28,7 @@
s=s.replace(name+'.',name+'->')
s=s.replace('PendingTask.', 'PendingTask->')
s=s.replace('int total;','int total=0;').replace('bool found;','bool found=false;')
- return s
+ return s.replace('other->Task.', 'other->Task->')
assign=body(P,'protected void Assign(')
collect=loop(assign[assign.index(' array<vector> reserved = {};'):], 'foreach (ERR_HQUnit unit : m_Units)')
retask=body(P,'protected void Retask(')
@@ -40,6 +40,8 @@
assert 'ReserveUnit(existing, reserved, first, second)' in body(P,'protected void Investigate(')
methods='\n'.join(body(P,s) for s in ('protected int Assigned(', 'protected void FinishWatch(', 'protected void LostOrder('))
if 'protected float HuntCommitment(' in P.read_text(): methods=body(P,'protected float HuntCommitment(')+'\n'+methods
+if 'protected void ReconsiderHunt(' in P.read_text(): methods=body(P,'protected void ReconsiderHunt(')+'\n'+methods
+if 'protected void RememberCompletedHunt(' in P.read_text(): methods=body(P,'protected void RememberCompletedHunt(')+'\n'+methods
unit_methods='\n'.join(body(P,s) for s in ('void CancelPending(', 'ERR_HQTask PlannedTask('))
rules='\n'.join(body(R,s) for s in ('static bool Finite(', 'static bool PositionValid(', 'static bool BearingAllowed('))
coverage_rules='\n'.join(body(C,s) for s in ('static bool PositionValid(', 'static int Cell(', 'static int Key(', 'static bool Point('))
@@ -70,7 +72,7 @@
ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQRoute* Downlink=nullptr;
void* PendingConvoyOrder=nullptr;bool PendingCheckin=false;void* Waypoint=nullptr;
'''+cpp(unit_methods)+r'''};
-struct ERR_HQHuntWindow {float NextHunt=900000;};
+struct ERR_HQHuntWindow {float CompletedUntil=0;float NextHunt=900000;};
struct Watch {void FinishInvestigation(std::string,int,int,float){}};
struct HQ {std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow window;Watch m_Watch;
ERR_HQHuntWindow* Hunt(int){return &window;}