--- 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::vectorm_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::vectorm_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 #include @@ -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 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::vectorm_Units;ERR_HQHuntWindow window;Watch m_Watch; ERR_HQHuntWindow* Hunt(int){return &window;}