Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
291 lines
21 KiB
Plaintext
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;}
|