--- /dev/null
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQMineSampler.c
@@ -0,0 +1,41 @@
+// Read-only carried PMN4 capability. The caller enumerates its immutable roster;
+// no mine is granted, replenished, armed or interpreted as a rifle magazine.
+class ERR_HQMineSampler
+{
+ static void Inspect(IEntity actor, SCR_AIGroup group, ERR_HQAmmoSnapshot result)
+ {
+  if (!result) return;
+  if (!Replication.IsServer() || !group || group.IsSlave() || group.GetPlayerCount(true) > 0
+   || !ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group)
+  { result.MineComplete = false; return; }
+  if (result.MineMembers >= 32) { result.MineComplete = false; return; }
+  result.MineMembers++;
+  InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
+  if (!inventory || inventory.GetOwner() != actor) { result.MineComplete = false; return; }
+  array<IEntity> items = {}; inventory.GetItems(items);
+  if (items.Count() > 128) { result.MineComplete = false; return; }
+  array<IEntity> seen = {};
+  foreach (IEntity item : items)
+  {
+   if (!item) { result.MineComplete = false; continue; }
+   if (seen.Contains(item)) continue;
+   seen.Insert(item);
+   // An unknown native inventory entry may be a supported mine; never call
+   // that an inspected zero. Known other item prefabs are simply irrelevant.
+   if (!item.GetPrefabData()) { result.MineComplete = false; continue; }
+   if (item.GetPrefabData().GetPrefabName() != ERR_EnemyLoadoutCatalog.MINE) continue;
+   InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
+   if (!carried || !carried.GetParentSlot() || carried.GetParentSlot().GetAttachedEntity() != item
+    || !ERR_InfantryAmmoTransfer.StoredUnder(item, actor))
+   { result.MineComplete = false; continue; }
+   if (!item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)) { result.MineComplete = false; continue; }
+   SCR_PressureTriggerComponent trigger = SCR_PressureTriggerComponent.Cast(item.FindComponent(SCR_PressureTriggerComponent));
+   if (!trigger) { result.MineComplete = false; continue; }
+   DamageManagerComponent damage = DamageManagerComponent.Cast(item.FindComponent(DamageManagerComponent));
+   if (!damage) { result.MineComplete = false; continue; }
+   // Known damaged or armed mines are not available carried stock.
+   if (damage.IsDestroyed() || trigger.IsActivated() || trigger.IsActivatedByDefault()) continue;
+   result.ReadyMines++;
+  }
+ }
+}
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c
@@ -4,6 +4,9 @@
 {
  bool Complete;
  bool LauncherComplete;
+ bool MineComplete;
+ int MineMembers;
+ int ReadyMines;
  int Members;
  int SmallArmsMuzzles;
  int SmallArmsRounds;
@@ -24,10 +27,13 @@
   copy.SmallArmsMagazines = SmallArmsMagazines; copy.SmallArmsChambers = SmallArmsChambers;
   copy.LauncherRounds = LauncherRounds; copy.LauncherMagazines = LauncherMagazines;
   copy.LauncherChambers = LauncherChambers;
+  copy.MineComplete = MineComplete; copy.MineMembers = MineMembers; copy.ReadyMines = ReadyMines;
   return copy;
  }
  bool Valid(int living)
  {
+  if (MineMembers < 0 || MineMembers > living || living > 32 || (MineComplete && MineMembers != living)) return false;
+  if (ReadyMines < 0 || ReadyMines > MineMembers * 128) return false;
   if (Members < 0 || Members > living || living > 32 || (Complete && Members != living)) return false;
   if (SmallArmsMuzzles < 0 || SmallArmsMuzzles > 1024) return false;
   if (SmallArmsRounds < 0 || SmallArmsRounds > 4096000) return false;
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQStatusSampler.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQStatusSampler.c
@@ -227,6 +227,7 @@
   ERR_HQStatusReport report = new ERR_HQStatusReport();
   report.AmmoState = new ERR_HQAmmoSnapshot();
   report.AmmoState.Complete = true; report.AmmoState.LauncherComplete = true;
+  report.AmmoState.MineComplete = true;
   report.SourceId = m_SourceId; report.MessageId = messageId; report.Position = position;
   report.Strength = m_Strength; report.ConfirmedLosses = m_Deaths; report.ObservedAt = now;
   for (int slot = 0; slot < m_Dead.Count(); slot++)
@@ -237,8 +238,10 @@
    IEntity actor = m_Members[i];
    if (!ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Group
     || SCR_FactionManager.SGetFaction(actor) != m_Faction)
-   { report.AmmoState.Complete = false; report.AmmoState.LauncherComplete = false; continue; }
+   { report.AmmoState.Complete = false; report.AmmoState.LauncherComplete = false;
+    report.AmmoState.MineComplete = false; continue; }
    report.Count++;
+   ERR_HQMineSampler.Inspect(actor, m_Group, report.AmmoState);
    ERR_HQAmmoSampler.Inspect(actor, m_Group, report.AmmoState);
    if (ERR_Actors.CanFight(actor)) report.Fit++;
   }
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
@@ -725,6 +725,10 @@
   string ammunition = "unknown";
   if (report.AmmoState && report.AmmoState.Complete) ammunition = string.Format("%1 magazine rounds, %2 chambered", report.AmmoState.SmallArmsRounds, report.AmmoState.SmallArmsChambers);
   route.Payload += "; small-arms ammunition=" + ammunition;
