Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,13 @@
Finite mine top-up coordinator proposal (target remains unchanged by this agent)
Apply fix.patch after /tmp/erri-mine-grant-1724/fix.patch and /tmp/erri-mine-transfer-1724/fix.patch. git apply --check passed against the current successor.
Register only new tests/test_infantry_mine_coordinator.py; five other tests are existing programs adapted to the expanded production interface. Eight focused programs pass via python /tmp/erri-mine-resupply-1724/run_tests.py; logs retained alongside patch.
Production changes: ERR_HQ, ERR_InfantryResupply, ERR_InfantrySupplyPool.
HQ tries existing infantry ammunition, then mounted ammunition, then one finite PMN4. Mine admission consumes only existing delivered <=120s complete mine reports with ReadyMines exactly 1; zero/unknown/multiple never establish demand. Original source/roster/radio/downlink/rendezvous/time/ownership checks remain shared. Optional original MineStock batch never delays normal magazine readiness. Its exact <=2 closed identities are copied once and paired with the SAME service m_Spent ledger, with unchanged 96-spent ceiling. No independent spent pool, new actor/vehicle, resource creation or engineer-attempt reset.
At physical rendezvous, select a permanently attributable original on-foot carrier with exactly one native-ready mine and a settled original startup grant (HQ.MineSupplyReady). Pin carrier across pre-submit retries. Leaf independently pins the exact existing carried mine and exact incoming original truck mine. At most one confirmed transfer ends the trip. An unsubmitted refusal may release its reservation; submitted identities remain spent permanently.
New service state7 retains both recipient LogisticsReservation and exact truck SupplyToken/held flag after failed/stopped unresolved mine submission. Owned waypoints and downlink are cleared immediately. PollTransfers releases matching claims only when leaf proves terminal custody or exact incoming deletion. Thus a second truck with a newer report after shared cooldown still cannot race the same carrier. Existing stopped/pool polling retains uncertainty without new orders. Pool diagnostics separately label magazines moved versus mines moved and include state7 among active claims.
No native run in this proposal; tests execute extracted production methods against native boundary doubles. Actual navigation, inventory capacity/transfer and multiplayer remain native qualification limits. Independent reviewer is testing removal of state7 as a negative control. No claim that this adds mine placement repeatability or mine resupply to zero-issue formations.
@@ -0,0 +1,2 @@
PASS: automatic inventory one-shot roll, exact item ownership, finite compatible ammo, rejection, capture/stop, deletion, loot and late callback quarantine
PASS: enabled/valid catalog eligibility, category restrictions, content filtering, GUID deduplication across factions and bounded catalogs
@@ -0,0 +1,20 @@
--- a/tests/test_admin_menu.py
+++ b/tests/test_admin_menu.py
@@ -10,7 +10,7 @@
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
-assert len(keys) == 14 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious"}
+assert len(keys) == 14 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
@@ -109,7 +109,7 @@
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
- for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
+ for(string key:{"automaticEquipmentChancePercent","heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
@@ -0,0 +1 @@
Concurrent automatic-equipment production changes added automaticEquipmentChancePercent as a text-command-only setting plus the hq equipment alias. This test-only proposal keeps all 14 menu controls and the exact hidden-key set assertion, explicitly rejects the new key in menu Command/Available, and verifies the actual HQ text alias. No production bytes changed. Eleven settings/admin programs pass; ai_stinger_admin requires no change. Logs and run.py retained here. Proposal consists of test_admin_menu.py and test_admin_providers.py only.
@@ -0,0 +1,11 @@
import subprocess,os
from pathlib import Path
r=Path('/home/jakub/devel/escapistsrereintroduced');p=Path('/tmp/erri-mine-resupply-1724/compat');env=os.environ|{'PYTHONPATH':str(r/'tests'),'ERR_CONVOY_SETTINGS_ROOT':str(r)}
names=sorted({x.name for x in (r/'tests').glob('test_admin*.py')}|{x.name for x in (r/'tests').glob('test*settings*.py')}|{'test_ai_stinger_admin.py','test_intel_admin.py'})
failed=[]
for name in names:
f=p/'tests'/name
if not f.exists():f=r/'tests'/name
x=subprocess.run(['python',str(f)],env=env,text=True,stdout=subprocess.PIPE,stderr=subprocess.STDOUT);(p/(name+'.log')).write_text(x.stdout);print(name,x.returncode)
if x.returncode:failed.append(name);print(x.stdout[-2500:])
assert not failed,failed
@@ -0,0 +1,2 @@
PASS: actual menu command routing covers all fourteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets
PASS: explicit weapon retention buttons use the same ready/authorized server command path
@@ -0,0 +1,2 @@
PASS: actual admin body authorization, god restoration, vanilla item deposit, callback completion/failure, timeout quarantine and stop/revocation paths
PASS: same-body revoke/regrant preserves pending insertions and timeout quarantine; new-body binding isolates late callbacks and never inherits god mode
@@ -0,0 +1 @@
PASS: mission provider registration is idempotent, optional dispatch is ordered, status aggregates, journals preserve cursors, aliases stay with their addons and topic help is bounded
@@ -0,0 +1 @@
PASS: actual current-controller/admin/mode authorization, per-round anti-replay, token-rotation refusal, command/snapshot throttles and client scope; owner-only replies and player-tool cleanup wiring
@@ -0,0 +1 @@
PASS: Core document-discovery alias, companion-only alias exclusion, strict integer parsing and setting boundaries
@@ -0,0 +1 @@
PASS: actual session Tick + journal router delivers each grant completion and new HQ line once, without repeats on empty/unavailable feeds or unauthorized ticks
@@ -0,0 +1 @@
PASS: actual Stinger show/set/help state precise actor-roll effects; read-only help, bounded values, malformed syntax and failed-save preservation; no rerolls/gameplay edits and no native persistence claim
@@ -0,0 +1 @@
PASS: actual read migrates old valid settings, preserves explicit disabled state, rejects invalid data; next-round feedback and fixed menu remain honest
@@ -0,0 +1 @@
PASS: actual Intel commands distinguish status/start/failure, preserve authorization gate and reject malformed requests; spent ledger is idempotent, server-only and bounded
@@ -0,0 +1 @@
PASS: random mode restrictions, full numeric ranges, independent calendar dates and strict daylight boundaries
@@ -0,0 +1,2 @@
PASS: legacy values preserved, unsupported fields reported, invalid data rejected, existing files protected, runtime bounds aligned
PASS: failed initial settings promotion leaves defaults writable after restart; prior/fallback saves survive copy, readback and mismatch faults
@@ -0,0 +1,129 @@
"""Exercise real menu routing/capability bodies and the authored control contract.
This does not claim native focus, rendering or multiplayer RPC coverage.
"""
import re,os
from pathlib import Path
BASE=Path(os.environ.get("ERR_CONVOY_SETTINGS_ROOT", Path(__file__).resolve().parents[1]))
from policy_harness import ROOT, body, run
source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
assert len(keys) == 14 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious", "automaticEquipmentChancePercent"}
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
# Native layouts place child widget declarations inside an anonymous Children
# block. A missing brace can preserve most widget names yet prevent menu load.
def validate_layout_structure(text):
tokens = re.findall(r'"(?:[^"\\]|\\.)*"|[{}]|[^\s{}"]+', text)
stack = []
prefix = []
for token in tokens:
if token.endswith('WidgetClass'):
assert not stack or stack[-1] == 'children', 'Widget outside parent Children block: ' + token
if token == '{':
kind = 'property'
if any(part.endswith('WidgetClass') for part in prefix):
kind = 'widget'
elif not prefix:
kind = 'children'
stack.append(kind)
prefix = []
elif token == '}':
assert stack, 'Unexpected closing brace'
stack.pop()
prefix = []
else:
prefix.append(token)
assert not stack, 'Unclosed layout block'
validate_layout_structure(layout.read_text())
try:
validate_layout_structure(layout.read_text().replace('Name "JournalState"', 'Name "JournalState" {', 1))
except AssertionError:
pass
else:
raise AssertionError('Malformed child structure must be rejected')
names = re.findall(r'\bName "([^"]+)"', layout.read_text())
assert 'RandomDay_startTimeMinutes' in names
assert len(names) == len(set(names)), 'Menu widget names must be unique'
for key in keys:
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, ('Default_' if key == 'viewDistanceMetres' else 'Random_') + key, 'Help_' + key))
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
assert name in names
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
assert name in names
journal = body(source, 'static string JournalText(').replace('ERR_JournalText.Menu', 'ERR_JournalText::Menu')
menu_format = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c', 'static string Menu(')
follow = body(source, 'static bool FollowJournal(')
command = body(source, 'static string Command(')
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
known = ' || '.join('key == "' + key + '"' for key in keys)
run(r'''
#include <cassert>
#include <string>
#include <vector>
struct string : std::string {
using std::string::string;
string(std::string s):std::string(s){}
bool IsEmpty(){return empty();}
int Length(){return size();}
int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
string Substring(int i,int n){return substr(i,n);}
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
};
struct ERR_JournalText {
''' + menu_format + r'''
};
struct Fields {
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
};
int main() {
string raw="#24 t=7s M3 observe guard";
assert(Fields::JournalText(raw)=="No. 24 t=7s M3 observe guard");
assert(raw=="#24 t=7s M3 observe guard");
assert(Fields::JournalText("unprefixed event")=="unprefixed event");
assert(Fields::JournalText("#25 delivered #24")=="No. 25 delivered No. 24");
assert(Fields::JournalText("").empty());
assert(!Fields::FollowJournal(false,0.5,1000,400,false));
assert(!Fields::FollowJournal(false,0,1000,400,false));
assert(!Fields::FollowJournal(false,0.97,1000,400,false));
assert(Fields::FollowJournal(false,1,1000,400,false));
assert(Fields::FollowJournal(false,0,1000,400,true));
assert(Fields::FollowJournal(false,0,200,400,false));
assert(Fields::FollowJournal(true,0,0,400,false));
std::vector<std::pair<string,string>> cases={
{"radioDefenceSeconds","mission radio"},{"aiIntelChancePercent","intel carrying"},
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
{"weatherProfile","mission weather"},{"viewDistanceMetres","mission viewdistance"},{"reportIntervalSeconds","hq interval"},
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
for(auto pair:cases){
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
assert(Fields::Command(pair.first,"").empty());
assert(Fields::Command(pair.first," \t ").empty());
assert(Fields::Command(pair.first,string(33,'x')).empty());
assert(Fields::Available(pair.first,true,true));
}
assert(Fields::Command("startTimeMinutes","14:25")=="mission time set 14:25");
assert(Fields::Command("startDateCode","2026-09-19")=="mission date set 2026-09-19");
assert(Fields::Command("weatherProfile","authored")=="mission weather set authored");
for(string key:{"automaticEquipmentChancePercent","heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
}
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
}
assert(!Fields::Available("aiIntelChancePercent",true,false));
assert(Fields::Available("aiIntelChancePercent",false,true));
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
}
''')
print('PASS: actual menu command routing covers all fourteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
assert all(name in names for name in ('KeepWeaponOn', 'KeepWeaponOff', 'Saved_keepWeaponOnUnconscious'))
click=body(source,'protected void OnButton(')
assert 'm_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true)' in click
assert click.index('!m_Ready') < click.index('"mission keepweapon set "')
print('PASS: explicit weapon retention buttons use the same ready/authorized server command path')
@@ -0,0 +1,75 @@
"""Exercise actual registration/dispatch bodies without requiring optional addons.
Native script compilation and authenticated RPC tests remain separate checks.
"""
from policy_harness import ROOT, body, run
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminService.c'
hq = ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAdminProvider.c'
intel = ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelAdminProvider.c'
source=core.read_text()
assert 'ERR_HQ' not in source and 'ERR_IntelService' not in source
assert 'modded class' not in (ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_AdminCommands.c').read_text()
assert 'Admin().Register(new ERR_HQAdminProvider(hq))' in (ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c').read_text()
assert 'Admin().Register(new ERR_IntelAdminProvider())' in (ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelService.c').read_text()
execute=body(core,'void Execute(')
assert execute.index('if (!Replication.IsServer()') < execute.index('if (!authorized)') < execute.index('Extension(mode, arguments, replies)')
assert 'arguments = "mission " + arguments' in execute
def translate(s):
return (s.replace('protected ', '').replace('override ', '')
.replace('array<string>', 'Array<string>&').replace('out int ', 'int& ')
.replace('ERR_AdminProvider provider', 'Provider* provider').replace('ERR_GameMode mode','Mode* mode')
.replace('foreach (Provider* provider : m_Providers)', 'for (Provider* provider : m_Providers)')
.replace('ERR_CommandArguments.CoreSetting(', 'CoreSetting(')
.replace('provider.', 'provider->').replace('m_Providers[i].', 'm_Providers[i]->')
.replace('.IsEmpty()', '.empty()').replace('bool handled;', 'bool handled = false;'))
methods='\n'.join(translate(body(core,sig)) for sig in ('void Register(', 'void Capabilities(', 'int JournalSequence(', 'bool ReadJournal(', 'protected string Setting(', 'protected bool Extension('))
aliases='struct HQAliases{'+translate(body(hq,'override string Setting('))+'};\nstruct IntelAliases{'+translate(body(intel,'override string Setting('))+'};'
help_bodies='struct HQHelp{'+translate(body(hq,'override bool Help('))+'};\nstruct IntelHelp{'+translate(body(intel,'override bool Help('))+'};'
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;
template<class T> struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}void Clear(){this->clear();}int Count(){return this->size();}};
struct Mode{};
string CoreSetting(string section,string option){if(section=="intel"&&option=="reveal")return "intelRevealChancePercent";return "";}
struct Provider{
string id;int sequence=-1;int calls=0;bool accepts=false;int reads=0;
string Id(){return id;}void Capabilities(Array<string>& values){values.Insert("supports="+id);}
int JournalSequence(){return sequence;}
bool ReadJournal(int after,Array<string>& lines,int& cursor,int limit=8){if(sequence<0)return false;reads++;cursor=std::min(sequence,after+limit);lines.Insert(id);return true;}
string Setting(string section,string){return section==id?id+"Setting":"";}
bool Execute(Mode*,string,Array<string>& replies){calls++;if(accepts)replies.Insert(id);return accepts;}
};
struct Admin{Array<Provider*>m_Providers;
'''+methods+'};\n'+aliases+help_bodies+r'''
int main(){
Admin admin;Mode mode;Array<string> lines;int cursor=0;
assert(admin.JournalSequence()==-1&&!admin.ReadJournal(0,lines,cursor));
assert(admin.Setting("intel","reveal")=="intelRevealChancePercent"&&admin.Setting("hq","squads").empty());
admin.Register(nullptr);Provider empty;admin.Register(&empty);assert(admin.m_Providers.empty());
Provider hq;hq.id="hq";hq.sequence=12;hq.accepts=true;
Provider intel;intel.id="intel";intel.accepts=true;
admin.Register(&hq);admin.Register(&hq);admin.Register(&intel);assert(admin.m_Providers.size()==2);
admin.Capabilities(lines);assert(lines.size()==2&&lines[0]=="supports=hq"&&lines[1]=="supports=intel");
assert(admin.JournalSequence()==12);assert(admin.ReadJournal(2,lines,cursor,4)&&cursor==6&&lines.size()==1&&lines[0]=="hq");
assert(hq.reads==1&&intel.reads==0);
lines.clear();assert(admin.Extension(&mode,"status",lines)&&lines.size()==2&&hq.calls==1&&intel.calls==1);
lines.clear();assert(admin.Extension(&mode,"hq show",lines)&&lines.size()==1&&hq.calls==2&&intel.calls==1);
hq.accepts=false;intel.accepts=false;lines.clear();assert(!admin.Extension(&mode,"unknown",lines)&&lines.empty());
Provider replacement;replacement.id="hq";replacement.sequence=20;
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
HQAliases ha;IntelAliases ia;
assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
assert(ha.Setting("hq","aa")=="antiAirChancePercent"&&ha.Setting("hq","mines")=="antiPersonnelMineChancePercent");
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
lines.clear();assert(!hh.Help("unknown",lines)&&!ih.Help("unknown",lines)&&lines.empty());
}
''')
print('PASS: mission provider registration is idempotent, optional dispatch is ordered, status aggregates, journals preserve cursors, aliases stay with their addons and topic help is bounded')
@@ -0,0 +1,610 @@
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -347,6 +347,10 @@
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) return unit.LogisticsReservation;
return 0;
}
+ bool MineSupplyReady(SCR_AIGroup group, IEntity actor)
+ {
+ return Replication.IsServer() && MissionActive() && m_Equipment && m_Equipment.MineSupplyReady(group, actor);
+ }
bool ReserveLogistics(SCR_AIGroup group, int token)
{
if (!Replication.IsServer() || token <= 0 || !MissionActive()) return false;
@@ -1292,6 +1296,10 @@
// finite truck and delivered radio entries only after that scan fails.
foreach (ERR_HQUnit mounted : m_Units)
if (!ConvoyContact(mounted, now) && supply.TryPlanMounted(mounted, donor, m_Watch.Find(mounted.ForceSourceId), donorStatus, now)) return true;
+ // Optional finite mines are last: only a delivered known one-mine formation
+ // may top up an existing carrier; no new issue or engineer-task reset.
+ foreach (ERR_HQUnit mineRecipient : m_Units)
+ if (supply.TryPlanMines(mineRecipient, donor, m_Watch.Find(mineRecipient.ForceSourceId), donorStatus, now)) return true;
return false;
}
protected void PlanConvoy(float now)
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c
@@ -13,6 +13,13 @@
&& entry.AmmoState.Complete && entry.AmmoState.Members == entry.Count
&& entry.AmmoState.SmallArmsMuzzles > 0
&& entry.AmmoState.SmallArmsRounds + entry.AmmoState.SmallArmsChambers <= entry.Count * 30;
+ }
+ static bool MinePair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
+ {
+ return Fresh(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
+ && recipient.AmmoState && recipient.AmmoState.Valid(recipient.Count)
+ && recipient.AmmoState.MineComplete && recipient.AmmoState.MineMembers == recipient.Count
+ && recipient.AmmoState.ReadyMines == 1 && vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
}
static bool Pair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
{
@@ -32,18 +39,25 @@
protected ERR_HQUnit m_Donor;
protected ref array<IEntity> m_Stock = {};
protected ref array<IEntity> m_Spent = {};
+ protected ref array<IEntity> m_MineStock = {};
+ protected ref ERR_ConvoyMineStock m_MineBatch;
+ protected bool m_MineStockCaptured;
+ protected bool m_MineTrip;
+ protected int m_MineRecipientSlot = -1;
+ protected int m_MinesDelivered;
protected ref array<IEntity> m_Members = {};
protected ref array<IEntity> m_Served = {};
protected ref map<string, int> m_Consumed = new map<string, int>();
protected ref map<string, float> m_NextAttempt = new map<string, float>();
protected ref ERR_HQRoute m_Order;
protected ref ERR_InfantryAmmoTransfer m_Move;
+ protected ref ERR_InfantryMineTransfer m_MineMove;
protected ref ERR_MountedResupply m_Mounted;
protected IEntity m_Receiving;
protected AIWaypoint m_MoveWaypoint;
protected AIWaypoint m_HoldWaypoint;
protected vector m_Rendezvous;
- protected int m_State; // 0 stock startup, 1 idle, 2 truck downlink, 3 stopped status, 4 infantry downlink, 5 transfer
+ protected int m_State; // 0 stock startup, 1 idle, 2 truck downlink, 3 stopped status, 4 infantry downlink, 5 transfer, 7 unresolved mine custody
protected int m_Trip;
protected int m_Token;
protected int m_ReportId;
@@ -73,16 +87,23 @@
return m_State;
}
ERR_MountedResupply Mounted() { return m_Mounted; }
- bool Unresolved() { return (m_Move && m_Move.Unresolved()) || (m_Mounted && m_Mounted.Unresolved()); }
+ bool Unresolved() { return (m_Move && m_Move.Unresolved()) || (m_MineMove && m_MineMove.Unresolved()) || (m_Mounted && m_Mounted.Unresolved()); }
protected void PollTransfers(float now)
{
if (m_Mounted) m_Mounted.Tick(now);
if (m_State <= 1 && m_Move && !m_Move.Unresolved()) m_Move = null;
+ if (m_MineMove && !m_MineMove.Unresolved() && (m_State <= 1 || m_State == 7))
+ {
+ m_MineMove = null;
+ if (m_State == 7) Finish("mine custody settled; original claims released without retry or refund");
+ }
}
int Delivered() { return m_Delivered; }
+ int MinesDelivered() { return m_MinesDelivered; }
int StockCount() { return m_Stock.Count(); }
protected void Audit(string reason)
{
+ if (m_MineTrip) reason = "mine top-up: " + reason;
m_Reason = reason;
m_HQ.Journal().Write("INFANTRY SUPPLY", string.Format("convoy-slot=%1 trip=%2 state=%3 moved=%4 spent=%5/%6; %7", m_ConvoySlot, m_Trip, m_State, m_Delivered, m_Spent.Count(), m_Stock.Count(), reason), m_ReportId);
}
@@ -163,6 +184,7 @@
if (!m_Convoy.CargoIssued) return;
ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Convoy);
if (supply && !supply.MagazinesIssued) return;
+ if (supply) m_MineBatch = supply.MineStock;
foreach (ERR_ConvoyCargoCallback operation : m_Convoy.Cargo)
if (!operation.Finished) return;
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Truck.Entity.FindComponent(InventoryStorageManagerComponent));
@@ -177,17 +199,28 @@
}
m_State = 1; Audit("truck magazine identities captured once after startup; absent/looted stock is never replaced");
}
- bool TryPlan(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
- {
- if (!Current() || m_State != 1 || m_Move || (m_Mounted && m_Mounted.Busy()) || m_Trip >= 128 || !TruckValid()) return false;
+ bool TryPlan(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now, bool mineTrip = false)
+ {
+ if (!Current() || m_State != 1 || m_Move || m_MineMove || (m_Mounted && m_Mounted.Busy()) || m_Trip >= 128 || !TruckValid()) return false;
if (m_Truck.Combat.Held || m_Truck.SupplyToken > 0 || !recipient || !donor) return false;
if (donor.Group != m_Truck.Group || recipient.HomeGuard || !Quiet(recipient) || !Quiet(donor)) return false;
if (donor.Downlink || donor.PendingOrder || donor.PendingConvoyOrder) return false;
- if (!ERR_InfantryResupplyRules.Pair(demand, donorStatus, now)) return false;
+ if (mineTrip)
+ {
+ if (!ERR_InfantryResupplyRules.MinePair(demand, donorStatus, now)) return false;
+ }
+ else if (!ERR_InfantryResupplyRules.Pair(demand, donorStatus, now)) return false;
if (demand.SourceId != recipient.ForceSourceId || donorStatus.SourceId != donor.ForceSourceId) return false;
if (demand.MessageId <= m_Consumed.Get(demand.SourceId) || now < m_NextAttempt.Get(demand.SourceId)) return false;
bool available;
- foreach (IEntity magazine : m_Stock) if (magazine && !m_Spent.Contains(magazine) && Contains(m_Truck.Entity, magazine)) { available = true; break; }
+ if (mineTrip)
+ {
+ if (!CaptureMines()) return false;
+ foreach (IEntity mine : m_MineStock)
+ if (ERR_InfantryMineTransfer.ReadyMine(mine) && !m_Spent.Contains(mine) && Contains(m_Truck.Entity, mine)) { available = true; break; }
+ }
+ else foreach (IEntity magazine : m_Stock) if (magazine && !m_Spent.Contains(magazine) && Contains(m_Truck.Entity, magazine)) { available = true; break; }
+ if (m_Spent.Count() >= 96) return false;
if (!available) return false;
m_Members.Clear();
for (int i = 0; i < recipient.StatusSampler.InitialStrength(); i++)
@@ -202,18 +235,42 @@
if (slot < 0) slot = 0; // Isolated legacy fixture uses the first convoy's range.
int token = 100000 + slot * 128 + m_Trip + 1;
if (!m_HQ.ReserveLogistics(recipient.Group, token)) { m_Recipient = null; m_Donor = null; return false; }
+ m_MineTrip = mineTrip; m_MineRecipientSlot = -1; m_Receiving = null;
m_Trip++; m_Token = token; m_Truck.SupplyToken = token; m_ReportId = demand.MessageId; m_TripDelivered = 0; m_Served.Clear();
m_Consumed.Set(demand.SourceId, demand.MessageId); m_NextAttempt.Set(demand.SourceId, now + 180000);
m_Deadline = Math.Min(now + 120000, demand.ObservedAt + 180000); m_State = 2;
- m_Order = m_HQ.Downlink(donor.Group, "INFANTRY SUPPLY HOLD", "stop existing ammo truck and report position", m_ReportId);
+ string purpose = "stop existing ammo truck and report position";
+ if (mineTrip) purpose += "; one finite PMN4 top-up for an existing carrier";
+ m_Order = m_HQ.Downlink(donor.Group, "INFANTRY SUPPLY HOLD", purpose, m_ReportId);
if (!m_Order) Finish("truck radio downlink unavailable");
else Audit("existing patrol reserved; truck hold awaits radio delivery");
return true;
}
+ // Optional mine startup must never delay normal magazine readiness. Pin the
+ // original batch during Stock(), then copy its closed exact identities once.
+ protected bool CaptureMines()
+ {
+ ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Convoy);
+ if (!supply || !m_MineBatch || supply.MineStock != m_MineBatch || !m_MineBatch.Complete()) return false;
+ if (m_MineStockCaptured) return !m_MineStock.IsEmpty();
+ m_MineStockCaptured = true;
+ int count = m_MineBatch.Count();
+ if (count < 0 || count > 2) return false;
+ for (int index = 0; index < count; index++)
+ {
+ IEntity item = m_MineBatch.At(index);
+ if (item && !m_MineStock.Contains(item)) m_MineStock.Insert(item);
+ }
+ return true;
+ }
+ bool TryPlanMines(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
+ {
+ return TryPlan(recipient, donor, demand, donorStatus, now, true);
+ }
// Share exact frozen inventory identities and global radio replay state.
bool TryPlanMounted(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
{
- if (!Current() || m_State != 1 || m_Move || m_ConvoySlot < 0 || !TruckValid(true)) return false;
+ if (!Current() || m_State != 1 || m_Move || m_MineMove || m_ConvoySlot < 0 || !TruckValid(true)) return false;
if (m_Mounted && m_Mounted.Busy()) return false;
if (!m_Mounted) m_Mounted = new ERR_MountedResupply(m_HQ, m_Mode, m_Convoy, m_ConvoySlot, m_Stock, m_Spent, m_Consumed, m_NextAttempt);
return m_Mounted.Begin(recipient, donor, demand, donorStatus, now);
@@ -239,10 +296,58 @@
{
Audit(reason);
if (m_Move) m_Move.Stop();
+ if (m_MineMove) m_MineMove.Stop();
ClearWaypoints();
+ m_Order = null;
+ // A late native move must not race another truck's newer report. Retain both
+ // exact claims until this one submitted identity proves custody or deletion.
+ if (m_MineMove && m_MineMove.Unresolved()) { m_State = 7; return; }
if (m_Recipient) m_HQ.ReleaseLogistics(m_Recipient.Group, m_Token);
if (m_Truck && m_Truck.SupplyToken == m_Token) { m_Truck.SupplyToken = 0; m_Truck.SupplyHeld = false; }
- m_Recipient = null; m_Donor = null; m_Order = null; m_Receiving = null; m_State = 1;
+ m_Recipient = null; m_Donor = null; m_Order = null; m_Receiving = null; m_State = 1; m_MineTrip = false; m_MineRecipientSlot = -1;
+ }
+ protected void TransferMine(float now)
+ {
+ if (m_MineMove)
+ {
+ int result = m_MineMove.Tick(now);
+ if (result == 0) return;
+ if (result < 0) { Finish("native mine transfer failed or expired; exact submitted identity stays spent"); return; }
+ m_MineMove = null; m_MinesDelivered++;
+ Finish("one exact finite PMN4 confirmed in the original carrier inventory"); return;
+ }
+ if (!m_Receiving)
+ {
+ int strength = m_Recipient.StatusSampler.InitialStrength();
+ for (int slot = 0; slot < strength && slot < 32; slot++)
+ {
+ IEntity actor = m_Recipient.StatusSampler.InitialMember(slot);
+ if (!actor || !m_Members.Contains(actor) || !m_Recipient.StatusSampler.CanAttribute(slot, actor, false)) continue;
+ if (!ERR_Actors.CanFight(actor) || vector.DistanceXZ(actor.GetOrigin(), m_Truck.Entity.GetOrigin()) > 4) continue;
+ if (!m_HQ.MineSupplyReady(m_Recipient.Group, actor)) continue;
+ if (!ERR_InfantryMineTransfer.Carrier(actor, m_Recipient.Group)) continue;
+ m_Receiving = actor; m_MineRecipientSlot = slot; break;
+ }
+ if (!m_Receiving) return;
+ }
+ if (!m_HQ.MineSupplyReady(m_Recipient.Group, m_Receiving)) return;
+ if (!m_Recipient.StatusSampler.CanAttribute(m_MineRecipientSlot, m_Receiving, false)
+ || !ERR_InfantryMineTransfer.Carrier(m_Receiving, m_Recipient.Group))
+ { Finish("original one-mine carrier lost or no longer has exactly one usable mine"); return; }
+ foreach (IEntity mine : m_MineStock)
+ {
+ if (!ERR_InfantryMineTransfer.ReadyMine(mine) || m_Spent.Contains(mine) || !Contains(m_Truck.Entity, mine)) continue;
+ ERR_FiniteMagazineReservation reservation = new ERR_FiniteMagazineReservation(m_MineStock, m_Spent, mine);
+ if (!reservation.Reserve()) continue;
+ m_MineMove = new ERR_InfantryMineTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, m_Receiving, m_Recipient.Group,
+ mine, reservation, m_Recipient.StatusSampler, m_MineRecipientSlot);
+ m_MineMove.Tick(now);
+ // The leaf commits before the one native move. Only an unsubmitted refusal
+ // may release its tentative claim while the same carrier approaches/retries.
+ if (m_MineMove.Submitted()) return;
+ reservation.ReleaseUnsubmitted(); m_MineMove = null;
+ break;
+ }
}
protected void Transfer(float now)
{
@@ -287,6 +392,7 @@
m_NextTick = now + 1000;
PollTransfers(now);
if (m_Mounted && m_Mounted.Busy()) return;
+ if (m_State == 7) return;
if (m_State == 0) { Stock(now); return; }
if (m_State == 1)
{
@@ -331,9 +437,17 @@
if (delivered < 0 || !Waypoints()) { Finish("infantry order interrupted or native waypoint creation failed"); return; }
m_Order = null; m_State = 5; Audit("infantry received reported rendezvous; native movement and physical transfer required");
}
- if (m_State == 5) Transfer(now);
- }
- string Status() { return string.Format("Infantry supply: state=%1 trips=%2 moved=%3 stock=%4 spent=%5; %6", State(), m_Trip, m_Delivered, m_Stock.Count(), m_Spent.Count(), m_Reason); }
+ if (m_State == 5)
+ {
+ if (m_MineTrip) TransferMine(now);
+ else Transfer(now);
+ }
+ }
+ string Status()
+ {
+ string detail = string.Format("Ground supply: state=%1 trips=%2 infantry moved=%3 stock=%4 spent=%5", State(), m_Trip, m_Delivered, m_Stock.Count(), m_Spent.Count());
+ return detail + string.Format("; mines moved=%1 initial=%2; %3", m_MinesDelivered, m_MineStock.Count(), m_Reason);
+ }
void Stop()
{
if (m_Stopped) return;
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantrySupplyPool.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_InfantrySupplyPool.c
@@ -66,15 +66,15 @@
}
string Status()
{
- int trucks, working, moved, stock;
+ int trucks, working, moved, stock, mines;
foreach (ERR_InfantryResupply service : m_Services)
{
if (!service) continue;
trucks++;
- if (service.State() >= 2 && service.State() <= 6) working++;
- moved += service.Delivered(); stock += service.StockCount();
+ if (service.State() >= 2 && service.State() <= 7) working++;
+ moved += service.Delivered(); stock += service.StockCount(); mines += service.MinesDelivered();
}
- return string.Format("Ground supply: %1 original trucks; %2 active trips; infantry moved=%3 original stock=%4", trucks, working, moved, stock);
+ return string.Format("Ground supply: %1 original trucks; %2 active trips; magazines moved=%3 original magazine stock=%4; mines moved=%5", trucks, working, moved, stock, mines);
}
void Stop()
{
--- a/tests/test_hq_multiconvoy_supply.py
+++ b/tests/test_hq_multiconvoy_supply.py
@@ -8,7 +8,7 @@
methods='\n'.join(body(p,s) for s in ('void PlanResupply(float now)','bool PlanResupplyTask(ERR_InfantryResupply supply, float now)'))
methods=methods.replace('null','nullptr').replace('foreach (','for (')
methods=methods.replace('ERR_HQUnit donor;','ERR_HQUnit donor=nullptr;')
-for name in ('m_Resupply','supply','task','unit','donor','recipient','mounted','m_Watch'):
+for name in ('m_Resupply','supply','task','unit','donor','recipient','mounted','mineRecipient','m_Watch'):
methods=methods.replace(name+'.',name+'->')
methods=methods.replace('task->Vehicles[1].','task->Vehicles[1]->')
run(r'''
@@ -24,10 +24,11 @@
struct Vehicle{int Group=0;};
struct Task{bool Stopped=false,Ready=true;Array<Vehicle*> Vehicles;};using ERR_ConvoyTask=Task*;
struct Supply{
- int state=1,calls=0,accepted=0,mountedCalls=0,mountedAccepted=0;bool infantry=true,mounted=false;Task*task=nullptr;Unit* recipient=nullptr;Entry* demand=nullptr;Entry* donorReport=nullptr;
+ int state=1,calls=0,accepted=0,mountedCalls=0,mountedAccepted=0;bool infantry=true,mounted=false,mines=false;int mineCalls=0,mineAccepted=0;Task*task=nullptr;Unit* recipient=nullptr;Entry* demand=nullptr;Entry* donorReport=nullptr;
int State(){return state;}Task*Task(){return task;}
bool TryPlan(Unit*r,Unit*,Entry*d,Entry*s,float){calls++;if(!infantry||r!=recipient||!d||!s||d!=demand||s!=donorReport)return false;accepted++;state=2;return true;}
bool TryPlanMounted(Unit*r,Unit*,Entry*d,Entry*s,float){mountedCalls++;if(!mounted||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;mountedAccepted++;state=6;return true;}
+ bool TryPlanMines(Unit*r,Unit*,Entry*d,Entry*s,float){mineCalls++;if(!mines||r!=recipient||d!=demand||s!=donorReport||!d||!s)return false;mineAccepted++;state=2;return true;}
};using ERR_InfantryResupply=Supply*;
struct Pool{Array<Supply*> services;int Count(){return services.Count();}Supply*At(int i){return services[i];}};
struct HQ{
@@ -42,6 +43,10 @@
watch.delivered={{"a",&aStatus},{"b",&bStatus},{"recipient",&demand}};pool.services={&first,&second};hq.m_Resupply=&pool;hq.m_Watch=&watch;hq.m_Units={&da,&db,&recipient};}
};
int main(){
+ {Fixture f;f.first.infantry=false;f.first.mounted=false;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.mineAccepted==1&&f.first.mountedCalls>0&&f.second.calls==0);}
+ {Fixture f;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.mineCalls==0);}
+ {Fixture f;f.first.infantry=false;f.first.mounted=true;f.first.mines=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==1&&f.first.mineCalls==0);}
+
{Fixture f;f.first.mounted=true;f.hq.PlanResupply(100);assert(f.first.accepted==1&&f.first.mountedCalls==0);}
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.hq.PlanResupply(100);assert(f.first.accepted==0&&f.first.mountedAccepted==1&&f.second.calls==0);}
{Fixture f;f.first.infantry=false;f.first.mounted=true;f.recipient.fighting=true;f.hq.PlanResupply(100);assert(f.first.mountedAccepted==0);}
--- /dev/null
+++ b/tests/test_infantry_mine_coordinator.py
@@ -0,0 +1,77 @@
+"""Actual mine top-up selector plus the real shared finite reservation; native leaf is doubled."""
+import os,re
+from pathlib import Path
+from policy_harness import ROOT,body,run
+from test_finite_magazine_reservation import translated_reservation
+p=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
+s=body(p,'protected void TransferMine(').replace('protected ','').replace('null','nullptr')
+s=s.replace('ERR_InfantryMineTransfer.', 'ERR_InfantryMineTransfer::').replace('ERR_Actors.', 'ERR_Actors::').replace('vector.DistanceXZ','Vec::DistanceXZ')
+s=s.replace('foreach (IEntity mine : m_MineStock)','for (IEntity mine : m_MineStock)')
+s=s.replace('ERR_FiniteMagazineReservation reservation', 'ERR_FiniteMagazineReservation* reservation').replace('new ERR_FiniteMagazineReservation(m_MineStock, m_Spent, mine)', 'new ERR_FiniteMagazineReservation(&m_MineStock, &m_Spent, mine)')
+for x in ['m_MineMove','m_Recipient','m_Truck','m_HQ','actor','m_Receiving','reservation']:s=s.replace(x+'.',x+'->')
+for x in ['StatusSampler','Entity']:s=s.replace(x+'.',x+'->')
+run(r'''
+#include <cassert>
+#include <string>
+#include <vector>
+#include <algorithm>
+#include <cmath>
+using string=std::string;
+struct Vec{float x=0;static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}};
+struct Group{};struct Mode{};struct Entity{bool ready=false,fight=true,owned=true,grant=true;int mines=1;Vec pos;Vec GetOrigin(){return pos;}};using IEntity=Entity*;
+template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void RemoveItem(T v){this->erase(std::remove(this->begin(),this->end(),v),this->end());}};
+'''+translated_reservation()+r'''
+struct Sampler{Array<IEntity>original;int InitialStrength(){return original.Count();}IEntity InitialMember(int i){return original[i];}bool CanAttribute(int i,IEntity x,bool){return i>=0&&i<InitialStrength()&&original[i]==x&&x->owned;}};
+struct Unit{struct Group*Group;Sampler*StatusSampler;};struct Truck{IEntity Entity;struct Group*Group;};
+struct HQ{bool MineSupplyReady(Group*,IEntity x){return x&&x->grant;}};
+struct ERR_Actors{static bool CanFight(IEntity x){return x&&x->fight;}};
+struct ERR_InfantryMineTransfer{
+ static inline bool accept=true;static inline int made=0;IEntity item,actor;ERR_FiniteMagazineReservation*r;bool submitted=false;int result=0;
+ ERR_InfantryMineTransfer(Mode*,IEntity,Group*,IEntity a,Group*,IEntity i,ERR_FiniteMagazineReservation*q,Sampler*,int):item(i),actor(a),r(q){made++;}
+ static bool ReadyMine(IEntity x){return x&&x->ready;}
+ static IEntity Carrier(IEntity x,Group*){return x&&x->mines==1?x:nullptr;}
+ int Tick(float){if(accept&&!submitted){assert(r->Commit());submitted=true;}return result;}
+ bool Submitted(){return submitted;}
+};
+struct Service{
+ HQ*m_HQ;Mode*m_Mode;Unit*m_Recipient;Truck*m_Truck;IEntity m_Receiving=nullptr;int m_MineRecipientSlot=-1,m_MinesDelivered=0,finishes=0;ERR_InfantryMineTransfer*m_MineMove=nullptr;
+ Array<IEntity>m_Members,m_MineStock,m_Spent,cargo;
+ bool Contains(IEntity,IEntity x){return cargo.Contains(x);}void Finish(string){finishes++;}
+'''+s+r'''
+};
+struct Fixture{HQ h;Mode mode;Group g,d;Entity actor,other,truck,mine,second;Sampler sampler;Unit unit;Truck donor;Service s;
+ Fixture():sampler{{&actor,&other}},unit{&g,&sampler},donor{&truck,&d}{mine.ready=second.ready=true;s.m_HQ=&h;s.m_Mode=&mode;s.m_Recipient=&unit;s.m_Truck=&donor;s.m_Members={&actor,&other};s.m_MineStock={&mine,&second};s.cargo=s.m_MineStock;ERR_InfantryMineTransfer::accept=true;}
+};
+int main(){
+ // Never grant mines to zero/two-mine members or to unsettled initial grants.
+ for(int refusal=0;refusal<6;refusal++){Fixture f;f.other.mines=0;
+ if(refusal==0)f.actor.mines=0;if(refusal==1)f.actor.mines=2;if(refusal==2)f.actor.grant=false;if(refusal==3)f.actor.owned=false;if(refusal==4)f.actor.pos.x=5;if(refusal==5)f.actor.fight=false;
+ f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.empty()&&!f.s.m_Receiving);}
+ // One exact finite original identity and the same shared spent list.
+ {Fixture f;f.s.TransferMine(0);auto*leaf=f.s.m_MineMove;assert(leaf&&leaf->actor==&f.actor&&leaf->item==&f.mine&&f.s.m_Spent.Count()==1&&f.s.m_Spent[0]==&f.mine);
+ f.s.TransferMine(1000);assert(f.s.m_MineMove==leaf&&f.s.m_Spent.Count()==1);leaf->result=1;f.s.TransferMine(2000);assert(!f.s.m_MineMove&&f.s.m_MinesDelivered==1&&f.s.finishes==1&&f.s.m_Spent.Count()==1);}
+ // Preflight refusal may release only before native submission, keeping actor pinned.
+ {Fixture f;ERR_InfantryMineTransfer::accept=false;f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.empty()&&f.s.m_Receiving==&f.actor);
+ f.actor.mines=0;ERR_InfantryMineTransfer::accept=true;f.s.TransferMine(1000);assert(!f.s.m_MineMove&&f.s.finishes==1&&f.s.m_Spent.empty());}
+ {Fixture f;ERR_InfantryMineTransfer::accept=false;f.s.TransferMine(0);f.actor.grant=false;ERR_InfantryMineTransfer::accept=true;f.s.TransferMine(1000);assert(!f.s.m_MineMove&&f.s.m_Spent.empty());}
+ // Captured/absent original or already-spent cargo is unavailable; no substitute.
+ {Fixture f;f.s.cargo.clear();f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.empty());}
+ {Fixture f;f.s.m_Spent=f.s.m_MineStock;f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.Count()==2);}
+ {Fixture f;for(int i=0;i<96;i++)f.s.m_Spent.Insert(&f.truck);f.s.TransferMine(0);assert(!f.s.m_MineMove&&f.s.m_Spent.Count()==96);}
+ // Failure delegates cleanup while retaining the submitted leaf and spent item.
+ {Fixture f;f.s.TransferMine(0);auto*leaf=f.s.m_MineMove;leaf->result=-1;f.s.TransferMine(1000);assert(f.s.m_MineMove==leaf&&f.s.finishes==1&&f.s.m_Spent.Count()==1);}
+}
+'''.replace('if(refusal==0)f.actor.mines=0;if(refusal==1)f.actor.mines=2;if(refusal==2)f.actor.grant=false;if(refusal==3)f.actor.owned=false;if(refusal==4)f.actor.pos.x=5;if(refusal==5)f.actor.fight=false;', 'if(refusal==0){f.actor.mines=0;}if(refusal==1){f.actor.mines=2;}if(refusal==2){f.actor.grant=false;}if(refusal==3){f.actor.owned=false;}if(refusal==4){f.actor.pos.x=5;}if(refusal==5){f.actor.fight=false;}'))
+print('PASS: actual one-mine original carrier selection, settled startup, shared finite claims, no refund after submission')
+h=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
+w=body(h,'bool MineSupplyReady(').replace('SCR_AIGroup group','Group* group').replace('Replication.','Replication::').replace('m_Equipment.','m_Equipment->')
+run(r'''
+#include <cassert>
+struct Group{};struct Item{};using IEntity=Item*;
+struct Equipment{bool ready=false;Group*group=nullptr;IEntity actor=nullptr;bool MineSupplyReady(Group*g,IEntity a){return ready&&g==group&&a==actor;}};
+struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
+struct HQ{Equipment*m_Equipment=nullptr;bool active=true;bool MissionActive(){return active;}
+'''+w+r'''};
+int main(){Group group,other;Item actor;Equipment e;e.group=&group;e.actor=&actor;HQ h;assert(!h.MineSupplyReady(&group,&actor));h.m_Equipment=&e;assert(!h.MineSupplyReady(&group,&actor));e.ready=true;assert(h.MineSupplyReady(&group,&actor));assert(!h.MineSupplyReady(&other,&actor));h.active=false;assert(!h.MineSupplyReady(&group,&actor));h.active=true;Replication::server=false;assert(!h.MineSupplyReady(&group,&actor));}
+''')
+print('PASS: actual HQ startup grant readiness forwarding is authoritative and fail-closed')
--- a/tests/test_infantry_resupply_coordinator.py
+++ b/tests/test_infantry_resupply_coordinator.py
@@ -13,7 +13,7 @@
for kind in ['ERR_HQWatchEntry','Physics']:
s=re.sub(r'\b'+kind+r' (\w+)',kind+r'* \1',s)
# Enforce native entity/reference member access becomes C++ pointer member access.
- for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_Mounted','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
+ for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_MineMove','m_Mounted','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
s=s.replace(name+'.',name+'->')
for name in ['AmmoState','Group','Combat','Entity']:
s=s.replace(name+'.',name+'->')
@@ -50,16 +50,20 @@
struct Watch {ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(int){return entry;}};
struct HQ {int token=7,releases=0,downlinks=0;struct Watch watch;Route* next=nullptr;int Journal(){return 0;}int LogisticsToken(Group*){return token;}void ReleaseLogistics(Group*,int owned){releases++;if(token==owned)token=0;}struct Watch* Watch(){return &watch;} Route* Downlink(Group*,string,string,int){downlinks++;return next;}};
struct Service {
- HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
+ HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;bool m_MineTrip=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;Vec m_Rendezvous;int m_State=2,m_Token=7,m_ReportId=11;
float m_NextTick=0,m_Deadline=200000,m_HeldAt=-1;bool m_Stopped=false,valid=true,quiet=true,roster=true,waypoints=true;int audits=0,transfers=0,waypointCalls=0,stocks=0;
- bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}
+ bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}int mineTransfers=0;void TransferMine(float){mineTransfers++;}
METHODS
};
struct Fixture {Physics physics;Entity entity{&physics};Combat combat;Truck truck;Group group,donorGroup;Unit recipient{&group},donor{&donorGroup};HQ hq;Route order,next;Service s;
Fixture(){truck.Combat=&combat;truck.Entity=&entity;s.m_HQ=&hq;s.m_Truck=&truck;s.m_Recipient=&recipient;s.m_Donor=&donor;s.m_Order=&order;hq.next=&next;}
};
int main(){
+ {Fixture f;Leaf leaf;f.s.m_MineMove=&leaf;f.s.m_MineTrip=true;f.truck.SupplyHeld=true;f.s.Stop();assert(f.s.m_State==7&&f.hq.token==7&&f.truck.SupplyHeld&&f.s.m_Recipient);
+ f.s.Tick(200000);assert(f.hq.token==7&&f.s.m_State==7&&f.s.transfers==0&&f.s.mineTransfers==0);leaf.unresolved=false;f.s.Tick(201000);assert(f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_State==1&&!f.s.m_MineMove);}
+ {Fixture f;f.s.m_State=5;f.s.m_MineTrip=true;f.s.Tick(0);assert(f.s.mineTransfers==1&&f.s.transfers==0);}
+
Ammo ammo;ERR_HQWatchEntry demand,donor;demand.AmmoState=&ammo;donor.SourceId=2;donor.Position={200,500,0};
assert(ERR_InfantryResupplyRules::Need(&demand,120000));assert(ERR_InfantryResupplyRules::Pair(&demand,&donor,120000));
assert(!ERR_InfantryResupplyRules::Fresh(&demand,120001));assert(!ERR_InfantryResupplyRules::Fresh(nullptr,0));
@@ -119,7 +123,8 @@
struct Service{Unit* m_Recipient;Mode* m_Mode;Array<Actor*> m_Members;bool Quiet(Unit*){return true;}
METHOD
};
-int main(){Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
+int main(){
+Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
// Permanent sampler release survives returning to the physical group.
assert(!service.Roster());sampler.released=false;assert(service.Roster());
sampler.strength=33;assert(!service.Roster());sampler.strength=0;assert(!service.Roster());sampler.strength=1;
--- a/tests/test_infantry_resupply_planning.py
+++ b/tests/test_infantry_resupply_planning.py
@@ -13,11 +13,11 @@
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for typ in ('InventoryStorageManagerComponent','ERR_ForceRecord','ERR_SupplyConvoyTask'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
- for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
+ for name in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_InfantryMineTransfer','ERR_ConvoyRules','ERR_ForceKind','ERR_ConvoyResources','ERR_Inventory','ERR_Actors','SCR_FactionManager','CompartmentAccessComponent','Replication','Math'):
s=s.replace(name+'.',name+'::')
s=s.replace('ERR_SupplyConvoyTask.Cast','ERR_SupplyConvoyTask::Cast').replace('FindComponent(BaseMagazineComponent)','HasMagazine()')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('InventoryStorageManagerComponent.Cast','InventoryStorageManagerComponent::Cast').replace('FindComponent(InventoryStorageManagerComponent)','FindComponent()')
- for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','m_Mounted','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine','supply','m_Consumed','m_NextAttempt'):
+ for name in ('m_Mode','m_HQ','m_Convoy','m_Truck','m_Recipient','m_Donor','m_Move','m_MineMove','m_MineBatch','m_Mounted','recipient','donor','demand','donorStatus','entry','operation','inventory','item','owner','record','actor','agent','unit','m_MoveWaypoint','m_HoldWaypoint','magazine','supply','m_Consumed','m_NextAttempt'):
s=s.replace(name+'.',name+'->')
for name in ('Receipt','Entity','Group','Utility','StatusSampler','AmmoState','Combat','m_Perception'):
s=s.replace(name+'.',name+'->')
@@ -25,8 +25,8 @@
s=s.replace('ERR_SupplyConvoyTask* supply', 'Convoy* supply')
s=s.replace('bool available;','bool available=false;').replace('bool living;','bool living=false;')
return s
-rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair('))
-methods='\n'.join(cpp(body(P,x)) for x in ('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
+rules='\n'.join(cpp(body(P,x)) for x in ('static bool Fresh(', 'static bool Need(', 'static bool Pair(', 'static bool MinePair('))
+methods='\n'.join(cpp(body(P,x)) for x in ('bool Bind(', 'ERR_ConvoyTask Task(', 'protected bool ConvoyCurrent(', 'protected bool Contains(', 'protected bool TruckValid(', 'protected bool Quiet(', 'protected bool Roster(', 'protected void Stock(', 'protected bool CaptureMines(', 'bool TryPlanMines(', 'bool TryPlan(', 'protected void ClearWaypoints(', 'protected void Finish(', 'void Stop('))
reserve=cpp(body(H,'bool ReserveLogistics('))+cpp(body(H,'void ReleaseLogistics('))
unique=cpp(body(I,'static int AppendUnique('))+cpp(body(I,'static int Items('))
run(r'''
@@ -48,7 +48,7 @@
struct InventoryStorageManagerComponent{Array<IEntity>items;void GetItems(Array<IEntity>& out){out=items;}static auto Cast(InventoryStorageManagerComponent* p){return p;}};
struct Prefab{string name="magazine";string GetPrefabName(){return name;}};
struct Group;using SCR_AIGroup=Group*;
-struct Entity{Prefab prefab;bool magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
+struct Entity{Prefab prefab;bool readyMine=false,magazine=true,hasPrefab=true,alive=true,player=false,fight=true,aboard=false,captured=false,released=false;int faction=1;Group* group=nullptr;InventoryStorageManagerComponent* inventory=nullptr;bool HasMagazine(){return magazine;}Prefab* GetPrefabData(){return hasPrefab?&prefab:nullptr;}InventoryStorageManagerComponent* FindComponent(){return inventory;}};
struct Agent{IEntity actor;IEntity GetControlledEntity(){return actor;}};using AIAgent=Agent*;
struct Waypoint{};
struct Group{Array<AIAgent>agents;bool slave=false;int players=0,faction=1;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}void GetAgents(Array<AIAgent>& out){out=agents;}void RemoveWaypoint(Waypoint*){}};
@@ -58,7 +58,7 @@
struct ERR_Inventory{
'''+unique+r'''
};
-struct Ammo{bool Complete=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int n){return Members==n;}};
+struct Ammo{bool MineComplete=true;int MineMembers=2,ReadyMines=1;bool Complete=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int n){return Members==n;}};
struct Entry{bool Closed=false;int Count=2,Fit=2,MessageId=1;float ObservedAt=0;Vec Position;string SourceId;Ammo* AmmoState=nullptr;};using ERR_HQWatchEntry=Entry*;
struct ERR_InfantryResupplyRules{
'''+rules+r'''
@@ -75,19 +75,21 @@
struct Truck{int Index=1,SupplyToken=0;bool Registered=true,Released=false,Arrived=false,SupplyHeld=false,crew=true;struct Entity* Entity=nullptr;struct Group* Group=nullptr;struct Combat* Combat=nullptr;bool CrewValid(Mode*){return crew;}};
struct Receipt{bool Consumed=true;Array<int>Records={1,2,3,4,5,6};};
struct Cargo{bool Finished=true;};using ERR_ConvoyCargoCallback=Cargo*;
-struct Convoy{bool supply=false,MagazinesIssued=true,CargoIssued=true,Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
+struct MineBatch{bool complete=true;Array<IEntity>items;bool Complete(){return complete;}int Count(){return items.Count();}IEntity At(int i){return items[i];}};
+struct Convoy{MineBatch*MineStock=nullptr;bool supply=false,MagazinesIssued=true,CargoIssued=true,Stopped=false,Ready=true;struct Receipt* Receipt=nullptr;Array<Truck*>Vehicles;Array<struct Cargo*>Cargo;float Deadline=30000;};
using ERR_ConvoyTask=Convoy*;
struct ConvoyService{Array<Convoy*>tasks;int Count(){return tasks.Count();}Convoy* At(int index){return index>=0&&index<Count()?tasks[index]:nullptr;}};
struct ERR_SupplyConvoyTask:Convoy{static Convoy* Cast(Convoy* task){return task&&task->supply?task:nullptr;}};
struct ERR_ConvoyRules{static bool Captured(Entity* e,Group*){return e->captured;}};
struct ERR_ConvoyResources{static string Magazine(){return "magazine";}};
-struct Route{};struct Leaf{void Stop(){}};struct Mounted{bool busy=false;bool Busy(){return busy;}void Stop(){}};
+struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity x){return x&&x->readyMine;}};
+struct Route{};struct Leaf{bool unresolved=false;void Stop(){}bool Unresolved(){return unresolved;}};struct Mounted{bool busy=false;bool Busy(){return busy;}void Stop(){}};
struct HQ{Mode* m_Mode;Array<ERR_HQUnit>m_Units;Convoy* convoy=nullptr;ConvoyService convoys;ConvoyService* Convoys(){return &convoys;}Route route;bool active=true,routeAvailable=true;int downlinks=0;bool MissionActive(){return active;}Convoy* ConvoyTask(){return convoy;}
Route* Downlink(Group*,string,string,int){downlinks++;return routeAvailable?&route:nullptr;}
'''+reserve+r'''
};
-struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
- Array<IEntity>m_Stock,m_Spent,m_Members,m_Served;Map<int>*m_Consumed=new Map<int>();Map<float>*m_NextAttempt=new Map<float>();int m_ConvoySlot=-1;float m_NextTick=0;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0,m_StockDeadline=-1;bool m_Stopped=false;
+struct Service{HQ* m_HQ;Mode* m_Mode;Convoy* m_Convoy=nullptr;Truck* m_Truck=nullptr;Unit* m_Recipient=nullptr;Unit* m_Donor=nullptr;Leaf* m_Move=nullptr;Leaf*m_MineMove=nullptr;MineBatch*m_MineBatch=nullptr;bool m_MineTrip=false,m_MineStockCaptured=false;int m_MineRecipientSlot=-1;Mounted*m_Mounted=nullptr;Route* m_Order=nullptr;IEntity m_Receiving=nullptr;
+ Array<IEntity>m_Stock,m_MineStock,m_Spent,m_Members,m_Served;Map<int>*m_Consumed=new Map<int>();Map<float>*m_NextAttempt=new Map<float>();int m_ConvoySlot=-1;float m_NextTick=0;int m_State=0,m_Trip=0,m_Token=0,m_ReportId=0,m_TripDelivered=0;float m_Deadline=0,m_StockDeadline=-1;bool m_Stopped=false;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;string reason;bool Current(){return !m_Stopped&&Replication::IsServer()&&m_HQ->active;}void Audit(string why){reason=why;}
'''+methods+r'''
};
@@ -177,6 +179,21 @@
{Fixture f;for(int i=48;i<96;i++)f.inventory.items.Insert(&f.items[i]);f.items[0].prefab.name="NATO_556_STANAG";
f.items[96].magazine=false;f.inventory.items.Insert(&f.items[96]);f.s.Stock(0);
assert(!f.s.m_Stopped&&f.s.m_State==1&&f.s.m_Stock.Count()==96&&f.s.m_Stock.Contains(&f.items[0])&&!f.s.m_Stock.Contains(&f.items[96]));}
+ // Mine demand is a detached known-one report, never a grant to zero or unknown.
+ {Fixture f;MineBatch batch;f.convoy.supply=true;f.convoy.MineStock=&batch;f.items[48].readyMine=true;batch.items={&f.items[48]};f.inventory.items.Insert(&f.items[48]);f.s.Stock(0);
+ for(int n: {0,2,3}){f.ammo.ReadyMines=n;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));}
+ f.ammo.ReadyMines=1;f.ammo.MineComplete=false;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));f.ammo.MineComplete=true;
+ batch.complete=false;assert(!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));batch.complete=true;
+ assert(f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0));assert(f.s.m_MineTrip&&f.s.m_MineStock.Count()==1);}
+ // A submitted unknown mine retains the real HQ claim across a newer delivered
+ // report and expired shared cooldown; a second truck cannot race the same actor.
+ {Fixture first,second;Leaf leaf;leaf.unresolved=true;first.s.Stock(0);assert(first.Plan());first.s.m_MineMove=&leaf;first.s.m_MineTrip=true;first.s.Finish("unknown native move");
+ assert(first.s.m_State==7&&first.recipient.LogisticsReservation==100001&&first.truck.SupplyToken==100001);
+ second.s.Stock(0);second.s.m_HQ=&first.hq;first.hq.convoy=&second.convoy;first.hq.convoys.tasks={&second.convoy};second.s.m_Consumed=first.s.m_Consumed;second.s.m_NextAttempt=first.s.m_NextAttempt;
+ first.Refresh(180001,2);second.donorStatus.ObservedAt=180001;
+ assert(!second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));assert(first.recipient.LogisticsReservation==100001);
+ leaf.unresolved=false;first.s.Finish("settled");assert(first.recipient.LogisticsReservation==0);
+ assert(second.s.TryPlan(&first.recipient,&second.donor,&first.demand,&second.donorStatus,180001));}
// Removed stock is unavailable; a different magazine added after capture does not replace its identity.
{Fixture f;f.s.Stock(0);f.inventory.items={&f.items[48]};assert(!f.Plan());f.Unchanged();f.inventory.items.Insert(&f.items[0]);assert(f.Plan());assert(f.s.m_Stock.Count()==48&&!f.s.m_Stock.Contains(&f.items[48]));}
{Fixture f;f.s.Stock(0);f.s.m_Spent=f.s.m_Stock;assert(!f.Plan());f.Unchanged();}
--- a/tests/test_infantry_supply_pool.py
+++ b/tests/test_infantry_supply_pool.py
@@ -14,7 +14,7 @@
source = source.replace(name + '.', name + '::')
for name in ('m_HQ', 'm_Mode', 'service', 'retained', 'convoys'):
source = source.replace(name + '.', name + '->')
- source = source.replace('int trucks, working, moved, stock;', 'int trucks=0, working=0, moved=0, stock=0;')
+ source = source.replace('int trucks, working, moved, stock, mines;', 'int trucks=0, working=0, moved=0, stock=0, mines=0;')
source = source.replace('string.Format', 'Format')
return source
@@ -35,7 +35,7 @@
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
struct ERR_ForceRules{static bool Finite(float value){return std::isfinite(value);}};
struct Math{static int Min(int a,int b){return a<b?a:b;}};
-string Format(string,int trucks,int working,int moved,int stock){return std::to_string(trucks)+":"+std::to_string(working)+":"+std::to_string(moved)+":"+std::to_string(stock);}
+string Format(string,int trucks,int working,int moved,int stock,int mines){return std::to_string(trucks)+":"+std::to_string(working)+":"+std::to_string(moved)+":"+std::to_string(stock)+":"+std::to_string(mines);}
struct ERR_GameMode{bool active=true;static inline ERR_GameMode* current=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}};
struct ERR_ConvoyTask{};
struct ERR_ConvoyService{Array<ERR_ConvoyTask*>tasks;int reads=0;int Count(){return tasks.Count();}ERR_ConvoyTask* At(int index){reads++;assert(index>=0&&index<tasks.Count());return tasks[index];}};
@@ -48,7 +48,7 @@
ERR_ConvoyTask* Task(){return task;}
void Tick(float){polls++;if(!stopped)ticks++;}
void Stop(){stopCalls++;stopped=true;}
- int State(){return state;}int Delivered(){return moved;}int StockCount(){return stock;}
+ int mines=0;int MinesDelivered(){return mines;}int State(){return state;}int Delivered(){return moved;}int StockCount(){return stock;}
};
struct Pool{
ERR_HQ* m_HQ=nullptr;ERR_GameMode* m_Mode=nullptr;Array<ERR_InfantryResupply*>m_Services;
@@ -78,7 +78,8 @@
// Delivered demand consumption/cooldown state has exact shared identity across trucks.
assert(a->consumed==b->consumed&&a->next==b->next);a->consumed->Set("patrol",17);a->next->Set("patrol",180000);
assert(b->consumed->Get("patrol")==17&&b->next->Get("patrol")==180000);
- a->state=3;a->moved=2;b->moved=4;b->stock=8;assert(f.pool.Status()=="2:1:6:56");
+ a->state=3;a->moved=2;b->moved=4;b->stock=8;assert(f.pool.Status()=="2:1:6:56:0");
+ b->state=7;b->mines=1;assert(f.pool.Status()=="2:2:6:56:1");
f.pool.Stop();assert(a->stopped&&b->stopped&&a->stopCalls==1&&b->stopCalls==1);f.pool.Stop();f.Add(1);f.pool.Tick(3000);
assert(f.pool.Count()==2&&a->ticks==2&&b->ticks==2&&a->stopCalls==1&&b->stopCalls==1);}
// Stopped and detached originals still receive bounded proof polls, never new work.
--- a/tests/test_mounted_supply_integration.py
+++ b/tests/test_mounted_supply_integration.py
@@ -6,7 +6,7 @@
H=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
methods='\n'.join(body(S,x) for x in ('int State()', 'ERR_MountedResupply Mounted()', 'bool Unresolved()', 'protected void PollTransfers(', 'bool TryPlanMounted('))
methods=methods.replace('protected ','').replace('null','nullptr').replace('ERR_MountedResupply Mounted()', 'ERR_MountedResupply* Mounted()')
-for n in ('m_Move','m_Mounted'):methods=methods.replace(n+'.',n+'->')
+for n in ('m_Move','m_MineMove','m_Mounted'):methods=methods.replace(n+'.',n+'->')
for n in ('ERR_HQUnit','ERR_HQWatchEntry'):methods=re.sub(r'\b'+n+r' (\w+)',n+r'* \1',methods)
run(r'''
#include <cassert>
@@ -23,14 +23,15 @@
bool Busy(){return busy;}bool Unresolved(){return unresolved;}void Tick(float){ticks++;}
};
struct Service{HQ*m_HQ=nullptr;Mode*m_Mode=nullptr;Convoy*m_Convoy=nullptr;int m_ConvoySlot=0,m_State=1;
- Stock*m_Stock=nullptr,*m_Spent=nullptr;Consumed*m_Consumed=nullptr;Cooldown*m_NextAttempt=nullptr;Leaf*m_Move=nullptr;ERR_MountedResupply*m_Mounted=nullptr;
- bool current=true,valid=true,arrived=false;bool Current(){return current;}bool TruckValid(bool allowArrived=false){return valid&&(!arrived||allowArrived);}
+ Stock*m_Stock=nullptr,*m_Spent=nullptr;Consumed*m_Consumed=nullptr;Cooldown*m_NextAttempt=nullptr;Leaf*m_Move=nullptr,*m_MineMove=nullptr;ERR_MountedResupply*m_Mounted=nullptr;
+ void Finish(const char*){m_State=1;}bool current=true,valid=true,arrived=false;bool Current(){return current;}bool TruckValid(bool allowArrived=false){return valid&&(!arrived||allowArrived);}
'''+methods+r'''
};
int main(){
HQ h;Mode m;Convoy c;Stock stock,spent;Consumed consumed;Cooldown next;ERR_HQUnit d,r;ERR_HQWatchEntry demand,status;
Service s;s.m_HQ=&h;s.m_Mode=&m;s.m_Convoy=&c;s.m_Stock=&stock;s.m_Spent=&spent;s.m_Consumed=&consumed;s.m_NextAttempt=&next;s.m_ConvoySlot=7;
s.m_State=0;assert(!s.TryPlanMounted(&r,&d,&demand,&status,0)&&!s.Mounted());s.m_State=1;
+ Leaf mine;s.m_MineMove=&mine;assert(!s.TryPlanMounted(&r,&d,&demand,&status,0)&&!s.Mounted()&&s.Unresolved());mine.unresolved=false;s.PollTransfers(0);assert(!s.m_MineMove);
Leaf foot;s.m_Move=&foot;assert(!s.TryPlanMounted(&r,&d,&demand,&status,0)&&!s.Mounted()&&s.Unresolved());
foot.unresolved=false;s.PollTransfers(0);assert(!s.m_Move);s.arrived=true;
assert(s.TryPlanMounted(&r,&d,&demand,&status,0));auto*mounted=s.Mounted();
@@ -0,0 +1,19 @@
check: /tmp/err-policy-9hu0n6u0/check.cpp:393: int main(): Assertion `!f.s.TryPlanMines(&f.recipient,&f.donor,&f.demand,&f.donorStatus,0)' failed.
Traceback (most recent call last):
File "/tmp/erri-mine-resupply-1724/tests/test_infantry_resupply_planning.py", line 32, in <module>
run(r'''
~~~^^^^^
#include <cassert>
^^^^^^^^^^^^^^^^^^
...<188 lines>...
}
^
''')
^^^^
File "/home/jakub/devel/escapistsrereintroduced/tests/policy_harness.py", line 14, in run
subprocess.run([str(p/'check')],check=True)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.14/subprocess.py", line 578, in run
raise CalledProcessError(retcode, process.args,
output=stdout, stderr=stderr)
subprocess.CalledProcessError: Command '['/tmp/err-policy-9hu0n6u0/check']' died with <Signals.SIGABRT: 6>.
@@ -0,0 +1,10 @@
--- a/tests/test_admin_providers.py
+++ b/tests/test_admin_providers.py
@@ -62,7 +62,6 @@
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
HQAliases ha;IntelAliases ia;
assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
- assert(ha.Setting("hq","equipment")=="automaticEquipmentChancePercent");
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
assert(ha.Setting("hq","aa")=="antiAirChancePercent"&&ha.Setting("hq","mines")=="antiPersonnelMineChancePercent");
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
@@ -0,0 +1 @@
PASS: actual HQ rotates finite donors, skips unavailable/contacted trucks, uses delivered reports and dispatches at most one trip per planning cadence
@@ -0,0 +1 @@
PASS: actual bounded empty-return and finite delivery; phase-isolated callbacks, exact ownership proof, stop quarantine and original actor/vehicle boundaries
@@ -0,0 +1,2 @@
PASS: actual one-mine original carrier selection, settled startup, shared finite claims, no refund after submission
PASS: actual HQ startup grant readiness forwarding is authoritative and fail-closed
@@ -0,0 +1,2 @@
infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed
PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership
@@ -0,0 +1 @@
PASS: actual convoy binding, finite identities, disjoint tokens, shared report replay/cooldown, original recipients and HQ cleanup
@@ -0,0 +1 @@
PASS: actual report-derived waypoint insertion/order and tighter positive radii; no formation navigation claim
@@ -0,0 +1 @@
PASS: actual finite convoy supply pool; immutable slots, shared demand state, sparse/capped discovery and permanent shutdown
@@ -0,0 +1 @@
PASS: actual shared mounted stock/history ownership, atomic exact HQ pair claim, and bounded stopped-owner proof polling