Fix stalled hunter recovery and document intel cleanup
This commit is contained in:
+7
-1
@@ -1,6 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
Latest published Workshop version: **0.0.112**
|
||||
Latest published Workshop version: **0.0.115**
|
||||
|
||||
## 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.
|
||||
- Hunters remember failed approach positions and pause before attempting another coordinated manoeuvre.
|
||||
- Ordinary intel folders are removed after revealing a location; objective documents are retained.
|
||||
|
||||
## 2026-10-17 — Fewer Šárka hunters
|
||||
|
||||
|
||||
@@ -14,6 +14,20 @@ class ERR_VehicleBehaviorCleanup
|
||||
|
||||
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
||||
// Readiness failures share one deadline even if the reason changes or combat
|
||||
// briefly interrupts reboarding. Native combat and moving vehicles defer exit.
|
||||
class ERR_HunterRecoveryWait
|
||||
{
|
||||
float Since = -1;
|
||||
void Reset() { Since = -1; }
|
||||
bool Expired(bool registered, bool ready, bool combat, float speed, float now)
|
||||
{
|
||||
if (!registered || ready) { Reset(); return false; }
|
||||
if (Since < 0 || now < Since) Since = now;
|
||||
return now - Since >= 120000 && !combat && speed >= 0 && speed <= 3;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HunterCrewRecovery
|
||||
{
|
||||
protected ERR_HunterVehicle m_Unit;
|
||||
@@ -22,6 +36,7 @@ class ERR_HunterCrewRecovery
|
||||
protected float m_NextAttempt;
|
||||
protected string m_LastReason;
|
||||
protected bool m_Stopped;
|
||||
ref ERR_HunterRecoveryWait Wait = new ERR_HunterRecoveryWait();
|
||||
string Reason;
|
||||
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
|
||||
void SuspendBoarding()
|
||||
|
||||
@@ -81,6 +81,7 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||
bool DeliveredContact;
|
||||
ref ERR_HunterCrewRecovery Recovery;
|
||||
ref ERR_HunterTactics Tactics;
|
||||
ref ERR_HunterApproachMemory FailedApproaches = new ERR_HunterApproachMemory();
|
||||
ref ERR_HunterCloseContact CloseContact;
|
||||
bool CloseContactHeld;
|
||||
bool CombatHeld;
|
||||
@@ -770,10 +771,22 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
// A delivered manoeuvre owns dismount/reboarding. The ordinary recovery
|
||||
// must not re-add its vehicle while the team is moving on foot.
|
||||
if (!unit.CloseContact) unit.CloseContact = new ERR_HunterCloseContact(unit);
|
||||
if (unit.CloseContact.Tick(m_Mode, now)) continue;
|
||||
if (unit.Tactics && unit.Tactics.Tick(hq, now)) continue;
|
||||
if (unit.CloseContact.Tick(m_Mode, now))
|
||||
{ if (unit.Recovery) unit.Recovery.Wait.Reset(); continue; }
|
||||
if (unit.Tactics && unit.Tactics.Tick(hq, now))
|
||||
{ if (unit.Recovery) unit.Recovery.Wait.Reset(); continue; }
|
||||
UpdateCombat(unit, now);
|
||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||
// Crew failures bypass the route-progress observer. Give them their own
|
||||
// bounded fallback, including a casualty occupying the driver's seat.
|
||||
if (unit.Recovery)
|
||||
{
|
||||
float recoverySpeed = -1;
|
||||
Physics recoveryPhysics = unit.Entity.GetPhysics();
|
||||
if (recoveryPhysics) recoverySpeed = recoveryPhysics.GetVelocity().Length();
|
||||
if (unit.Recovery.Wait.Expired(unit.Registered, unit.CrewValid(m_Mode), unit.CombatHeld, recoverySpeed, now))
|
||||
{ AbandonStuckVehicle(unit, "driver recovery timed out after two minutes"); continue; }
|
||||
}
|
||||
if (unit.CombatHeld) continue;
|
||||
if (!unit.Registered)
|
||||
{
|
||||
@@ -860,7 +873,7 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||
}
|
||||
protected void AbandonStuckVehicle(ERR_HunterVehicle unit)
|
||||
protected void AbandonStuckVehicle(ERR_HunterVehicle unit, string reason = "three route retries failed")
|
||||
{
|
||||
if (unit.Released || unit.PlayerCaptureObserved || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) return;
|
||||
// Stop reboarding/tactics before detaching the vehicle. Preserve the original
|
||||
@@ -878,7 +891,7 @@ class ERR_HunterService : ERR_ConvoyService
|
||||
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||
agent.m_UtilityComponent.AddAction(new SCR_AIGetOutVehicle(agent.m_UtilityComponent, null, unit.Entity, priority: 1000));
|
||||
}
|
||||
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": three route retries failed; original crew ordered out for on-foot HQ duties, vehicle retained");
|
||||
ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + reason + "; original crew ordered out for on-foot HQ duties, vehicle retained");
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
// A bounded response by original hunter crews. Enemy positions are immutable
|
||||
// radio-delivered snapshots; native perception still owns aiming and firing.
|
||||
// Failures belong to the physical crew, not a disposable tactical assignment.
|
||||
// Bound both memory and lifetime so changed terrain/contact can be tried later.
|
||||
class ERR_HunterApproachMemory
|
||||
{
|
||||
protected ref array<vector> m_Positions = {};
|
||||
protected ref array<float> m_Expires = {};
|
||||
float RetryAfter;
|
||||
void Prune(float now)
|
||||
{
|
||||
for (int i = m_Expires.Count() - 1; i >= 0; i--)
|
||||
if (now >= m_Expires[i]) { m_Expires.Remove(i); m_Positions.Remove(i); }
|
||||
}
|
||||
void Reject(vector position, float now)
|
||||
{
|
||||
Prune(now); RetryAfter = now + 60000;
|
||||
for (int i = 0; i < m_Positions.Count(); i++)
|
||||
if (vector.DistanceXZ(position, m_Positions[i]) < 85)
|
||||
{ m_Expires[i] = now + 300000; return; }
|
||||
if (m_Positions.Count() >= 8) { m_Positions.Remove(0); m_Expires.Remove(0); }
|
||||
m_Positions.Insert(position); m_Expires.Insert(now + 300000);
|
||||
}
|
||||
void Append(array<vector> excluded, float now)
|
||||
{
|
||||
Prune(now);
|
||||
foreach (vector position : m_Positions) excluded.Insert(position);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_HunterAttackRules
|
||||
{
|
||||
static float ManeuverBudget(float metres) { return Math.Clamp(30 + metres / 2, 90, 240) * 1000; }
|
||||
@@ -328,12 +356,22 @@ class ERR_HunterTactics
|
||||
bool Plan(array<vector> occupied)
|
||||
{
|
||||
if (!Living()) return false;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (!Delivered && now < Unit.FailedApproaches.RetryAfter) return false;
|
||||
array<vector> excluded = {};
|
||||
foreach (vector reservedPosition : occupied) excluded.Insert(reservedPosition);
|
||||
Unit.FailedApproaches.Append(excluded, now);
|
||||
SCR_AIGroupUtilityComponent utility = Unit.Group.GetGroupUtilityComponent();
|
||||
if (!utility) return false;
|
||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||
float currentRange = vector.DistanceXZ(Unit.Entity.GetOrigin(), Attack.ReportPosition);
|
||||
if (Role == 0 && currentRange >= 140 && currentRange <= 500 && VisualCover())
|
||||
{ Stage = Unit.Entity.GetOrigin(); return true; }
|
||||
{
|
||||
bool rejected;
|
||||
foreach (vector failedPosition : excluded)
|
||||
if (vector.DistanceXZ(Unit.Entity.GetOrigin(), failedPosition) < 85) rejected = true;
|
||||
if (!rejected) { Stage = Unit.Entity.GetOrigin(); return true; }
|
||||
}
|
||||
// Approach from this crew's side of the contact. A common cover axis used
|
||||
// to send the opposite-side truck around the entire fight before dismount.
|
||||
vector approachForward = ERR_MountedCover.Approach(Unit.Entity.GetOrigin(), Attack.ReportPosition, Attack.Forward);
|
||||
@@ -348,7 +386,7 @@ class ERR_HunterTactics
|
||||
if (attempt % 2 == 0) lateral = -lateral;
|
||||
vector seed = desired - approachForward * (attempt * 15) + approachLeft * lateral;
|
||||
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, point, angles, 100)) continue;
|
||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
|
||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, excluded) || !utility.IsPositionAllowed(point) || !placement.Clear(point, angles[0], 2.5)) continue;
|
||||
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||
Stage = point; found = true; break;
|
||||
}
|
||||
@@ -359,7 +397,7 @@ class ERR_HunterTactics
|
||||
vector point = desired;
|
||||
if (offroad > 0) point += Vector(Math.RandomFloat(-90, 90), 0, Math.RandomFloat(-90, 90));
|
||||
point = ERR_Actors.Ground(point);
|
||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, occupied) || !utility.IsPositionAllowed(point)) continue;
|
||||
if (!ERR_HunterAttackRules.Stage(point, Attack.ReportPosition, approachForward, excluded) || !utility.IsPositionAllowed(point)) continue;
|
||||
if (!placement.Clear(point, ERR_ForceRules.Heading(point, Attack.ReportPosition, 0), 2.5)) continue;
|
||||
if (Role == 0 && !ERR_MountedCover.ClearLane(Unit.Entity, point, Attack.ReportPosition)) continue;
|
||||
Stage = point; found = true;
|
||||
@@ -371,7 +409,7 @@ class ERR_HunterTactics
|
||||
vector parked = Unit.Entity.GetOrigin();
|
||||
float range = vector.DistanceXZ(parked, Attack.ReportPosition);
|
||||
bool separate = true;
|
||||
foreach (vector reserved : occupied) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
|
||||
foreach (vector reserved : excluded) if (vector.DistanceXZ(parked, reserved) < 85) separate = false;
|
||||
if (range >= 140 && range <= 500 && separate) { Stage = parked; found = true; }
|
||||
}
|
||||
if (!found) { Note("no separated safe approach; keeping current orders"); return false; }
|
||||
@@ -410,6 +448,7 @@ class ERR_HunterTactics
|
||||
}
|
||||
protected bool AlternateStage(float now)
|
||||
{
|
||||
RejectStage(now);
|
||||
// Reserve failed positions as well as the other teams. Do not repeatedly
|
||||
// choose the same screened corner or send two vehicles to one firing point.
|
||||
array<vector> occupied = {};
|
||||
@@ -426,9 +465,13 @@ class ERR_HunterTactics
|
||||
Note("alternate approach assigned=" + Stage.ToString());
|
||||
return true;
|
||||
}
|
||||
protected void RejectStage(float now)
|
||||
{
|
||||
Unit.FailedApproaches.Reject(Stage, now);
|
||||
}
|
||||
protected bool RecoverApproach(float now)
|
||||
{
|
||||
if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); Stop(); return false; }
|
||||
if (m_ApproachRetries >= 2) { Note("approach still blocked after two recoveries; releasing assignment"); RejectStage(now); Stop(); return false; }
|
||||
m_ApproachRetries++;
|
||||
bool assigned;
|
||||
if (m_ApproachRetries == 1) assigned = Move(Stage, 35);
|
||||
@@ -444,7 +487,7 @@ class ERR_HunterTactics
|
||||
if (VisualCover()) { m_BlindSince = -1; return true; }
|
||||
if (m_BlindSince < 0) m_BlindSince = now;
|
||||
if (now - m_BlindSince < 20000) return true;
|
||||
if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); Stop(); return false; }
|
||||
if (m_CoverRetries >= 3) { Note("no visual cover after three alternate positions; releasing assignment"); RejectStage(now); Stop(); return false; }
|
||||
m_CoverRetries++; m_BlindSince = now;
|
||||
Note("cover position has no visual contact; looking for another firing angle");
|
||||
AlternateStage(now);
|
||||
@@ -500,7 +543,7 @@ class ERR_HunterTactics
|
||||
{
|
||||
if (ERR_HunterAttackRules.ExtendApproach(m_ApproachStarted, m_ApproachProgress, now))
|
||||
{ Deadline = Math.Min(now + 30000, m_ApproachStarted + 300000); Note("approach still progressing; allowing another bounded route interval"); }
|
||||
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); Stop(); return false; }
|
||||
else { Note("approach made no recent progress or exhausted five-minute budget; release this assignment"); RejectStage(now); Stop(); return false; }
|
||||
}
|
||||
else if (now - m_ApproachProgress >= 30000 && !RecoverApproach(now)) return false;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
"""Execute recovery deadlines and cross-assignment rejection memory from production."""
|
||||
from policy_harness import ROOT, body, run
|
||||
|
||||
ai = ROOT / 'addons/core/Scripts/Game/ERR/10_AI'
|
||||
wait = body(ai / 'ERR_HunterCrewRecovery.c', 'class ERR_HunterRecoveryWait')
|
||||
run('#include <cassert>\nstruct ' + wait.removeprefix('class ') + r''';
|
||||
int main() {
|
||||
ERR_HunterRecoveryWait wait;
|
||||
assert(!wait.Expired(false,false,false,0,1000)); // admission owns its own deadline
|
||||
assert(!wait.Expired(true,false,false,0,2000));
|
||||
assert(!wait.Expired(true,false,false,0,121999));
|
||||
assert(wait.Expired(true,false,false,0,122000));
|
||||
// Repeated combat/seat-failure transitions cannot restart the deadline.
|
||||
assert(!wait.Expired(true,false,true,0,123000));
|
||||
assert(!wait.Expired(true,false,false,4,124000));
|
||||
assert(!wait.Expired(true,false,false,-1,125000)); // no physics sample
|
||||
assert(wait.Expired(true,false,false,3,126000));
|
||||
assert(!wait.Expired(true,true,false,0,127000)); // recovered or adopted driver
|
||||
assert(!wait.Expired(true,false,false,0,200000));
|
||||
assert(!wait.Expired(true,false,false,0,319999));
|
||||
assert(wait.Expired(true,false,false,0,320000));
|
||||
wait.Reset(); // deliberate tactical dismount owns its separate time budget
|
||||
assert(!wait.Expired(true,false,false,0,500000));
|
||||
assert(!wait.Expired(true,false,false,0,1000)); // world clock reset
|
||||
assert(!wait.Expired(true,false,false,0,120999));
|
||||
assert(wait.Expired(true,false,false,0,121000));
|
||||
}
|
||||
''')
|
||||
|
||||
memory = body(ai / 'ERR_HunterTactics.c', 'class ERR_HunterApproachMemory')
|
||||
memory = memory.replace('protected ', '').replace('ref array<vector>', 'Array<Vec>').replace('ref array<float>', 'Array<float>')
|
||||
memory = memory.replace('array<vector> excluded', 'Array<Vec>& excluded').replace('foreach (vector position :', 'for (Vec position :')
|
||||
memory = memory.replace('vector position', 'Vec position').replace('vector.DistanceXZ', 'Distance').replace('float RetryAfter;', 'float RetryAfter = 0;')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
struct Vec {float x=0;}; float Distance(Vec a,Vec b){return std::abs(a.x-b.x);}
|
||||
template<class T>struct Array:std::vector<T>{
|
||||
int Count(){return this->size();}void Remove(int i){this->erase(this->begin()+i);}void Insert(T value){this->push_back(value);}
|
||||
};
|
||||
struct ''' + memory.removeprefix('class ') + r''';
|
||||
int main(){
|
||||
ERR_HunterApproachMemory crew;crew.Reject({100},1000);
|
||||
assert(crew.RetryAfter==61000);
|
||||
Array<Vec> nextAssignment;crew.Append(nextAssignment,9000);
|
||||
assert(nextAssignment.size()==1&&nextAssignment[0].x==100); // eight seconds after release
|
||||
crew.Reject({101},2000);Array<Vec> refreshed;crew.Append(refreshed,301000);
|
||||
assert(refreshed.size()==1); // nearly identical stage refreshes, does not duplicate
|
||||
Array<Vec> expired;crew.Append(expired,302000);assert(expired.empty());
|
||||
for(int i=0;i<12;i++)crew.Reject({float(i*100)},400000+i);
|
||||
Array<Vec> bounded;crew.Append(bounded,400020);assert(bounded.size()==8&&bounded[0].x==400);
|
||||
ERR_HunterApproachMemory otherCrew;Array<Vec> independent;otherCrew.Append(independent,400020);assert(independent.empty());
|
||||
Array<Vec> cleared;crew.Append(cleared,700011);assert(cleared.empty());
|
||||
}
|
||||
''')
|
||||
|
||||
# Wiring guards complement execution: the blocked-seat branch must reach this
|
||||
# clock before normal travel's invalid-crew early return, and every planner
|
||||
# candidate (including already parked vehicles) must consult shared memory.
|
||||
service = (ai / 'ERR_HunterService.c').read_text()
|
||||
tick = body(ai / 'ERR_HunterService.c', 'override void Tick(float now)')
|
||||
assert tick.index('unit.Recovery.Wait.Expired(') < tick.index('if (!unit.CrewValid(m_Mode)) continue;')
|
||||
assert tick.count('unit.Recovery.Wait.Reset()') == 2
|
||||
assert 'AbandonStuckVehicle(unit, "driver recovery timed out after two minutes")' in tick
|
||||
assert 'ref ERR_HunterApproachMemory FailedApproaches' in service
|
||||
plan = body(ai / 'ERR_HunterTactics.c', 'bool Plan(array<vector> occupied)')
|
||||
assert 'if (!Delivered && now < Unit.FailedApproaches.RetryAfter) return false;' in plan
|
||||
assert 'Unit.FailedApproaches.Append(excluded, now);' in plan
|
||||
assert plan.count('approachForward, excluded)') == 2
|
||||
assert 'foreach (vector failedPosition : excluded)' in plan
|
||||
assert 'foreach (vector reserved : excluded)' in plan
|
||||
assert 'Unit.FailedApproaches.Reject(Stage, now);' in body(ai / 'ERR_HunterTactics.c', 'protected void RejectStage(')
|
||||
print('PASS: blocked crew deadline survives combat interruptions, defers unsafe exits, resets after recovery; failed stages survive assignments with bounded expiry')
|
||||
@@ -40,6 +40,7 @@ int main(){
|
||||
print('PASS: actual movement recovery retries delivered orders three times, respects combat/speed/arrival/progress, then abandons the stalled vehicle')
|
||||
|
||||
s=body(p,'protected void AbandonStuckVehicle(').replace('protected ', '').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.', 'unit->')
|
||||
s=s.replace('string reason', 'std::string reason')
|
||||
s=s.replace('SCR_AIGroupUtilityComponent utility = unit->Group.GetGroupUtilityComponent();','Utility* utility = unit->Group->GetGroupUtilityComponent();')
|
||||
s=s.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit->Entity.FindComponent(SCR_AIVehicleUsageComponent));','void* usage = unit->Entity;')
|
||||
s=s.replace('foreach (IEntity member : unit->Members)','for (Actor* member : unit->Members)')
|
||||
|
||||
@@ -60,6 +60,7 @@ struct UnitType{string Source="hunter";struct GroupType*Group=nullptr;IEntity*En
|
||||
struct AttackType{Vec ReportPosition;float Started=0;bool alive=true,ready=false;bool CoverAlive(){return alive;}bool CoverReady(){return ready;}};
|
||||
struct GunTeam{int ticks=0;void Tick(float){ticks++;}void Pause(){}};
|
||||
struct Tactics{
|
||||
int rejected=0;void RejectStage(float){rejected++;}
|
||||
int m_ApproachRetries=0,m_CoverRetries=0,alternates=0;float m_BlindSince=-1;bool alternateWorks=true;
|
||||
bool AlternateStage(float now){alternates++;if(!alternateWorks)return false;State=1;m_BlindSince=-1;m_ApproachProgress=now;Stage={500,0,-250};return true;}
|
||||
GunTeam*m_GunTeam=nullptr;bool align=false,facing=false;bool TryAlign(){return align;}bool FacingSector(){return facing;}
|
||||
@@ -224,6 +225,7 @@ struct UnitType{struct Entity entity;struct Entity*Entity=&entity;};
|
||||
struct Tactics;struct AttackType{Array<Tactics*>Teams;};
|
||||
float ManeuverBudget(float){return 240000;}
|
||||
struct Tactics{
|
||||
int rejected=0;void RejectStage(float){rejected++;}
|
||||
UnitType unit;UnitType*Unit=&unit;AttackType attack;AttackType*Attack=&attack;
|
||||
Vec Stage{0,0,-210},Approach{155,0,-240},Sector{105,0,35},m_ApproachPosition;
|
||||
Array<Vec>m_RejectedStages,reservations;int State=3;float m_BlindSince=1000,m_ApproachProgress=0,m_ApproachStarted=1000,m_TravelDistance=1000,Deadline=100;
|
||||
@@ -235,7 +237,7 @@ struct Tactics{
|
||||
int main(){
|
||||
for(int failure=0;failure<2;failure++){
|
||||
Tactics t;t.plan=failure!=0;t.move=failure!=1;
|
||||
assert(!t.AlternateStage(21000));assert(t.Stage.x==0&&t.Approach.x==155&&t.State==3&&t.m_RejectedStages.empty());
|
||||
assert(!t.AlternateStage(21000));assert(t.Stage.x==0&&t.Approach.x==155&&t.State==3&&t.m_RejectedStages.empty()&&t.rejected==1);
|
||||
}
|
||||
Tactics t,other,dead;dead.live=false;other.Stage={-200,0,-210};t.attack.Teams={&t,&other,&dead};t.m_RejectedStages.Insert({-100,0,-300});
|
||||
assert(t.AlternateStage(21000));assert(t.reservations.size()==4&&t.reservations.back().x==-200);
|
||||
|
||||
@@ -7,12 +7,12 @@ from policy_harness import ROOT,body,run
|
||||
P=Path(os.environ.get('ERR_DISCOVERY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c'))
|
||||
s=body(P,'protected void Recover(')
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('foreach (','for (').replace('array<','Array<')
|
||||
for typ in ('InventoryStorageManagerComponent','ERR_DocumentComponent','ERR_LocationComponent'):
|
||||
for typ in ('InventoryStorageManagerComponent','SCR_InventoryStorageManagerComponent','ERR_DocumentComponent','ERR_LocationComponent'):
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
s=s.replace('Array<ERR_LocationComponent>','Array<ERR_LocationComponent*>').replace('bool mineRecovered;', 'bool mineRecovered=false;')
|
||||
for ns in ('Replication','ERR_Actors','SCR_FactionManager','InventoryStorageManagerComponent','ERR_DocumentComponent','ERR_LocationKind','Math'):
|
||||
for ns in ('Replication','ERR_Actors','SCR_FactionManager','InventoryStorageManagerComponent','SCR_InventoryStorageManagerComponent','SCR_EntityHelper','ERR_DocumentComponent','ERR_LocationKind','Math'):
|
||||
s=s.replace(ns+'.',ns+'::')
|
||||
for obj in ('m_Mode','player','folder','location','document','chosen'):
|
||||
for obj in ('m_Mode','player','folder','location','document','chosen','scrInventory'):
|
||||
s=re.sub(r'\b'+obj+r'\.',obj+'->',s)
|
||||
for method in ('Players','Evidence','Settings','MineIntel','GetGame','GetPlayerManager'):
|
||||
s=s.replace(method+'().',method+'()->')
|
||||
@@ -27,8 +27,10 @@ template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct Faction{};
|
||||
struct ERR_DocumentComponent{string target;string TargetId(){return target;}static ERR_DocumentComponent* Cast(void*p){return (ERR_DocumentComponent*)p;}};
|
||||
struct InventoryStorageManagerComponent{Array<IEntity> folders;static InventoryStorageManagerComponent* Cast(void*p){return (InventoryStorageManagerComponent*)p;}};
|
||||
struct Entity{bool alive=true,canonical=true;Faction*faction=nullptr;InventoryStorageManagerComponent*inventory=nullptr;ERR_DocumentComponent*document=nullptr;void*FindComponent(std::string type){if(type=="InventoryStorageManagerComponent")return inventory;if(type=="ERR_DocumentComponent")return document;return nullptr;}};
|
||||
struct InventoryStorageManagerComponent{virtual ~InventoryStorageManagerComponent()=default;Array<IEntity> folders;static InventoryStorageManagerComponent* Cast(void*p){return (InventoryStorageManagerComponent*)p;}};
|
||||
struct SCR_InventoryStorageManagerComponent:InventoryStorageManagerComponent{bool acceptDelete=true;int deleteCalls=0;IEntity deleted=nullptr;static SCR_InventoryStorageManagerComponent*Cast(InventoryStorageManagerComponent*p){return dynamic_cast<SCR_InventoryStorageManagerComponent*>(p);}bool TryDeleteItem(IEntity item){deleteCalls++;if(acceptDelete)deleted=item;return acceptDelete;}};
|
||||
struct Entity{bool alive=true,canonical=true,deleted=false;Faction*faction=nullptr;InventoryStorageManagerComponent*inventory=nullptr;ERR_DocumentComponent*document=nullptr;void*FindComponent(std::string type){if(type=="InventoryStorageManagerComponent")return inventory;if(type=="ERR_DocumentComponent")return document;return nullptr;}};
|
||||
namespace SCR_EntityHelper{void DeleteEntityAndChildren(IEntity e){e->deleted=true;}}
|
||||
namespace Replication{bool server=true;bool IsServer(){return server;}}
|
||||
namespace ERR_Actors{bool Alive(IEntity e){return e&&e->alive;}}
|
||||
namespace SCR_FactionManager{Faction*SGetFaction(IEntity e){return e?e->faction:nullptr;}}
|
||||
@@ -36,7 +38,7 @@ namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,DEPOT};}
|
||||
namespace Math{int RandomInt(int,int){return 99;}}
|
||||
struct ERR_LocationComponent{string id;int kind=ERR_LocationKind::RADIO,knowledge=0,calls=0;bool allow=true;int Kind(){return kind;}int Knowledge(){return knowledge;}string Id(){return id;}bool Discover(int,int){calls++;if(!allow)return false;knowledge++;return true;}};
|
||||
struct Roster{IEntity CurrentActor(IEntity e){return e&&e->canonical?e:nullptr;}};
|
||||
struct Evidence{IEntity sealed=nullptr;string target;string Target(IEntity e){return e==sealed?target:"";}};
|
||||
struct Evidence{IEntity sealed=nullptr,retired=nullptr;string target;string Target(IEntity e){return e==sealed?target:"";}void Retire(IEntity e){retired=e;}};
|
||||
struct Settings{int Runtime(string){return 15;}};
|
||||
struct MineIntel{bool newlyRecovered=false;int calls=0;Array<IEntity> recovered;Array<IEntity>*spent=nullptr;IEntity lastPlayer=nullptr;bool Recover(IEntity player,IEntity folder){assert(spent&&!spent->Contains(folder));calls++;lastPlayer=player;recovered.Insert(folder);return newlyRecovered;}};
|
||||
struct Mode{bool worldPreparing=false;bool WorldPreparing(){return worldPreparing;}Roster roster;struct Evidence evidence;struct Settings settings;struct MineIntel mine;Faction*faction=nullptr;bool haveMine=true;Array<ERR_LocationComponent*>locations;int exhausted=0;
|
||||
@@ -46,7 +48,7 @@ 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;}
|
||||
'''+s+r'''
|
||||
};
|
||||
struct Fixture{Faction friendly,foreign;Mode mode;Discovery discovery;Entity player,second,folder;InventoryStorageManagerComponent firstInv,secondInv;ERR_DocumentComponent objective;ERR_LocationComponent radio,other;
|
||||
struct Fixture{Faction friendly,foreign;Mode mode;Discovery discovery;Entity player,second,folder;SCR_InventoryStorageManagerComponent firstInv,secondInv;ERR_DocumentComponent objective;ERR_LocationComponent radio,other;
|
||||
Fixture(){Replication::server=true;mode.faction=&friendly;discovery.m_Mode=&mode;mode.mine.spent=&discovery.m_Spent;player.faction=&friendly;second.faction=&friendly;player.inventory=&firstInv;second.inventory=&secondInv;firstInv.folders.Insert(&folder);radio.id="radio";other.id="other";mode.locations={&other,&radio};}}
|
||||
;
|
||||
int main(){
|
||||
@@ -54,10 +56,19 @@ 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.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
|
||||
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
|
||||
// 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.
|
||||
|
||||
@@ -24,6 +24,7 @@ def check():
|
||||
subprocess.run([sys.executable,str(ROOT/'tests/test_civilian_traffic.py')],check=True)
|
||||
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_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)
|
||||
|
||||
Reference in New Issue
Block a user