+  string mines = "unknown/incomplete";
+  if (report.AmmoState && report.AmmoState.MineComplete && report.AmmoState.MineMembers == report.Count)
+   mines = string.Format("%1 available unarmed PMN4", report.AmmoState.ReadyMines);
+  route.Payload += "; carried mines=" + mines;
   if (report.MountedAmmo)
   {
    string mounted = "unknown/incomplete";
--- /dev/null
+++ b/tests/test_hq_mine_sampler.py
@@ -0,0 +1,87 @@
+"""Actual bounded carried PMN4 sampler and detached counts; no native qualification."""
+from pathlib import Path
+import os,re
+from policy_harness import ROOT,body,run
+BASE=Path(os.environ.get('ERR_MINE_STATUS_ROOT',ROOT))
+P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMineSampler.c'
+A=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
+s=P.read_text()
+for forbidden in ('TrySpawn','TryMove','SetOrigin','GetOrigin','ActivateTrigger','GetAgents','GetWorld','SetAmmoCount'):
+ assert forbidden not in s,forbidden
+snapshot=A.read_text().split('class ERR_HQAmmoSampler')[0]
+owned=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryAmmoTransfer.c',' static bool StoredUnder(')
+def cpp(s):
+ s=re.sub(r'//[^\n]*','',s).replace('class ','struct ').replace('protected ','').replace('foreach (','for (').replace('array<','Array<').replace('null','nullptr')
+ s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory')
+ for entity in ('item','entity'):
+  s=s.replace(f'InventoryItemComponent.Cast({entity}.FindComponent(InventoryItemComponent))',f'{entity}->carried')
+ s=s.replace('SCR_PressureTriggerComponent.Cast(item.FindComponent(SCR_PressureTriggerComponent))','item->trigger')
+ s=s.replace('DamageManagerComponent.Cast(item.FindComponent(DamageManagerComponent))','item->hasDamage ? &item->damage : nullptr')
+ s=s.replace('item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)','item->collision')
+ for name in ('Replication','ERR_Actors','ERR_EnemyLoadoutCatalog','ERR_InfantryAmmoTransfer'):
+  s=s.replace(name+'.',name+'::')
+ s=re.sub(r'\bERR_HQAmmoSnapshot (?=\w)','ERR_HQAmmoSnapshot* ',s)
+ for name in ('actor','group','result','inventory','item','entity','carried','trigger','copy','storage','damage'):
+  s=re.sub(r'\b'+name+r'\.',name+'->',s)
+ s=s.replace('GetPrefabData().','GetPrefabData()->').replace('GetParentSlot().','GetParentSlot()->')
+ s=re.sub(r'\b(bool|int) (\w+);',r'\1 \2=0;',s)
+ return s.replace('\n}\n','\n};\n')
+run(r'''
+#include <cassert>
+#include <vector>
+#include <string>
+#include <algorithm>
+using string=std::string;
+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();}};
+struct Entity;struct Group;struct Inventory;using IEntity=Entity*;using SCR_AIGroup=Group*;
+struct Prefab{string name="PMN4";string GetPrefabName(){return name;}};
+struct Trigger{bool active=false,byDefault=false;bool IsActivated(){return active;}bool IsActivatedByDefault(){return byDefault;}};using SCR_PressureTriggerComponent=Trigger*;
+struct Storage{Entity*owner=nullptr;Entity*GetOwner(){return owner;}};using BaseInventoryStorageComponent=Storage*;
+struct Slot{Entity*attached=nullptr;Storage*storage=nullptr;Entity*GetAttachedEntity(){return attached;}Storage*GetStorage(){return storage;}};
+struct Carried{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};using InventoryItemComponent=Carried*;
+struct Damage{bool dead=false;bool IsDestroyed(){return dead;}};using DamageManagerComponent=Damage*;
+struct Entity{Group*group=nullptr;Inventory*inventory=nullptr;Prefab*prefab=nullptr;Carried*carried=nullptr;Trigger*trigger=nullptr;Entity*parent=nullptr;
+ Damage damage;bool hasDamage=true;bool alive=true,player=false,collision=true;Prefab*GetPrefabData(){return prefab;}Entity*GetParent(){return parent;}};
+struct Group{bool slave=false;int players=0;bool IsSlave(){return slave;}int GetPlayerCount(bool){return players;}};
+struct Inventory{Entity*owner=nullptr;Array<IEntity>items;int reads=0;IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a){reads++;a=items;}};using InventoryStorageManagerComponent=Inventory*;
+struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
+struct ERR_EnemyLoadoutCatalog{inline static string MINE="PMN4";};
+struct ERR_Actors{static bool Alive(IEntity e){return e&&e->alive;}static bool Player(IEntity e){return e&&e->player;}static struct Group*Group(IEntity e){return e?e->group:nullptr;}};
+struct ERR_InfantryAmmoTransfer{
+'''+cpp(owned)+r'''
+};
+'''+cpp(snapshot)+cpp(s)+r'''
+struct Fixture{Entity actor,mine,other,bag;Group group;Inventory inventory;Prefab prefab,otherPrefab;Carried carried,bagCarried;Slot slot,bagSlot;Storage storage,bagStorage;Trigger trigger;
+ Fixture(){actor.group=&group;actor.inventory=&inventory;inventory.owner=&actor;inventory.items={&mine};mine.prefab=&prefab;mine.carried=&carried;mine.trigger=&trigger;carried.slot=&slot;slot.attached=&mine;slot.storage=&storage;storage.owner=&actor;otherPrefab.name="rifle";other.prefab=&otherPrefab;}
+ ERR_HQAmmoSnapshot Sample(){ERR_HQAmmoSnapshot s;s.MineComplete=true;s.Complete=true;s.LauncherComplete=true;s.Members=1;ERR_HQMineSampler::Inspect(&actor,&group,&s);return s;}
+};
+int main(){
+ {Fixture f;f.inventory.items={&f.mine,&f.other,&f.mine};auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==1&&s.MineMembers==1&&s.Valid(1));
+ auto frozen=s.Snapshot();f.inventory.items.clear();s.ReadyMines=0;assert(frozen->ReadyMines==1&&frozen->MineMembers==1&&frozen->MineComplete);}
+ {Fixture f;f.inventory.items.clear();auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==0&&s.Valid(1));}
+ // Existing nested carried storage is accepted; no weapon manager is required.
+ {Fixture f;f.storage.owner=&f.bag;f.bag.carried=&f.bagCarried;f.bagCarried.slot=&f.bagSlot;f.bagSlot.attached=&f.bag;f.bagSlot.storage=&f.bagStorage;f.bagStorage.owner=&f.actor;
+ auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==1);}
+ // Known armed/default-armed/damaged items are not available unarmed mines.
+ for(int k=0;k<3;k++){Fixture f;if(k==0)f.trigger.active=true;if(k==1)f.trigger.byDefault=true;if(k==2)f.mine.damage.dead=true;
+ auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==0&&s.Complete&&s.LauncherComplete);}
+ // Unsupported/broken inventory or PMN4 custody never means known empty.
+ for(int k=0;k<13;k++){Fixture f;switch(k){case 0:f.actor.inventory=nullptr;break;case 1:f.inventory.owner=&f.other;break;
+ case 2:f.inventory.items.resize(129,&f.mine);break;case 3:f.inventory.items={nullptr};break;case 4:f.mine.prefab=nullptr;break;
+ case 5:f.mine.carried=nullptr;break;case 6:f.carried.slot=nullptr;break;case 7:f.slot.attached=&f.other;break;
+ case 8:f.storage.owner=&f.other;break;case 9:f.mine.collision=false;break;case 10:f.mine.trigger=nullptr;break;case 11:f.slot.storage=nullptr;break;case 12:f.mine.hasDamage=false;break;}
+ auto s=f.Sample();assert(!s.MineComplete&&s.ReadyMines==0&&s.Complete&&s.LauncherComplete&&s.Valid(1));}
+ // Cyclic ownership is bounded rather than accepted as carried stock.
+ {Fixture f;f.storage.owner=&f.mine;auto s=f.Sample();assert(!s.MineComplete&&s.ReadyMines==0);}
+ // Foreign/dead/player/oversized roster rejection does not query inventory.
+ for(int k=0;k<6;k++){Fixture f;Group other;if(k==0)f.actor.group=&other;if(k==1)f.actor.alive=false;if(k==2)f.actor.player=true;
+ if(k==3){f.group.players=1;}if(k==4){f.group.slave=true;}if(k==5){Replication::server=false;}
+ auto s=f.Sample();assert(!s.MineComplete&&s.MineMembers==0&&f.inventory.reads==0);Replication::server=true;}
+ {Fixture f;ERR_HQAmmoSnapshot s;s.MineComplete=true;s.MineMembers=32;ERR_HQMineSampler::Inspect(&f.actor,&f.group,&s);assert(!s.MineComplete&&s.MineMembers==32&&f.inventory.reads==0);}
+ {Fixture f;f.inventory.items.resize(128,&f.mine);auto s=f.Sample();assert(s.MineComplete&&s.ReadyMines==1);}
+ // Impossible wire counts and incomplete coverage are rejected independently.
+ {ERR_HQAmmoSnapshot s;assert(s.Valid(0)&&!s.MineComplete);s.ReadyMines=1;assert(!s.Valid(0));s.ReadyMines=0;s.MineMembers=-1;assert(!s.Valid(1));
+ s.MineMembers=1;s.MineComplete=true;assert(s.Valid(1)&&!s.Valid(2));s.ReadyMines=129;assert(!s.Valid(1));s.ReadyMines=-1;assert(!s.Valid(1));}
+}
+''')
+print('PASS: actual carried PMN4 inventory, original-owner guards, bounded deduplication, independent unknown state and detached counts')
--- a/tests/test_hq_status_sampler.py
+++ b/tests/test_hq_status_sampler.py
@@ -10,7 +10,7 @@
  for old,new in [('protected ',''),('null','nullptr'),('Replication.','Replication::'),
   ('ERR_Actors.','ERR_Actors::'),('ERR_HQPursuitRules.','ERR_HQPursuitRules::'),
   ('SCR_FactionManager.','SCR_FactionManager::'),('ERR_HQRadio.','ERR_HQRadio::'),('Math.','Math::'),
-  ('ERR_HQAmmoSampler.','ERR_HQAmmoSampler::'),('ERR_HQMountedAmmoSampler.','ERR_HQMountedAmmoSampler::'),
+  ('ERR_HQMineSampler.','ERR_HQMineSampler::'),('ERR_HQAmmoSampler.','ERR_HQAmmoSampler::'),('ERR_HQMountedAmmoSampler.','ERR_HQMountedAmmoSampler::'),
   ('ERR_GameMode.','ERR_GameMode::'),('ERR_ForceKind.','ERR_ForceKind::'),('ERR_ConvoyRules.','ERR_ConvoyRules::'),('Vehicle.','Vehicle::'),('CompartmentAccessComponent.','CompartmentAccessComponent::'),('sourceId.IsEmpty()','sourceId.empty()'),('sourceId.Length()','sourceId.size()')]:s=s.replace(old,new)
  for typ in ('IEntity','AIAgent','SCR_AIGroup','Faction','ERR_HQStatusReport','DamageManagerComponent','BaseCompartmentSlot','ERR_GameMode','ERR_ForceRecord'):
   s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
@@ -75,9 +75,10 @@
 };
 struct SCR_FactionManager{static Faction* SGetFaction(IEntity* a){return a?a->faction:nullptr;}};
 struct ERR_HQRadio{static int Capability(IEntity* a){return ERR_Actors::CanFight(a)&&a->radio?1:0;}};
-struct ERR_HQAmmoSnapshot{bool Complete=false,LauncherComplete=false;int Members=0;};
+struct ERR_HQAmmoSnapshot{bool Complete=false,LauncherComplete=false,MineComplete=false;int Members=0,MineMembers=0,ReadyMines=0;};
 struct ERR_HQAmmoSampler{inline static Array<IEntity*> sampled;
  static void Inspect(IEntity* actor,SCR_AIGroup* group,ERR_HQAmmoSnapshot* state){assert(actor&&actor->group==group);sampled.Insert(actor);state->Members++;}};
+struct ERR_HQMineSampler{static void Inspect(IEntity*,SCR_AIGroup*,ERR_HQAmmoSnapshot* s){s->MineMembers++;}};
 struct ERR_HQMountedAmmoSnapshot{bool Complete=false;int Rounds=0;};
 struct ERR_HQMountedAmmoSampler{inline static int calls=0;inline static IEntity* sampled=nullptr;
  static ERR_HQMountedAmmoSnapshot* Inspect(IEntity* root){calls++;sampled=root;return new ERR_HQMountedAmmoSnapshot{true,root->mountedRounds};}};
@@ -304,7 +305,7 @@
  std::reverse(group.agents.begin(),group.agents.end());
  assert(sample.InitialMember(1)==&members[1]&&sample.CanAttribute(1,&members[1]));
  assert(!sample.CanAttribute(1,&members[2]));std::reverse(group.agents.begin(),group.agents.end());
- auto first=sample.Sample(1000,1,&members[0]);assert(first&&first->Count==4&&first->Fit==4&&first->Strength==4);
+ auto first=sample.Sample(1000,1,&members[0]);assert(first&&first->Count==4&&first->Fit==4&&first->Strength==4);assert(first->AmmoState->MineComplete&&first->AmmoState->MineMembers==4);
  assert(first->Position.x==100&&first->SourceId=="site");
  assert(first->AmmoState&&first->AmmoState->Complete&&first->AmmoState->Members==4);
  assert(ERR_HQAmmoSampler::sampled.Count()==4);
