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:
@@ -0,0 +1,356 @@
|
||||
ERR_ScenarioDefinition {
|
||||
MinimumRadioSources 2
|
||||
ScoutCount 10
|
||||
Sites {
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-prison-1"
|
||||
RandomClear 1
|
||||
Label "Prison"
|
||||
Kind 0
|
||||
Prefab "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
|
||||
Position 6090 0 7160
|
||||
Guarded 1
|
||||
GuardOffset 35 0 0
|
||||
Container "{D93480F29E73C1AB}Prefabs/ERR/PrisonBackpack.et"
|
||||
ContainerOffset -4 0 0
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{C0F7DD85A86B2900}Prefabs/Weapons/Handguns/PM/Handgun_PM.et" Count 5 }
|
||||
ERR_SiteStock { Prefab "{8B853CDD11BA916E}Prefabs/Weapons/Magazines/Magazine_9x18_PM_8rnd_Ball.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-supply-1"
|
||||
AllowFactionPool 1
|
||||
Label "Southern supply cache"
|
||||
Kind 4
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 6150 0 7050
|
||||
Guarded 1
|
||||
GuardOffset 18 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 0 0 0
|
||||
Transport ERR_SiteTransport {
|
||||
Optional 1
|
||||
Prefab "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
|
||||
Offset 0 0 -12
|
||||
Heading 180
|
||||
ClearanceRadius 4
|
||||
MaximumTerrainRange 1
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
ERR_SiteStock { Prefab "{C09C7C97EFA34909}Prefabs/ERR/Items/EmergencyRepairKit.et" Count 1 }
|
||||
ERR_SiteStock { Prefab "{12D5AD21E383B768}Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.et" Count 2 }
|
||||
}
|
||||
RevealOnEscape 1
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-radio-1"
|
||||
RandomClear 1
|
||||
RootHeight 0.8
|
||||
Label "Radio station"
|
||||
DiscoverRadius 8
|
||||
Kind 1
|
||||
Prefab "{47D725F56A904ACE}Prefabs/ERR/RadioCamp.et"
|
||||
Position 6500 0 7200
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 0 }
|
||||
ERR_SiteProp { Prefab "{B3949F6D215A9F78}Prefabs/Props/Military/Antennas/Antenna_FIA_02/Antenna_FIA_02_base.et" Offset -7 0 2 }
|
||||
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -1.5 0 0 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -4.5 0 -6 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 4.5 0 -6 }
|
||||
}
|
||||
Guarded 1
|
||||
GuardOffset 30 0 0
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-extraction-1"
|
||||
Label "Initial helicopter pickup site"
|
||||
Kind 2
|
||||
Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
Position 4800.953 0 11487.412
|
||||
AircraftOffset 0 0 0
|
||||
RequireAirfield 1
|
||||
Guarded 1
|
||||
GuardOffset 30 0 0
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-medical-1"
|
||||
Label "Field hospital 1"
|
||||
Kind 6
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 6340 0 7400
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 6 0 -3
|
||||
DiscoverRadius 60
|
||||
RevealOnEscape 1
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{F8427C3EFC52644E}Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et" Offset 0 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 6 0 0 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -6 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 6 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-outpost-1"
|
||||
Label "Field camp 1"
|
||||
Kind 3
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 6740 0 7300
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 0 0 -4
|
||||
DiscoverRadius 60
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-mortar-1"
|
||||
Label "Mortar site 1"
|
||||
Kind 5
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 6000 0 7000
|
||||
RandomClear 1
|
||||
DiscoverRadius 35
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-mortar-2"
|
||||
Label "Mortar site 2"
|
||||
Kind 5
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 4500 0 6500
|
||||
RandomClear 1
|
||||
DiscoverRadius 35
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-mortar-3"
|
||||
Label "Mortar site 3"
|
||||
Kind 5
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 7000 0 9500
|
||||
RandomClear 1
|
||||
DiscoverRadius 35
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-mortar-4"
|
||||
Label "Mortar site 4"
|
||||
Kind 5
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 4000 0 10500
|
||||
RandomClear 1
|
||||
DiscoverRadius 35
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-medical-2"
|
||||
Label "Field hospital 2"
|
||||
Kind 6
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 3000 0 6000
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 6 0 -3
|
||||
DiscoverRadius 60
|
||||
RevealOnEscape 0
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{F8427C3EFC52644E}Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et" Offset 0 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 6 0 0 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -6 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 6 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-medical-3"
|
||||
Label "Field hospital 3"
|
||||
Kind 6
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 8000 0 9500
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 6 0 -3
|
||||
DiscoverRadius 60
|
||||
RevealOnEscape 0
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{F8427C3EFC52644E}Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et" Offset 0 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 6 0 0 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -6 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 6 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-camp-2"
|
||||
Label "Field camp 2"
|
||||
Kind 3
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 2500 0 4500
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 0 0 -4
|
||||
DiscoverRadius 60
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-camp-3"
|
||||
Label "Field camp 3"
|
||||
Kind 3
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 5500 0 3500
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 0 0 -4
|
||||
DiscoverRadius 60
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-camp-4"
|
||||
Label "Field camp 4"
|
||||
Kind 3
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 7800 0 6000
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 0 0 -4
|
||||
DiscoverRadius 60
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-camp-5"
|
||||
Label "Field camp 5"
|
||||
Kind 3
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 5200 0 10000
|
||||
RandomClear 1
|
||||
Guarded 1
|
||||
GuardCount 4
|
||||
ResponseCount 2
|
||||
GuardOffset 12 0 0
|
||||
ResponseOffset -12 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
ContainerOffset 0 0 -4
|
||||
DiscoverRadius 60
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||
}
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+783
@@ -0,0 +1,783 @@
|
||||
enum ERR_PrepareResult { FAILED = -1, PENDING, READY }
|
||||
|
||||
class ERR_StockPlacement
|
||||
{
|
||||
IEntity Item;
|
||||
InventoryStorageManagerComponent Inventory;
|
||||
bool Accepted;
|
||||
}
|
||||
|
||||
class ERR_SiteRuntime
|
||||
{
|
||||
ref ERR_SiteDefinition Definition;
|
||||
ERR_LocationComponent Location;
|
||||
IEntity Container;
|
||||
ref array<IEntity> DepotMagazines = {}; // Exact authored startup identities; never refreshed from live inventory.
|
||||
ref array<IEntity> DepotMines = {}; // At most two authored inert PMN4 per camp, never replenished.
|
||||
SCR_AIGroup Group;
|
||||
ref ERR_SiteRuntime Responders;
|
||||
string ExpectedGuardFaction;
|
||||
int InitialGuardCount;
|
||||
ref ERR_PrisonPatrol PrisonPatrol;
|
||||
int LastInitialPhysical = -1;
|
||||
int LastInitialConscious = -1;
|
||||
Vehicle Transport;
|
||||
IEntity TransportEntity;
|
||||
bool TransportAttempted;
|
||||
bool TransportOmitted;
|
||||
float TransportDeadline = -1;
|
||||
AIWaypointCycle PatrolCycle;
|
||||
ref array<AIWaypoint> PatrolWaypoints = {};
|
||||
ref array<IEntity> Props = {};
|
||||
}
|
||||
|
||||
// One owner for finite physical site contents. No population or stock refill tick.
|
||||
class ERR_SiteService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected int m_ContentGuardIndex;
|
||||
protected ref ERR_ExtractionPlacement m_ExtractionPlacement;
|
||||
ERR_ExtractionPlacement ExtractionPlacement() { return m_ExtractionPlacement; }
|
||||
protected ref array<ref ERR_SiteRuntime> m_Sites = {};
|
||||
protected ref array<IEntity> m_Entities = {};
|
||||
protected ref array<ref ERR_StockPlacement> m_Placements = {};
|
||||
protected ERR_PrepareResult m_State = ERR_PrepareResult.PENDING;
|
||||
protected float m_Deadline;
|
||||
protected float m_GuardDeadline;
|
||||
protected bool m_Begun;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Activated;
|
||||
protected bool m_Obstructed;
|
||||
protected string m_FailureReason;
|
||||
protected float m_NextDiscovery;
|
||||
string FailureReason() { return m_FailureReason; }
|
||||
|
||||
void ERR_SiteService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
int Count() { return m_Sites.Count(); }
|
||||
ERR_SiteRuntime At(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Sites.Count()) return null;
|
||||
return m_Sites[index];
|
||||
}
|
||||
// Optional ground loading consumes only retained original camp items.
|
||||
int DepotMagazineCount(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || m_State != ERR_PrepareResult.READY || !site || !m_Sites.Contains(site)) return 0;
|
||||
if (!site.Definition || site.Definition.Kind != ERR_LocationKind.OUTPOST || site.Definition.RevealOnEscape) return 0;
|
||||
if (!site.Container || !Owns(site.Container) || !site.Responders || !site.Responders.Group) return 0;
|
||||
if (site.DepotMagazines.Count() > 96) return 0;
|
||||
return site.DepotMagazines.Count();
|
||||
}
|
||||
int DepotMineCount(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || m_State != ERR_PrepareResult.READY || !site || !m_Sites.Contains(site)) return 0;
|
||||
if (!site.Definition || site.Definition.Kind != ERR_LocationKind.OUTPOST || site.Definition.RevealOnEscape) return 0;
|
||||
if (!site.Container || !Owns(site.Container) || !site.Responders || !site.Responders.Group) return 0;
|
||||
if (site.DepotMines.Count() > 2) return 0;
|
||||
return site.DepotMines.Count();
|
||||
}
|
||||
int DepotItemCount(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!site || site.DepotMagazines.Count() > 96 || site.DepotMines.Count() > 2) return 0;
|
||||
int count = DepotMagazineCount(site) + DepotMineCount(site);
|
||||
if (count > 96) return 0;
|
||||
return count;
|
||||
}
|
||||
IEntity DepotItem(ERR_SiteRuntime site, int index)
|
||||
{
|
||||
if (index < 0 || index >= DepotItemCount(site)) return null;
|
||||
int magazines = DepotMagazineCount(site);
|
||||
if (index < magazines) return site.DepotMagazines[index];
|
||||
return site.DepotMines[index - magazines];
|
||||
}
|
||||
void DepotStock(ERR_SiteRuntime site, out array<IEntity> items)
|
||||
{
|
||||
items = {};
|
||||
int count = DepotItemCount(site);
|
||||
for (int i = 0; i < count; i++) items.Insert(DepotItem(site, i));
|
||||
}
|
||||
IEntity DepotMagazine(ERR_SiteRuntime site, int index)
|
||||
{
|
||||
if (index < 0 || index >= DepotMagazineCount(site)) return null;
|
||||
return site.DepotMagazines[index];
|
||||
}
|
||||
bool DepotOriginal(ERR_SiteRuntime site, IEntity item)
|
||||
{
|
||||
return item && DepotItemCount(site) > 0 && (site.DepotMagazines.Contains(item) || site.DepotMines.Contains(item));
|
||||
}
|
||||
bool Owns(IEntity entity) { return entity && m_Entities.Contains(entity); }
|
||||
// Snapshot only nearby mission-owned objects. Distant sites must not block
|
||||
// arrival, and vanilla map streaming remains the native preload's job.
|
||||
bool SpawnScene(vector position, out array<IEntity> result)
|
||||
{
|
||||
result = {};
|
||||
array<IEntity> pending = {};
|
||||
foreach (IEntity root : m_Entities)
|
||||
if (root && vector.DistanceXZ(root.GetOrigin(), position) <= 120) pending.Insert(root);
|
||||
int visited;
|
||||
while (!pending.IsEmpty())
|
||||
{
|
||||
if (++visited > 512) return false;
|
||||
IEntity entity = pending[0]; pending.Remove(0);
|
||||
if (!entity) continue;
|
||||
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
if (component && !result.Contains(entity)) result.Insert(entity);
|
||||
if (result.Count() > 256) return false;
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child) { pending.Insert(child); child = child.GetSibling(); }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
int TransportCount()
|
||||
{
|
||||
int count;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
if (site.Transport) count++;
|
||||
return count;
|
||||
}
|
||||
// Own physical group identity only; command providers do not inspect players.
|
||||
ERR_SiteRuntime FindGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!group) return null;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
if (site.Group == group || (site.Responders && site.Responders.Group == group)) return site;
|
||||
return null;
|
||||
}
|
||||
ERR_SiteRuntime Find(string id)
|
||||
{
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
if (site.Definition.Id == id) return site;
|
||||
return null;
|
||||
}
|
||||
protected bool CheckClearance(IEntity entity)
|
||||
{
|
||||
if (entity && entity.GetVObject()) m_Obstructed = true;
|
||||
return !m_Obstructed;
|
||||
}
|
||||
protected bool CacheClearance(vector position)
|
||||
{
|
||||
m_Obstructed = false;
|
||||
vector ground = ERR_Actors.Ground(position);
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(ground + "-1.3 0.1 -1.3", ground + "1.3 2 1.3", CheckClearance);
|
||||
PrintFormat("Reintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
|
||||
return !m_Obstructed;
|
||||
}
|
||||
protected bool GeneratedCacheClearance(vector position)
|
||||
{
|
||||
// Match the physical terrain survey. An entity's broad bounding box may
|
||||
// overlap an empty volume near a tree or ruin without blocking the cache.
|
||||
vector ground = ERR_Actors.Ground(position);
|
||||
TraceBox trace = new TraceBox();
|
||||
trace.Start = ground + "0 0.12 0"; trace.End = ground + "0 2 0";
|
||||
trace.Mins = "-1.3 0 -1.3"; trace.Maxs = "1.3 0.1 1.3";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
return GetGame().GetWorld().TraceMove(trace, null) >= 1;
|
||||
}
|
||||
protected bool BackpackClearance(vector position)
|
||||
{
|
||||
vector floor = ERR_Actors.Ground(position);
|
||||
TraceBox trace = new TraceBox();
|
||||
trace.Start = floor + "0 0.15 0"; trace.End = floor + "0 1.15 0";
|
||||
trace.Mins = "-0.5 0 -0.5"; trace.Maxs = "0.5 0.1 0.5";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
return GetGame().GetWorld().TraceMove(trace, null) >= 1;
|
||||
}
|
||||
protected bool CreateGuardOrders(ERR_SiteRuntime site, vector anchor)
|
||||
{
|
||||
if (site.Definition.Kind == ERR_LocationKind.RADIO) anchor = ERR_Actors.Ground(site.Location.Position());
|
||||
if (!site.Definition.Patrol || site.Definition.Patrol.IsEmpty())
|
||||
{
|
||||
AIWaypoint defend = AIWaypoint.Cast(Spawn(ERR_Resources.DefendWaypoint(), anchor));
|
||||
if (!defend) return false;
|
||||
if (site.Definition.Kind == ERR_LocationKind.RADIO) defend.SetCompletionRadius(20);
|
||||
site.Group.AddWaypoint(defend);
|
||||
return true;
|
||||
}
|
||||
foreach (ERR_SitePatrolPoint point : site.Definition.Patrol)
|
||||
{
|
||||
AIWaypoint move = AIWaypoint.Cast(Spawn(ERR_Resources.MoveWaypoint(), anchor + point.Offset));
|
||||
if (!move) return false;
|
||||
site.PatrolWaypoints.Insert(move);
|
||||
}
|
||||
site.PatrolCycle = AIWaypointCycle.Cast(Spawn(ERR_Resources.CycleWaypoint(), anchor));
|
||||
if (!site.PatrolCycle) return false;
|
||||
site.PatrolCycle.SetRerunCounter(-1);
|
||||
site.PatrolCycle.SetWaypoints(site.PatrolWaypoints);
|
||||
// The native cycle inserts its own children and itself. Adding children here
|
||||
// would duplicate the route. HQ inserts Search at index 0 above this queue.
|
||||
site.Group.AddWaypoint(site.PatrolCycle);
|
||||
return true;
|
||||
}
|
||||
protected void StopPatrols()
|
||||
{
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
{
|
||||
array<AIWaypoint> empty = {};
|
||||
if (site.PatrolCycle) site.PatrolCycle.SetWaypoints(empty);
|
||||
if (site.Group)
|
||||
{
|
||||
array<AIWaypoint> queued = {};
|
||||
site.Group.GetWaypoints(queued);
|
||||
foreach (AIWaypoint waypoint : queued)
|
||||
if (waypoint && (waypoint == site.PatrolCycle || site.PatrolWaypoints.Contains(waypoint))) site.Group.RemoveWaypoint(waypoint);
|
||||
}
|
||||
foreach (AIWaypoint move : site.PatrolWaypoints) ERR_Actors.DeleteSpawned(move);
|
||||
ERR_Actors.DeleteSpawned(site.PatrolCycle);
|
||||
site.PatrolWaypoints.Clear();
|
||||
site.PatrolCycle = null;
|
||||
}
|
||||
}
|
||||
protected IEntity Spawn(ResourceName resource, vector position)
|
||||
{
|
||||
IEntity entity = ERR_Actors.Spawn(resource, ERR_Actors.Ground(position));
|
||||
if (entity) m_Entities.Insert(entity);
|
||||
return entity;
|
||||
}
|
||||
// Read-only placement survey. It neither removes scenery nor searches for a
|
||||
// replacement position; authors must correct a rejected parking location.
|
||||
bool TransportClearance(vector position, float radius, float maximumRange)
|
||||
{
|
||||
if (radius < 2 || radius > 8 || maximumRange < 0 || maximumRange > 1.5) return false;
|
||||
vector ground = ERR_Actors.Ground(position);
|
||||
float lowest = ground[1];
|
||||
float highest = ground[1];
|
||||
for (int x = -1; x <= 1; x++)
|
||||
{
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
vector sample = ERR_Actors.Ground(position + Vector(x * radius, 0, z * radius));
|
||||
lowest = Math.Min(lowest, sample[1]);
|
||||
highest = Math.Max(highest, sample[1]);
|
||||
}
|
||||
}
|
||||
m_Obstructed = false;
|
||||
vector low = Vector(position[0] - radius, lowest + 0.1, position[2] - radius);
|
||||
vector high = Vector(position[0] + radius, highest + 3, position[2] + radius);
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(low, high, CheckClearance);
|
||||
bool clear = lowest >= 1 && highest - lowest <= maximumRange && !m_Obstructed;
|
||||
PrintFormat("Reintroduced [sites]: transport parking=%1 radius=%2 terrainRange=%3 obstructed=%4 clear=%5", ERR_Actors.Ground(position), radius, highest - lowest, m_Obstructed, clear);
|
||||
return clear;
|
||||
}
|
||||
// Native recursive deletion must never consume an occupant or character that
|
||||
// acquired this candidate. Unknown/oversized hierarchies are relinquished.
|
||||
protected bool TransportEmpty(IEntity exact)
|
||||
{
|
||||
array<IEntity> pending = {exact};
|
||||
int visited;
|
||||
while (!pending.IsEmpty())
|
||||
{
|
||||
if (++visited > 128) return false;
|
||||
IEntity entity = pending[0]; pending.Remove(0);
|
||||
if (!entity) continue;
|
||||
if (ERR_Actors.Player(entity) || ChimeraCharacter.Cast(entity) || SCR_AIGroup.Cast(entity)) return false;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(entity.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (Vehicle.Cast(entity) && !manager) return false;
|
||||
if (manager)
|
||||
{
|
||||
array<BaseCompartmentSlot> seats = {}; manager.GetCompartments(seats);
|
||||
if (seats.Count() > 64) return false;
|
||||
foreach (BaseCompartmentSlot seat : seats) if (!seat || seat.GetOccupant()) return false;
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (pending.Count() >= 128) return false;
|
||||
pending.Insert(child); child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Only the transport identity recorded by CreateTransport may be omitted.
|
||||
// Required transports preserve the existing whole-preparation failure policy.
|
||||
protected bool TransportFailure(ERR_SiteRuntime site, string reason)
|
||||
{
|
||||
if (!site.Definition.Transport.Optional) return Fail(reason);
|
||||
if (site.TransportOmitted) return true;
|
||||
site.TransportOmitted = true;
|
||||
IEntity exact = site.TransportEntity;
|
||||
site.TransportEntity = null;
|
||||
site.Transport = null;
|
||||
if (exact && m_Entities.Contains(exact))
|
||||
{
|
||||
m_Entities.RemoveItem(exact);
|
||||
if (TransportEmpty(exact)) ERR_Actors.DeleteSpawned(exact);
|
||||
else PrintFormat("Reintroduced [sites]: optional transport cleanup relinquished: %1; occupied or uncertain candidate retained", site.Definition.Id);
|
||||
}
|
||||
PrintFormat("Reintroduced [sites]: optional transport omitted: %1; %2", site.Definition.Id, reason);
|
||||
return true;
|
||||
}
|
||||
protected bool CreateTransport(ERR_SiteRuntime site, vector sitePosition, bool allowOptional = false)
|
||||
{
|
||||
ERR_SiteTransport spec = site.Definition.Transport;
|
||||
if (!spec || (spec.Optional && !allowOptional)) return true;
|
||||
if (site.TransportAttempted) return spec.Optional || site.TransportOmitted || site.Transport != null;
|
||||
site.TransportAttempted = true;
|
||||
vector position = ERR_Actors.Ground(sitePosition + spec.Offset);
|
||||
if (!TransportClearance(position, spec.ClearanceRadius, spec.MaximumTerrainRange)) return TransportFailure(site, "obstructed or unsafe transport parking: " + site.Definition.Id);
|
||||
IEntity entity = ERR_Actors.Spawn(spec.Prefab, position, Vector(spec.Heading, 0, 0));
|
||||
if (!entity) return TransportFailure(site, "transport prefab: " + site.Definition.Id);
|
||||
m_Entities.Insert(entity);
|
||||
site.TransportEntity = entity;
|
||||
if (spec.Optional) site.TransportDeadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
||||
site.Transport = Vehicle.Cast(entity);
|
||||
if (!site.Transport) return TransportFailure(site, "transport must be a vehicle: " + site.Definition.Id);
|
||||
CarControllerComponent controller = CarControllerComponent.Cast(site.Transport.FindComponent(CarControllerComponent));
|
||||
if (!controller) return TransportFailure(site, "transport must be wheeled: " + site.Definition.Id);
|
||||
controller.SetPersistentHandBrake(true);
|
||||
ERR_VehicleSecurityComponent security = ERR_VehicleSecurityComponent.Find(site.Transport);
|
||||
if (security && !security.Configure()) return TransportFailure(site, "transport security configuration: " + site.Definition.Id);
|
||||
return true;
|
||||
}
|
||||
protected bool TransportReady(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!site.Definition.Transport || site.TransportOmitted) return true;
|
||||
Vehicle vehicle = site.Transport;
|
||||
if (!ERR_Actors.Alive(vehicle)) return false;
|
||||
vector low, high;
|
||||
vehicle.GetWorldBounds(low, high);
|
||||
vector center = ERR_Actors.Ground(site.Definition.Position + site.Definition.Transport.Offset);
|
||||
float radius = site.Definition.Transport.ClearanceRadius - 0.5;
|
||||
if (Math.AbsFloat(low[0] - center[0]) > radius || Math.AbsFloat(high[0] - center[0]) > radius || Math.AbsFloat(low[2] - center[2]) > radius || Math.AbsFloat(high[2] - center[2]) > radius)
|
||||
{
|
||||
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Reintroduced [sites]: transport footprint low=%1 high=%2 surveyedCenter=%3 origin=%4 allowedRadius=%5", low, high, center, vehicle.GetOrigin(), radius);
|
||||
return false;
|
||||
}
|
||||
FuelManagerComponent fuel = FuelManagerComponent.Cast(vehicle.FindComponent(FuelManagerComponent));
|
||||
if (!fuel || !fuel.HasFuel())
|
||||
{
|
||||
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Reintroduced [sites]: transport fuel manager=%1 hasFuel=%2", fuel != null, fuel && fuel.HasFuel());
|
||||
return false;
|
||||
}
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return false;
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
manager.GetCompartments(seats);
|
||||
bool driver, passenger;
|
||||
foreach (BaseCompartmentSlot slot : seats)
|
||||
{
|
||||
if (!slot) continue;
|
||||
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
|
||||
if (type == ECompartmentType.PILOT) driver = true;
|
||||
if (type == ECompartmentType.CARGO) passenger = true;
|
||||
}
|
||||
if ((!driver || !passenger) && GetGame().GetWorld().GetWorldTime() > m_Deadline) PrintFormat("Reintroduced [sites]: transport seats=%1 driver=%2 passenger=%3", seats.Count(), driver, passenger);
|
||||
return driver && passenger;
|
||||
}
|
||||
protected bool Fail(string reason)
|
||||
{
|
||||
m_FailureReason = reason;
|
||||
Print("Reintroduced [sites]: preparation failed: " + reason, LogLevel.ERROR);
|
||||
Rollback();
|
||||
m_State = ERR_PrepareResult.FAILED;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Begin(ERR_ScenarioDefinition definition, ResourceName fieldMap)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Begun || m_Stopped) return false;
|
||||
m_Begun = true;
|
||||
if (!definition || !definition.Valid()) return Fail("invalid scenario definition");
|
||||
m_Deadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
||||
m_GuardDeadline = GetGame().GetWorld().GetWorldTime() + ERR_InitialGuardRules.TimeoutMilliseconds();
|
||||
foreach (ERR_SiteDefinition spec : definition.Sites)
|
||||
{
|
||||
vector position = ERR_Actors.Ground(spec.Position);
|
||||
if (position[1] < 1) return Fail("site below sea level: " + spec.Id);
|
||||
vector rootPosition = position;
|
||||
if (spec.PlacementResolved && spec.Kind == ERR_LocationKind.PRISON) rootPosition = spec.Position;
|
||||
rootPosition[1] = rootPosition[1] + spec.RootHeight;
|
||||
IEntity root = ERR_Actors.Spawn(spec.Prefab, rootPosition, Vector(spec.Heading, 0, 0));
|
||||
if (root) m_Entities.Insert(root);
|
||||
if (!root) return Fail("site composition: " + spec.Id);
|
||||
ERR_SiteRuntime site = new ERR_SiteRuntime();
|
||||
site.Definition = spec;
|
||||
site.Location = ERR_LocationComponent.Cast(root.FindComponent(ERR_LocationComponent));
|
||||
if (!site.Location || !site.Location.Configure(spec.Id, spec.Kind, spec.Label)) return Fail("location identity: " + spec.Id);
|
||||
m_Sites.Insert(site);
|
||||
if (spec.Kind == ERR_LocationKind.PRISON) m_Mode.ConfigureStart(position, spec.Heading);
|
||||
if (spec.Kind == ERR_LocationKind.EXTRACTION)
|
||||
{
|
||||
vector aircraftPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(spec.AircraftOffset, spec.Heading));
|
||||
if (spec.RequireAirfield && !ERR_AirfieldPlacement.Clear(aircraftPosition)) return Fail("extraction airfield pad is missing or obstructed; no off-airfield fallback");
|
||||
if (definition.TerrainGenerated && !spec.RequireAirfield && !ERR_AirfieldPlacement.ClearGround(aircraftPosition)) return Fail("generated extraction landing zone is obstructed");
|
||||
Vehicle helicopter = Vehicle.Cast(Spawn(m_Mode.Factions().ExtractionPrefab, aircraftPosition));
|
||||
if (!helicopter) return Fail("extraction vehicle");
|
||||
if (!m_Mode.SetExtractionVehicle(helicopter)) return Fail("extraction vehicle registration");
|
||||
m_ExtractionPlacement = new ERR_ExtractionPlacement(m_Mode, helicopter);
|
||||
}
|
||||
if (!spec.Container.IsEmpty())
|
||||
{
|
||||
vector containerPosition = position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading);
|
||||
bool clearContainer;
|
||||
if (spec.Kind == ERR_LocationKind.PRISON) clearContainer = BackpackClearance(containerPosition);
|
||||
else if (spec.PlacementResolved) clearContainer = GeneratedCacheClearance(containerPosition);
|
||||
else clearContainer = CacheClearance(containerPosition);
|
||||
if (!clearContainer) return Fail("obstructed cache position: " + spec.Id);
|
||||
site.Container = Spawn(spec.Container, position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading));
|
||||
if (!site.Container) return Fail("container: " + spec.Id);
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return Fail("container inventory: " + spec.Id);
|
||||
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(site.Container.FindComponent(BaseInventoryStorageComponent));
|
||||
if (!storage) return Fail("container storage: " + spec.Id);
|
||||
inventory.AddStorage(storage);
|
||||
if (spec.Stock)
|
||||
{
|
||||
foreach (ERR_SiteStock stock : spec.Stock)
|
||||
{
|
||||
ResourceName resource = stock.Prefab;
|
||||
if (resource == ERR_Supplies.Map()) resource = fieldMap;
|
||||
for (int i = 0; i < stock.Count; i++)
|
||||
{
|
||||
// Created exactly once; insertion retries always use this same entity.
|
||||
IEntity item = Spawn(resource, site.Container.GetOrigin());
|
||||
if (!item) return Fail("stock resource: " + spec.Id);
|
||||
if (!stock.TargetId.IsEmpty())
|
||||
{
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(item.FindComponent(ERR_DocumentComponent));
|
||||
if (!document) return Fail("targeted document component");
|
||||
document.SetTarget(stock.TargetId);
|
||||
}
|
||||
ERR_StockPlacement placement = new ERR_StockPlacement();
|
||||
if (spec.Kind == ERR_LocationKind.OUTPOST && !spec.RevealOnEscape && stock.TargetId.IsEmpty()
|
||||
&& site.DepotMagazines.Count() + site.DepotMines.Count() < 96)
|
||||
{
|
||||
if (item.FindComponent(BaseMagazineComponent)) site.DepotMagazines.Insert(item);
|
||||
else if (stock.Prefab == "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et"
|
||||
&& site.DepotMines.Count() < 2) site.DepotMines.Insert(item);
|
||||
}
|
||||
placement.Item = item;
|
||||
placement.Inventory = inventory;
|
||||
m_Placements.Insert(placement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spec.Props)
|
||||
{
|
||||
foreach (ERR_SiteProp prop : spec.Props)
|
||||
{
|
||||
vector propPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(prop.Offset, spec.Heading));
|
||||
// Radio furniture intentionally surrounds/supports its own elevated radio.
|
||||
// Generated camp terrain was checked before any of these objects existed.
|
||||
if (!spec.PlacementResolved && spec.Kind != ERR_LocationKind.RADIO && !CacheClearance(propPosition)) return Fail("obstructed scenery position: " + spec.Id);
|
||||
propPosition[1] = propPosition[1] + prop.Offset[1];
|
||||
IEntity scenery = ERR_Actors.Spawn(prop.Prefab, propPosition, prop.Angles + Vector(spec.Heading, 0, 0));
|
||||
if (!scenery) return Fail("scenery prefab: " + spec.Id);
|
||||
m_Entities.Insert(scenery); site.Props.Insert(scenery);
|
||||
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (aiWorld) aiWorld.RequestNavmeshRebuildEntity(scenery);
|
||||
}
|
||||
}
|
||||
if (!CreateTransport(site, position)) return false;
|
||||
if (spec.Guarded || !spec.Guards.IsEmpty())
|
||||
{
|
||||
vector guardPosition = position + ERR_CampTerrain.Rotate(spec.GuardOffset, spec.Heading);
|
||||
ResourceName groupPrefab = spec.Guards;
|
||||
site.ExpectedGuardFaction = m_Mode.Factions().EnemyFactionKey;
|
||||
if (spec.AllowFactionPool && m_Mode.Content())
|
||||
{
|
||||
ResourceName selectedGroup = m_Mode.Content().GuardGroup(m_ContentGuardIndex++);
|
||||
if (!selectedGroup.IsEmpty())
|
||||
{
|
||||
groupPrefab = selectedGroup;
|
||||
site.ExpectedGuardFaction = m_Mode.Content().GuardFaction(selectedGroup);
|
||||
}
|
||||
}
|
||||
if (groupPrefab.IsEmpty()) groupPrefab = m_Mode.Factions().GuardGroupPrefab;
|
||||
if (spec.Kind == ERR_LocationKind.PRISON)
|
||||
{
|
||||
site.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", guardPosition));
|
||||
if (!ERR_PrisonPatrol.Configure(site.Group, groupPrefab, m_Mode.EnemyFaction())) return Fail("prison patrol roster: " + spec.Id);
|
||||
site.PrisonPatrol = new ERR_PrisonPatrol();
|
||||
}
|
||||
else if (spec.GuardCount > 0)
|
||||
{
|
||||
site.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", guardPosition));
|
||||
Faction faction = GetGame().GetFactionManager().GetFactionByKey(site.ExpectedGuardFaction);
|
||||
if (!ERR_SiteSquads.Configure(site.Group, groupPrefab, faction, spec.GuardCount)) return Fail("site defender roster: " + spec.Id);
|
||||
}
|
||||
else site.Group = SCR_AIGroup.Cast(Spawn(groupPrefab, guardPosition));
|
||||
if (!site.Group) return Fail("guard group resource: " + spec.Id);
|
||||
if (!site.Group.m_aUnitPrefabSlots || site.Group.m_aUnitPrefabSlots.IsEmpty() || !site.Group.GetSpawnImmediately()) return Fail("guard profile must request an initial physical squad: " + spec.Id);
|
||||
site.InitialGuardCount = site.Group.m_aUnitPrefabSlots.Count();
|
||||
foreach (ResourceName member : site.Group.m_aUnitPrefabSlots)
|
||||
if (member.IsEmpty()) return Fail("guard profile has an empty member resource: " + spec.Id);
|
||||
if (!CreateGuardOrders(site, guardPosition)) return Fail("guard initial orders: " + spec.Id);
|
||||
if (!CreateResponders(site, groupPrefab)) return Fail("site mobile roster: " + spec.Id);
|
||||
}
|
||||
PrintFormat("Reintroduced [sites]: authored %1 at %2", spec.Id, position);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected ERR_PrepareResult InitialGuards(ERR_SiteRuntime site)
|
||||
{
|
||||
bool requested = site.Definition.Guarded || !site.Definition.Guards.IsEmpty();
|
||||
if (!requested) return ERR_PrepareResult.READY;
|
||||
if (!site.Group || site.InitialGuardCount <= 0)
|
||||
{
|
||||
Fail("initial guard group disappeared or has no configured members: " + site.Definition.Id);
|
||||
return m_State;
|
||||
}
|
||||
if (!m_Mode.IsMissionEnemy(site.Group.GetFaction()))
|
||||
{
|
||||
Fail("guard group faction disagrees with catalog: " + site.Definition.Id);
|
||||
return m_State;
|
||||
}
|
||||
if (site.Group.GetFaction().GetFactionKey() != site.ExpectedGuardFaction)
|
||||
{
|
||||
Fail("guard prefab faction disagrees with its content definition: " + site.Definition.Id);
|
||||
return m_State;
|
||||
}
|
||||
array<AIAgent> agents = {};
|
||||
array<IEntity> physical = {};
|
||||
site.Group.GetAgents(agents);
|
||||
int conscious;
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (!actor || physical.Contains(actor)) continue;
|
||||
physical.Insert(actor);
|
||||
if (SCR_FactionManager.SGetFaction(actor) != site.Group.GetFaction())
|
||||
{
|
||||
Fail("initial guard actor faction disagrees with catalog: " + site.Definition.Id);
|
||||
return m_State;
|
||||
}
|
||||
if (ERR_Actors.CanFight(actor)) conscious++;
|
||||
}
|
||||
bool complete = site.Group.IsExpandComplete();
|
||||
if (site.LastInitialPhysical != physical.Count() || site.LastInitialConscious != conscious)
|
||||
{
|
||||
PrintFormat("Reintroduced [guards]: initial site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete);
|
||||
site.LastInitialPhysical = physical.Count();
|
||||
site.LastInitialConscious = conscious;
|
||||
}
|
||||
ERR_PrepareResult result = ERR_InitialGuardRules.State(site.InitialGuardCount, physical.Count(), conscious, complete, GetGame().GetWorld().GetWorldTime() > m_GuardDeadline);
|
||||
if (result == ERR_PrepareResult.FAILED)
|
||||
{
|
||||
AIPathfindingComponent path = AIPathfindingComponent.Cast(site.Group.FindComponent(AIPathfindingComponent));
|
||||
NavmeshWorldComponent navmesh;
|
||||
if (path) navmesh = path.GetNavmeshComponent();
|
||||
bool tileLoaded, tileRequested;
|
||||
if (navmesh)
|
||||
{
|
||||
tileLoaded = navmesh.IsTileLoaded(site.Group.GetOrigin());
|
||||
tileRequested = navmesh.IsTileRequested(site.Group.GetOrigin());
|
||||
}
|
||||
PrintFormat("Reintroduced [guards]: timeout detail site=%1 navmesh=%2 tileLoaded=%3 tileRequested=%4 dormantAlive=%5 dormantDead=%6", site.Definition.Id, navmesh != null, tileLoaded, tileRequested, site.Group.GetDormantAliveCount(), site.Group.GetDormantDeadCount());
|
||||
Fail(string.Format("initial guard population incomplete or invalid: site=%1 requested=%2 physical=%3 conscious=%4 nativeComplete=%5; limit=90s; no replacement requested", site.Definition.Id, site.InitialGuardCount, physical.Count(), conscious, complete));
|
||||
}
|
||||
if (result == ERR_PrepareResult.READY && site.PrisonPatrol)
|
||||
{
|
||||
if (!site.PrisonPatrol.Place(site.Group, site.Location.Position()))
|
||||
{
|
||||
Fail("no clear perimeter positions for prison patrol"); return ERR_PrepareResult.FAILED;
|
||||
}
|
||||
result = site.PrisonPatrol.Radio(m_Mode, site.Group, m_GuardDeadline);
|
||||
if (result == ERR_PrepareResult.FAILED) Fail("prison patrol large radio could not be equipped");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
protected void CancelInitialGuardRequests()
|
||||
{
|
||||
// Stock DespawnMembers returns early for an empty group; explicitly remove
|
||||
// its queued initial request before a failed/stopped preparation deletes it.
|
||||
if (!GetGame()) return;
|
||||
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!world) return;
|
||||
array<ERR_SiteRuntime> pendingGroups = {};
|
||||
foreach (ERR_SiteRuntime owner : m_Sites) { pendingGroups.Insert(owner); if (owner.Responders) pendingGroups.Insert(owner.Responders); }
|
||||
foreach (ERR_SiteRuntime site : pendingGroups)
|
||||
{
|
||||
if (!site.Group || site.InitialGuardCount <= 0) continue;
|
||||
// Retire this instance's initial spawn intent as well as its current queue
|
||||
// entries. A native expansion pass may otherwise schedule the empty capacity
|
||||
// again. Existing physical members remain until the owner rolls back.
|
||||
site.Group.SetSpawnImmediately(false);
|
||||
site.Group.m_aUnitPrefabSlots = new array<ResourceName>();
|
||||
world.PurgeSpawnRequestsForGroup(site.Group);
|
||||
}
|
||||
}
|
||||
|
||||
ERR_PrepareResult Prepare()
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Begun || m_Stopped) return ERR_PrepareResult.FAILED;
|
||||
if (m_State != ERR_PrepareResult.PENDING) return m_State;
|
||||
if (!m_ExtractionPlacement) { Fail("extraction placement owner missing"); return m_State; }
|
||||
ERR_PrepareResult aircraftPlacement = m_ExtractionPlacement.Poll(GetGame().GetWorld().GetWorldTime());
|
||||
if (aircraftPlacement == ERR_PrepareResult.FAILED) { Fail(m_ExtractionPlacement.FailureReason()); return m_State; }
|
||||
int pending, accepted, pendingGuards;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
{
|
||||
if (site.Definition.Transport && !site.Definition.Transport.Optional && !TransportReady(site))
|
||||
{
|
||||
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) Fail("transport needs fuel, driver/passenger seats and a fitting parking footprint: " + site.Definition.Id);
|
||||
return m_State;
|
||||
}
|
||||
ERR_PrepareResult guards = InitialGuards(site);
|
||||
if (guards == ERR_PrepareResult.FAILED) return m_State;
|
||||
if (guards == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||
if (site.Responders)
|
||||
{
|
||||
ERR_PrepareResult mobile = InitialGuards(site.Responders);
|
||||
if (mobile == ERR_PrepareResult.FAILED) return m_State;
|
||||
if (mobile == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||
}
|
||||
}
|
||||
string firstRejected;
|
||||
foreach (ERR_StockPlacement placement : m_Placements)
|
||||
{
|
||||
if (!placement.Item || !placement.Inventory) { Fail("stock disappeared during initialization"); return m_State; }
|
||||
array<IEntity> items = {};
|
||||
placement.Inventory.GetItems(items);
|
||||
if (items.Contains(placement.Item)) continue;
|
||||
pending++;
|
||||
if (firstRejected.IsEmpty() && placement.Item.GetPrefabData()) firstRejected = placement.Item.GetPrefabData().GetPrefabName();
|
||||
if (placement.Accepted) accepted++;
|
||||
if (!placement.Accepted) placement.Accepted = placement.Inventory.TryInsertItem(placement.Item);
|
||||
}
|
||||
if (pending)
|
||||
{
|
||||
if (GetGame().GetWorld().GetWorldTime() > m_Deadline) Fail(string.Format("physical stock insertion timed out: pending=%1 accepted=%2 sample=%3", pending, accepted, firstRejected));
|
||||
return m_State;
|
||||
}
|
||||
if (pendingGuards > 0 || aircraftPlacement == ERR_PrepareResult.PENDING) return m_State;
|
||||
// All required scenery, stock and physical guards now exist. Survey optional
|
||||
// parking once against that real population, then bound only vehicle settling.
|
||||
int pendingTransports;
|
||||
foreach (ERR_SiteRuntime optionalSite : m_Sites)
|
||||
{
|
||||
if (!optionalSite.Definition.Transport || !optionalSite.Definition.Transport.Optional) continue;
|
||||
if (!CreateTransport(optionalSite, optionalSite.Definition.Position, true)) return m_State;
|
||||
if (TransportReady(optionalSite)) continue;
|
||||
if (GetGame().GetWorld().GetWorldTime() > optionalSite.TransportDeadline)
|
||||
{
|
||||
if (!TransportFailure(optionalSite, "transport needs fuel, driver/passenger seats and a fitting parking footprint: " + optionalSite.Definition.Id)) return m_State;
|
||||
}
|
||||
else pendingTransports++;
|
||||
}
|
||||
if (pendingTransports > 0) return m_State;
|
||||
foreach (IEntity entity : m_Entities) m_Mode.Own(entity);
|
||||
m_State = ERR_PrepareResult.READY;
|
||||
PrintFormat("Reintroduced [sites]: %1 sites and %2 physical stock items ready", m_Sites.Count(), m_Placements.Count());
|
||||
m_Placements.Clear();
|
||||
return m_State;
|
||||
}
|
||||
|
||||
void Activate()
|
||||
{
|
||||
if (m_State != ERR_PrepareResult.READY || m_Stopped || m_Activated) return;
|
||||
m_Activated = true;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
{
|
||||
if (site.Definition.Kind == ERR_LocationKind.PRISON && site.Location) site.Location.Discover(0, 100);
|
||||
if (site.Group)
|
||||
{
|
||||
ERR_ForceRoute route;
|
||||
ERR_ForceKind kind = ERR_ForceKind.GARRISON;
|
||||
if (site.Definition.Patrol && !site.Definition.Patrol.IsEmpty())
|
||||
{
|
||||
route = new ERR_ForceRoute(true);
|
||||
vector anchor = site.Definition.Position + site.Definition.GuardOffset;
|
||||
foreach (ERR_SitePatrolPoint point : site.Definition.Patrol) route.Add(anchor + point.Offset);
|
||||
kind = ERR_ForceKind.PATROL;
|
||||
}
|
||||
m_Mode.Forces().Register(site.Definition.Id, kind, site.Group.GetOrigin(), m_Mode.ForceLiving(site.Group), route, site.Group, GetGame().GetWorld().GetWorldTime());
|
||||
m_Mode.RegisterGroup(site.Group);
|
||||
ActivateResponders(site);
|
||||
}
|
||||
}
|
||||
}
|
||||
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (!Replication.IsServer() || current != ERR_Phase.RADIO_SEARCH) return;
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
if (site.Definition.RevealOnEscape && site.Location) site.Location.Discover(0, 100);
|
||||
}
|
||||
bool CanObserve(IEntity player, ERR_SiteRuntime site)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode.Active() || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return false;
|
||||
if (!site || !site.Definition || !m_Sites.Contains(site) || !site.Location || !m_Mode.HasLocation(site.Location) || !ERR_Actors.CanFight(player)) return false;
|
||||
if (!ERR_Actors.Player(player) || !m_Mode.Players() || !m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return false;
|
||||
if (!m_Mode.Players().CurrentActor(player)) return false;
|
||||
vector position = site.Location.Position();
|
||||
vector observer = player.GetOrigin();
|
||||
if (!ERR_SiteDiscoveryRules.InRange(site.Definition.DiscoverRadius, vector.DistanceXZ(observer, position), Math.AbsFloat(observer[1] - position[1]))) return false;
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = player.GetOrigin() + "0 1.5 0";
|
||||
trace.End = position + "0 1.3 0";
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = player;
|
||||
return GetGame().GetWorld().TraceMove(trace, null) >= 1;
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || now < m_NextDiscovery) return;
|
||||
m_NextDiscovery = now + 5000;
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players, true);
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
{
|
||||
if (!site.Location || site.Location.Knowledge() >= 2) continue;
|
||||
foreach (IEntity player : players)
|
||||
{
|
||||
if (!CanObserve(player, site)) continue;
|
||||
site.Location.Discover(0, 100);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Rollback()
|
||||
{
|
||||
// An unrelated required failure can arrive while an optional candidate is
|
||||
// settling. Apply the same occupant-safe omission before recursive rollback.
|
||||
if (m_State == ERR_PrepareResult.PENDING)
|
||||
foreach (ERR_SiteRuntime optionalSite : m_Sites)
|
||||
if (optionalSite.Definition.Transport && optionalSite.Definition.Transport.Optional && optionalSite.TransportEntity)
|
||||
TransportFailure(optionalSite, "preparation rolled back before optional transport readiness");
|
||||
Stop();
|
||||
// Children/items are listed after parents; delete them first.
|
||||
for (int i = m_Entities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Entities[i]);
|
||||
m_Entities.Clear();
|
||||
m_Placements.Clear();
|
||||
m_Sites.Clear();
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_ExtractionPlacement) m_ExtractionPlacement.Stop();
|
||||
if (m_State == ERR_PrepareResult.PENDING) CancelInitialGuardRequests();
|
||||
StopPatrols();
|
||||
}
|
||||
protected bool CreateResponders(ERR_SiteRuntime site, ResourceName profile)
|
||||
{
|
||||
ERR_SiteDefinition spec = site.Definition;
|
||||
if (spec.ResponseCount == 0) return true;
|
||||
ERR_SiteRuntime mobile = new ERR_SiteRuntime();
|
||||
site.Responders = mobile;
|
||||
mobile.Definition = spec; mobile.Location = site.Location;
|
||||
mobile.ExpectedGuardFaction = site.ExpectedGuardFaction;
|
||||
vector position = ERR_Actors.Ground(spec.Position + ERR_CampTerrain.Rotate(spec.ResponseOffset, spec.Heading));
|
||||
mobile.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", position));
|
||||
mobile.InitialGuardCount = spec.ResponseCount;
|
||||
if (!ERR_SiteSquads.Configure(mobile.Group, profile, site.Group.GetFaction(), spec.ResponseCount)) return false;
|
||||
// Local guard duty resumes when an HQ search expires. HQ inserts its delivered
|
||||
// search ahead of this waypoint; only this mobile group is eligible to leave.
|
||||
AIWaypoint home = AIWaypoint.Cast(Spawn(ERR_Resources.DefendWaypoint(), position));
|
||||
if (!home) return false;
|
||||
home.SetCompletionRadius(25); mobile.Group.AddWaypoint(home);
|
||||
return true;
|
||||
}
|
||||
protected void ActivateResponders(ERR_SiteRuntime site)
|
||||
{
|
||||
if (!site.Responders || !site.Responders.Group) return;
|
||||
SCR_AIGroup group = site.Responders.Group;
|
||||
m_Mode.Forces().Register(site.Definition.Id + "-responders", ERR_ForceKind.PATROL, group.GetOrigin(), m_Mode.ForceLiving(group), null, group, GetGame().GetWorld().GetWorldTime());
|
||||
m_Mode.RegisterGroup(group);
|
||||
ERR_HQ.Event("SITE GARRISON", string.Format("%1 (%2) at %3: %4 permanent defenders stay at the site; %5 mobile soldiers may receive HQ hunting orders; faction=%6", site.Definition.Label, site.Definition.Id, site.Location.Position(), m_Mode.ForceLiving(site.Group), m_Mode.ForceLiving(group), site.ExpectedGuardFaction));
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
// Optional depot work runs after all existing tactical/logistics planners.
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref ERR_GroundDepotLoading m_GroundDepot;
|
||||
int GroundSupplyCount()
|
||||
{
|
||||
if (!m_Resupply) return 0;
|
||||
return m_Resupply.Count();
|
||||
}
|
||||
ERR_InfantryResupply GroundSupplyAt(int index)
|
||||
{
|
||||
if (!m_Resupply) return null;
|
||||
return m_Resupply.At(index);
|
||||
}
|
||||
ERR_HQUnit GroundUnit(SCR_AIGroup group)
|
||||
{
|
||||
if (!group) return null;
|
||||
foreach (ERR_HQUnit unit : m_Units) if (unit && unit.Group == group) return unit;
|
||||
return null;
|
||||
}
|
||||
bool ReserveGroundDepot(ERR_HQUnit crew, ERR_HQUnit donor, ERR_InfantryResupply service, int token, bool mineCargo = false)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopping || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return false;
|
||||
if (m_Mode.Command() != this) return false;
|
||||
if (token <= 300000 || token > 300096 || !crew || !donor || crew == donor || !m_Units.Contains(crew)) return false;
|
||||
if (!service || !MountedLogisticsEligible(donor)) return false;
|
||||
if (mineCargo) { if (!service.DepotMineNeeded()) return false; }
|
||||
else if (!service.DepotNeeded()) return false;
|
||||
ERR_ConvoyVehicle truck = service.DepotTruck();
|
||||
if (!truck || truck.Group != donor.Group || truck.SupplyToken > 0) return false;
|
||||
if (crew.HomeGuard || crew.ReservedForSupport || crew.Task || crew.PendingTask || crew.PendingCheckin || crew.PendingConvoyOrder) return false;
|
||||
if (!ReserveLogistics(crew.Group, token)) return false;
|
||||
// Single authoritative call, no native mutation between validation and claims.
|
||||
donor.LogisticsReservation = token; truck.SupplyToken = token;
|
||||
return true;
|
||||
}
|
||||
void ReleaseGroundDepot(ERR_HQUnit crew, ERR_HQUnit donor, ERR_ConvoyVehicle truck, int token)
|
||||
{
|
||||
if (!Replication.IsServer() || token <= 300000 || token > 300096) return;
|
||||
// These retained exact unit objects outlive base Stop's m_Units.Clear().
|
||||
if (crew && crew.LogisticsReservation == token) crew.LogisticsReservation = 0;
|
||||
if (donor && donor.LogisticsReservation == token) donor.LogisticsReservation = 0;
|
||||
if (truck && truck.SupplyToken == token) { truck.SupplyToken = 0; truck.SupplyHeld = false; }
|
||||
}
|
||||
override string Status()
|
||||
{
|
||||
string result = super.Status();
|
||||
if (m_GroundDepot) result += "\n" + m_GroundDepot.Status();
|
||||
return result;
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
super.Tick(now);
|
||||
if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Mode.Command() != this) return;
|
||||
if (m_GroundDepot) m_GroundDepot.Tick(now);
|
||||
if (m_Stopping || !m_Mode.Active() || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return;
|
||||
if (!m_GroundDepot) m_GroundDepot = new ERR_GroundDepotLoading(this, m_Mode);
|
||||
m_GroundDepot.Plan(now);
|
||||
}
|
||||
protected void CancelGroundDepotPoll()
|
||||
{
|
||||
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(PollGroundDepot);
|
||||
}
|
||||
protected void PollGroundDepot()
|
||||
{
|
||||
CancelGroundDepotPoll();
|
||||
if (!m_Stopping || !Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || m_Mode.Command() != this) return;
|
||||
if (!m_GroundDepot || !GetGame() || !GetGame().GetWorld() || !GetGame().GetCallqueue()) return;
|
||||
m_GroundDepot.Tick(GetGame().GetWorld().GetWorldTime());
|
||||
if (m_GroundDepot.Unresolved()) GetGame().GetCallqueue().CallLater(PollGroundDepot, 1000, false);
|
||||
}
|
||||
override void Stop()
|
||||
{
|
||||
if (m_GroundDepot) m_GroundDepot.Stop();
|
||||
super.Stop();
|
||||
PollGroundDepot();
|
||||
}
|
||||
void ~ERR_HQ() { CancelGroundDepotPoll(); }
|
||||
}
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
// Native storage anchors only; no invented interaction point or remote move.
|
||||
class ERR_GroundDepotAccess
|
||||
{
|
||||
protected IEntity m_Root;
|
||||
protected IEntity m_Actor;
|
||||
protected bool Filter(notnull IEntity entity)
|
||||
{
|
||||
return !ERR_ConvoyRules.PartOf(entity, m_Root) && !ERR_ConvoyRules.PartOf(entity, m_Actor);
|
||||
}
|
||||
static bool Points(IEntity root, out array<vector> points)
|
||||
{
|
||||
points = {};
|
||||
if (!root) return false;
|
||||
ActionsManagerComponent manager = ActionsManagerComponent.Cast(root.FindComponent(ActionsManagerComponent));
|
||||
if (!manager || !manager.IsEnabled()) return false;
|
||||
array<UserActionContext> contexts = {}; manager.GetContextList(contexts);
|
||||
if (contexts.IsEmpty() || contexts.Count() > 128) return false;
|
||||
foreach (UserActionContext context : contexts)
|
||||
{
|
||||
if (!context) return false;
|
||||
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
|
||||
if (actions.Count() > 64) return false;
|
||||
bool storage;
|
||||
foreach (BaseUserAction action : actions)
|
||||
if (action && action.GetOwner() == root && (SCR_OpenStorageAction.Cast(action) || SCR_OpenVehicleStorageAction.Cast(action))) storage = true;
|
||||
if (!storage) continue;
|
||||
vector transform[4];
|
||||
if (!context.GetTransformationWorld(transform) || !ERR_ForceRules.FiniteVector(transform[3])) return false;
|
||||
points.Insert(transform[3]);
|
||||
}
|
||||
return !points.IsEmpty();
|
||||
}
|
||||
bool Check(IEntity root, IEntity actor)
|
||||
{
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(actor);
|
||||
if (!character || !GetGame() || !GetGame().GetWorld()) return false;
|
||||
array<vector> points;
|
||||
if (!Points(root, points)) return false;
|
||||
vector start = character.AimingPosition();
|
||||
if (!ERR_ForceRules.FiniteVector(start)) return false;
|
||||
m_Root = root; m_Actor = actor;
|
||||
foreach (vector point : points)
|
||||
{
|
||||
float distance = vector.Distance(start, point);
|
||||
if (!(distance >= 0.05 && distance <= 3)) continue;
|
||||
TraceParam trace = new TraceParam(); trace.Start = start; trace.End = point;
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS; trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
if (GetGame().GetWorld().TraceMove(trace, Filter) == 1) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool Clear(IEntity root, IEntity actor)
|
||||
{
|
||||
ERR_GroundDepotAccess access = new ERR_GroundDepotAccess(); return access.Check(root, actor);
|
||||
}
|
||||
}
|
||||
|
||||
// Opportunistic camp -> original responder -> original ammo truck. One job
|
||||
// mission-wide; no new force, warehouse, stock, native inventory retry or refill.
|
||||
class ERR_GroundDepotLoading
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<IEntity> m_Original = {};
|
||||
protected ref array<IEntity> m_Spent = {};
|
||||
protected bool m_Captured;
|
||||
protected bool m_Stopped;
|
||||
protected int m_Attempts;
|
||||
protected int m_Delivered;
|
||||
protected int m_State; // 0 idle,1 truck radio,2 stopped report,3 porter radio,4 pickup,5 delivery,6 returning,7 quarantine
|
||||
protected int m_Token;
|
||||
protected float m_Deadline;
|
||||
protected float m_HeldAt = -1;
|
||||
protected float m_NextPlan;
|
||||
protected float m_NextTick;
|
||||
protected float m_Last = -1;
|
||||
protected ERR_SiteRuntime m_Site;
|
||||
protected IEntity m_Cache;
|
||||
protected InventoryStorageManagerComponent m_CacheInventory;
|
||||
protected IEntity m_Worker;
|
||||
protected SCR_AIUtilityComponent m_Utility;
|
||||
protected int m_WorkerSlot;
|
||||
protected ref ERR_HQUnit m_Crew;
|
||||
protected ref ERR_HQUnit m_Donor;
|
||||
protected ref ERR_ForceRecord m_CrewRecord;
|
||||
protected ERR_InfantryResupply m_Service;
|
||||
protected ERR_ConvoyTask m_Task;
|
||||
protected ERR_ConvoyVehicle m_Truck;
|
||||
protected IEntity m_Item;
|
||||
protected vector m_CachePosition;
|
||||
protected vector m_TruckPosition;
|
||||
protected ref ERR_HQRoute m_Order;
|
||||
protected ref SCR_AIBehaviorBase m_Action;
|
||||
protected ref ERR_GroundDepotTransfer m_Move;
|
||||
protected ref ERR_FiniteMagazineReservation m_Reservation;
|
||||
protected bool m_PickedUp;
|
||||
protected bool m_DeliveryClaimed;
|
||||
protected bool m_Imported;
|
||||
protected bool m_MineCargo;
|
||||
bool MineCargo() { return m_MineCargo; }
|
||||
void ERR_GroundDepotLoading(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; }
|
||||
bool Busy() { return m_State != 0; }
|
||||
bool Unresolved() { return m_Move && m_Move.Unresolved(); }
|
||||
string Status() { return string.Format("Ground depot: one job, state=%1; delivered=%2; attempts=%3/96; original sources=%4", m_State, m_Delivered, m_Attempts, m_Original.Count()); }
|
||||
protected bool Active()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Mode.Command() == m_HQ;
|
||||
}
|
||||
protected void Note(string reason)
|
||||
{
|
||||
if (m_HQ) m_HQ.Journal().Write("GROUND DEPOT", reason, m_Token);
|
||||
}
|
||||
protected static bool Present(IEntity entity)
|
||||
{
|
||||
if (!entity) return false;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(entity.FindComponent(DamageManagerComponent));
|
||||
return !damage || !damage.IsDestroyed();
|
||||
}
|
||||
protected bool Stationary()
|
||||
{
|
||||
if (!m_Truck || !m_Truck.Entity || !m_Truck.Entity.GetPhysics()) return false;
|
||||
Physics physics = m_Truck.Entity.GetPhysics();
|
||||
float speed = physics.GetVelocity().Length(); float angular = physics.GetAngularVelocity().Length();
|
||||
return speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1;
|
||||
}
|
||||
protected bool Quiet(ERR_HQUnit unit)
|
||||
{
|
||||
return unit && unit.Group && unit.Utility && unit.StatusSampler && !unit.Group.IsSlave()
|
||||
&& unit.Group.GetPlayerCount(true) == 0 && unit.Group.GetFaction() == m_Mode.EnemyFaction()
|
||||
&& (!unit.Utility.m_Perception || unit.Utility.m_Perception.m_aTargets.IsEmpty());
|
||||
}
|
||||
protected bool WorkerValid()
|
||||
{
|
||||
if (!Quiet(m_Crew) || !m_Worker || !m_Utility || !m_Utility.m_AIInfo || !ERR_Actors.CanFight(m_Worker)) return false;
|
||||
if (!m_Crew.StatusSampler.OwnsRoster(m_Crew.Group) || !m_Crew.StatusSampler.CanAttribute(m_WorkerSlot, m_Worker, false)) return false;
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, m_Crew.Group, m_Worker) || CompartmentAccessComponent.GetVehicleIn(m_Worker)) return false;
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Worker));
|
||||
return agent && agent.m_UtilityComponent == m_Utility && m_CrewRecord && m_Mode.Forces().FindGroup(m_Crew.Group) == m_CrewRecord
|
||||
&& m_CrewRecord.Physical && !m_CrewRecord.Eliminated;
|
||||
}
|
||||
protected bool Valid()
|
||||
{
|
||||
if (!ERR_GroundDepotTransfer.Cargo(m_Item, m_MineCargo)) return false;
|
||||
if (!Active() || !m_Service || m_Service.Task() != m_Task || !m_Service.DepotCurrent() || m_Service.DepotTruck() != m_Truck) return false;
|
||||
if (!m_Task || !m_Task.Receipt || !m_Task.Receipt.Consumed || m_Task.Vehicles.Count() != 6 || m_Task.Vehicles[1] != m_Truck) return false;
|
||||
ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Task);
|
||||
if (!supply || !supply.Tactics || supply.Tactics.State() != 0 || !m_Truck.Combat || m_Truck.Combat.Held) return false;
|
||||
if (!Quiet(m_Donor) || m_Donor.Group != m_Truck.Group || !m_HQ.ValidateBoundVehicle(m_Donor.Group, m_Truck.Entity)) return false;
|
||||
if (!m_Site || !m_Mode.Sites() || !m_Site.Definition || m_Mode.Sites().Find(m_Site.Definition.Id) != m_Site) return false;
|
||||
if (m_Site.Container != m_Cache || !Present(m_Cache) || m_Cache.FindComponent(InventoryStorageManagerComponent) != m_CacheInventory) return false;
|
||||
if (!m_Site.Responders || m_Site.Responders.Group != m_Crew.Group || !m_Mode.Sites().DepotOriginal(m_Site, m_Item)) return false;
|
||||
if (vector.Distance(m_Cache.GetOrigin(), m_CachePosition) > 2 || !WorkerValid()) return false;
|
||||
return m_Token > 300000 && m_Truck.SupplyToken == m_Token && m_Crew.LogisticsReservation == m_Token && m_Donor.LogisticsReservation == m_Token;
|
||||
}
|
||||
protected void Capture()
|
||||
{
|
||||
if (m_Captured || !m_Mode.Sites()) return;
|
||||
m_Captured = true;
|
||||
for (int i = 0; i < m_Mode.Sites().Count() && i < 64; i++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At(i);
|
||||
array<IEntity> originals = {}; m_Mode.Sites().DepotStock(site, originals);
|
||||
foreach (IEntity item : originals)
|
||||
{
|
||||
if (m_Original.Count() >= 96) return;
|
||||
if (item && !m_Original.Contains(item)) m_Original.Insert(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
protected bool Stored(IEntity item, IEntity owner)
|
||||
{
|
||||
if (!item || !owner || !ERR_InfantryAmmoTransfer.StoredUnder(item, owner)) return false;
|
||||
InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
|
||||
if (!carried || !carried.GetParentSlot() || carried.GetParentSlot().GetAttachedEntity() != item) return false;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory || inventory.GetOwner() != owner) return false;
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
return items.Count() <= 128 && items.Contains(item);
|
||||
}
|
||||
protected bool SelectWorker(ERR_HQUnit crew, IEntity item, bool mineCargo = false)
|
||||
{
|
||||
if (!item || !Quiet(crew) || crew.HomeGuard || crew.ReservedForSupport || crew.LogisticsReservation > 0) return false;
|
||||
if (crew.Assignment || crew.Task || crew.PendingTask || crew.PendingOrder || crew.Downlink || crew.Waypoint) return false;
|
||||
int count = crew.StatusSampler.InitialStrength();
|
||||
if (count < 1 || count > 32 || !crew.StatusSampler.OwnsRoster(crew.Group)) return false;
|
||||
for (int slot = 0; slot < count; slot++)
|
||||
{
|
||||
IEntity actor = crew.StatusSampler.InitialMember(slot);
|
||||
if (!actor || !crew.StatusSampler.CanAttribute(slot, actor, false) || !ERR_EnemyEquipmentRules.Eligible(m_Mode, crew.Group, actor) || CompartmentAccessComponent.GetVehicleIn(actor)) continue;
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
||||
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) continue;
|
||||
if (!mineCargo && !ERR_InfantryAmmoTransfer.Compatible(actor, item)) continue;
|
||||
m_Worker = actor; m_WorkerSlot = slot; m_Utility = agent.m_UtilityComponent; return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
void Plan(float now)
|
||||
{
|
||||
if (!Active() || Busy() || !ERR_ForceRules.Finite(now) || now < m_NextPlan || m_Attempts >= 96) return;
|
||||
m_NextPlan = now + 5000; Capture();
|
||||
for (int index = 0; index < m_HQ.GroundSupplyCount() && index < 20; index++)
|
||||
{
|
||||
ERR_InfantryResupply service = m_HQ.GroundSupplyAt(index);
|
||||
if (!service || (!service.DepotNeeded() && !service.DepotMineNeeded())) continue;
|
||||
ERR_ConvoyVehicle truck = service.DepotTruck();
|
||||
if (!truck || truck.SupplyToken > 0) continue;
|
||||
ERR_HQUnit donor = m_HQ.GroundUnit(truck.Group);
|
||||
if (!Quiet(donor)) continue;
|
||||
ERR_HQWatchEntry report = m_HQ.Watch().Find(donor.ForceSourceId);
|
||||
if (!ERR_InfantryResupplyRules.Fresh(report, now)) continue;
|
||||
for (int siteIndex = 0; siteIndex < m_Mode.Sites().Count() && siteIndex < 64; siteIndex++)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().At(siteIndex);
|
||||
if (!site || !site.Definition || m_Mode.Sites().DepotItemCount(site) <= 0) continue;
|
||||
// The camp is an authored own location; only the truck position is reported.
|
||||
if (vector.DistanceXZ(site.Definition.Position, report.Position) > 150) continue;
|
||||
ERR_HQUnit crew = m_HQ.GroundUnit(site.Responders.Group);
|
||||
for (int itemIndex = 0; itemIndex < m_Mode.Sites().DepotItemCount(site) && itemIndex < 96; itemIndex++)
|
||||
{
|
||||
IEntity item = m_Mode.Sites().DepotItem(site, itemIndex);
|
||||
bool mineCargo = ERR_InfantryMineTransfer.ReadyMine(item);
|
||||
if (mineCargo) { if (!service.DepotMineNeeded()) continue; }
|
||||
else if (!service.DepotNeeded()) continue;
|
||||
if (!m_Original.Contains(item) || m_Spent.Contains(item) || !ERR_GroundDepotTransfer.Cargo(item, mineCargo) || !Stored(item, site.Container)) continue;
|
||||
// Select an original worker for this exact item; an incompatible first
|
||||
// member must not hide a later eligible carrier of finite camp stock.
|
||||
if (!SelectWorker(crew, item, mineCargo)) continue;
|
||||
int token = 300000 + m_Attempts + 1;
|
||||
if (!m_HQ.ReserveGroundDepot(crew, donor, service, token, mineCargo)) continue;
|
||||
m_Reservation = new ERR_FiniteMagazineReservation(m_Original, m_Spent, item);
|
||||
if (!m_Reservation.Reserve()) { m_HQ.ReleaseGroundDepot(crew, donor, truck, token); continue; }
|
||||
m_MineCargo = mineCargo;
|
||||
m_Attempts++; m_Token = token; m_Crew = crew; m_Donor = donor; m_Service = service; m_Task = service.Task(); m_Truck = truck;
|
||||
m_Site = site; m_Cache = site.Container; m_CachePosition = m_Cache.GetOrigin(); m_Item = item;
|
||||
m_CacheInventory = InventoryStorageManagerComponent.Cast(m_Cache.FindComponent(InventoryStorageManagerComponent));
|
||||
m_CrewRecord = m_Mode.Forces().FindGroup(crew.Group);
|
||||
m_PickedUp = false; m_DeliveryClaimed = false; m_Imported = false; m_HeldAt = -1;
|
||||
m_Deadline = Math.Min(now + 180000, report.ObservedAt + 240000); m_State = 1;
|
||||
m_Order = m_HQ.Downlink(donor.Group, "DEPOT HOLD", "hold original truck for one finite camp item", report.MessageId);
|
||||
if (!m_Order) Abort("truck radio unavailable"); else Note("one original camp item reserved; awaiting truck hold radio");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
protected void ClearAction()
|
||||
{
|
||||
if (m_Action && m_Worker && m_Utility && m_Utility.m_AIInfo)
|
||||
{
|
||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Worker));
|
||||
if (agent && agent.m_UtilityComponent == m_Utility)
|
||||
{
|
||||
array<ref AIActionBase> actions = {}; m_Utility.GetActions(actions);
|
||||
if (actions.Count() <= 128 && actions.Contains(m_Action)) m_Action.Complete();
|
||||
}
|
||||
}
|
||||
m_Action = null;
|
||||
}
|
||||
protected bool Walk(vector position)
|
||||
{
|
||||
if (!WorkerValid() || !ERR_ForceRules.FiniteVector(position)) return false;
|
||||
ClearAction(); m_Action = new SCR_AIMoveIndividuallyBehavior(m_Utility, null, ERR_Actors.Ground(position), 1000, 0, null, 0.5);
|
||||
m_Utility.AddAction(m_Action); return true;
|
||||
}
|
||||
protected void Cleanup()
|
||||
{
|
||||
ClearAction();
|
||||
if (m_Reservation) m_Reservation.ReleaseUnsubmitted();
|
||||
if (m_HQ) m_HQ.ReleaseGroundDepot(m_Crew, m_Donor, m_Truck, m_Token);
|
||||
m_State = 0; m_MineCargo = false; m_Token = 0; m_Order = null; m_Move = null; m_Reservation = null;
|
||||
m_Crew = null; m_Donor = null; m_Service = null; m_Task = null; m_Truck = null;
|
||||
m_Worker = null; m_Utility = null; m_Site = null; m_Cache = null; m_Item = null; m_CacheInventory = null; m_CrewRecord = null;
|
||||
}
|
||||
protected void Abort(string reason)
|
||||
{
|
||||
Note(reason); ClearAction(); m_Order = null;
|
||||
if (m_Move) m_Move.Stop();
|
||||
if (Unresolved()) { m_State = 7; return; }
|
||||
Cleanup();
|
||||
}
|
||||
void Stop() { if (m_Stopped) return; m_Stopped = true; Abort("depot service stopped; any submitted uncertainty retains original claims"); }
|
||||
bool TransferValid(ERR_GroundDepotTransfer operation)
|
||||
{
|
||||
if (!operation || operation != m_Move || operation.Item() != m_Item || operation.MineCargo() != m_MineCargo || !Valid() || !Stationary() || !m_Truck.SupplyHeld) return false;
|
||||
if (operation.Delivery()) return m_PickedUp && m_State == 5 && ERR_GroundDepotAccess.Clear(m_Truck.Entity, m_Worker);
|
||||
return !m_PickedUp && m_State == 4 && ERR_GroundDepotAccess.Clear(m_Cache, m_Worker);
|
||||
}
|
||||
bool ClaimDelivery(ERR_GroundDepotTransfer operation)
|
||||
{
|
||||
if (!TransferValid(operation) || !operation.Delivery() || m_DeliveryClaimed || !m_Reservation.CommittedFor(m_Item)) return false;
|
||||
if (m_MineCargo) { if (!m_Service.DepotMineNeeded()) return false; }
|
||||
else if (!m_Service.DepotNeeded()) return false;
|
||||
m_DeliveryClaimed = true; return true;
|
||||
}
|
||||
bool DeliveredTo(ERR_InfantryResupply service, IEntity item, bool mineCargo = false)
|
||||
{
|
||||
return mineCargo == m_MineCargo && !m_Imported && m_State == 5 && service == m_Service && item == m_Item && m_Move && m_Move.Delivery() && m_Move.MineCargo() == m_MineCargo
|
||||
&& m_Move.Confirmed() && m_Reservation && m_Reservation.CommittedFor(item) && Valid();
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
// Terminal proof only: a late callback cannot resume walking or admit stock.
|
||||
if (m_State == 7) { if (!Unresolved()) Cleanup(); return; }
|
||||
if (!Busy()) return;
|
||||
if (!ERR_ForceRules.Finite(now) || now < m_Last || now >= m_Deadline || !Valid()) { Abort("depot deadline or original ownership lost"); return; }
|
||||
m_Last = now;
|
||||
if (now < m_NextTick) return;
|
||||
m_NextTick = now + 1000;
|
||||
if (m_State == 1 || m_State == 3)
|
||||
{
|
||||
int result = m_Order.Advance(now, m_HQ.Journal());
|
||||
if (result == 0) return;
|
||||
if (result < 0) { Abort("depot radio interrupted"); return; }
|
||||
m_Order = null;
|
||||
if (m_State == 1) { m_Truck.SupplyHeld = true; m_Truck.ClearOrder(); m_State = 2; return; }
|
||||
if (!Walk(m_CachePosition + "2 0 0")) { Abort("original porter cannot walk to camp cache"); return; }
|
||||
m_State = 4; return;
|
||||
}
|
||||
if (m_State == 2)
|
||||
{
|
||||
if (!Stationary()) return;
|
||||
if (m_HeldAt < 0) { m_HeldAt = now; m_Donor.NextStatus = now; return; }
|
||||
ERR_HQWatchEntry report = m_HQ.Watch().Find(m_Donor.ForceSourceId);
|
||||
if (!ERR_InfantryResupplyRules.Fresh(report, now) || report.ObservedAt < m_HeldAt) return;
|
||||
if (vector.DistanceXZ(report.Position, m_Site.Definition.Position) > 150) { Abort("reported stopped truck outside camp service range"); return; }
|
||||
m_TruckPosition = report.Position + "3 0 0";
|
||||
m_Order = m_HQ.Downlink(m_Crew.Group, "DEPOT COURIER", string.Format("carry one existing item from camp to stopped truck at %1", m_TruckPosition), report.MessageId);
|
||||
if (!m_Order) { Abort("porter radio unavailable"); return; }
|
||||
m_State = 3; return;
|
||||
}
|
||||
if (m_State == 6)
|
||||
{
|
||||
if (vector.DistanceXZ(m_Worker.GetOrigin(), m_CachePosition) <= 5
|
||||
&& vector.DistanceXZ(m_Worker.GetOrigin(), m_Truck.Entity.GetOrigin()) >= 6) Cleanup();
|
||||
return;
|
||||
}
|
||||
if (!Stationary()) { Abort("held truck moved during physical loading"); return; }
|
||||
if (!m_Move)
|
||||
{
|
||||
IEntity source = m_Cache; IEntity destination = m_Worker;
|
||||
bool delivery = m_State == 5;
|
||||
if (delivery) { source = m_Worker; destination = m_Truck.Entity; }
|
||||
IEntity access = m_Cache; if (delivery) access = m_Truck.Entity;
|
||||
if (!ERR_GroundDepotAccess.Clear(access, m_Worker)) return;
|
||||
m_Move = new ERR_GroundDepotTransfer(this, source, destination, m_Item, m_Reservation, delivery, m_MineCargo);
|
||||
}
|
||||
int moved = m_Move.Tick(now);
|
||||
if (moved == 0) return;
|
||||
if (moved < 0) { Abort("native depot move refused, changed or expired; no submitted retry"); return; }
|
||||
if (m_State == 4)
|
||||
{
|
||||
m_PickedUp = true; m_Move = null; // Retire first-leg proof before custody changes again.
|
||||
if (!Walk(m_TruckPosition)) { Abort("porter cannot walk to reported truck"); return; }
|
||||
m_State = 5; return;
|
||||
}
|
||||
bool admitted;
|
||||
if (m_MineCargo) admitted = m_Service.AdmitDepotMine(this, m_Item);
|
||||
else admitted = m_Service.AdmitDepot(this, m_Item);
|
||||
if (!admitted) { Abort("proved physical delivery could not enter finite truck allowance"); return; }
|
||||
m_Imported = true; m_Delivered++; m_Move = null;
|
||||
Note("one original camp item physically delivered and admitted; porter returning to camp");
|
||||
if (!Walk(m_CachePosition + "2 0 0")) { Cleanup(); return; }
|
||||
m_State = 6;
|
||||
}
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
class ERR_GroundDepotCallback : ScriptedInventoryOperationCallback
|
||||
{
|
||||
ERR_GroundDepotTransfer Transfer;
|
||||
protected override void OnComplete() { if (Transfer) Transfer.Acknowledge(true); }
|
||||
protected override void OnAllComplete() { if (Transfer) Transfer.Acknowledge(true); }
|
||||
protected override void OnFailed() { if (Transfer) Transfer.Acknowledge(false); }
|
||||
protected override void OnAllFailed() { if (Transfer) Transfer.Acknowledge(false); }
|
||||
}
|
||||
|
||||
// One exact native move. Its owner retains the leaf until uncertain custody
|
||||
// settles; pickup and delivery never share callback state.
|
||||
class ERR_GroundDepotTransfer
|
||||
{
|
||||
protected ERR_GroundDepotLoading m_Owner;
|
||||
protected IEntity m_From;
|
||||
protected IEntity m_To;
|
||||
protected IEntity m_Item;
|
||||
protected InventoryStorageManagerComponent m_Source;
|
||||
protected InventoryStorageManagerComponent m_Destination;
|
||||
protected InventoryStorageSlot m_FromSlot;
|
||||
protected BaseInventoryStorageComponent m_FromStorage;
|
||||
protected BaseInventoryStorageComponent m_ToStorage;
|
||||
protected ref ERR_FiniteMagazineReservation m_Reservation;
|
||||
protected ref ERR_GroundDepotCallback m_Callback;
|
||||
protected bool m_Delivery;
|
||||
protected bool m_MineCargo;
|
||||
protected bool m_Submitted;
|
||||
protected int m_Ack;
|
||||
protected int m_State;
|
||||
protected int m_Rounds;
|
||||
protected int m_Capacity;
|
||||
protected float m_Started = -1;
|
||||
protected float m_Last = -1;
|
||||
void ERR_GroundDepotTransfer(ERR_GroundDepotLoading owner, IEntity source, IEntity destination, IEntity item, ERR_FiniteMagazineReservation reservation, bool delivery, bool mineCargo = false)
|
||||
{
|
||||
m_Owner = owner; m_From = source; m_To = destination; m_Item = item; m_Reservation = reservation; m_Delivery = delivery; m_MineCargo = mineCargo;
|
||||
if (!source || !destination || !item) return;
|
||||
m_Source = InventoryStorageManagerComponent.Cast(source.FindComponent(InventoryStorageManagerComponent));
|
||||
m_Destination = InventoryStorageManagerComponent.Cast(destination.FindComponent(InventoryStorageManagerComponent));
|
||||
InventoryItemComponent inventoryItem = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
|
||||
if (inventoryItem) m_FromSlot = inventoryItem.GetParentSlot();
|
||||
if (m_FromSlot) m_FromStorage = m_FromSlot.GetStorage();
|
||||
BaseMagazineComponent magazine = BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent));
|
||||
if (magazine) { m_Rounds = magazine.GetAmmoCount(); m_Capacity = magazine.GetMaxAmmoCount(); }
|
||||
}
|
||||
static bool Magazine(IEntity item)
|
||||
{
|
||||
if (!item) return false;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(item.FindComponent(DamageManagerComponent));
|
||||
if (damage && damage.IsDestroyed()) return false;
|
||||
BaseMagazineComponent magazine = BaseMagazineComponent.Cast(item.FindComponent(BaseMagazineComponent));
|
||||
return magazine && !magazine.IsUsed() && magazine.GetMagazineWell() && magazine.GetAmmoCount() > 0
|
||||
&& magazine.GetAmmoCount() <= magazine.GetMaxAmmoCount() && magazine.GetMaxAmmoCount() <= 100000;
|
||||
}
|
||||
static bool Cargo(IEntity item, bool mineCargo = false)
|
||||
{
|
||||
if (mineCargo) return ERR_InfantryMineTransfer.ReadyMine(item);
|
||||
return Magazine(item);
|
||||
}
|
||||
protected bool CargoUnchanged()
|
||||
{
|
||||
if (!Cargo(m_Item, m_MineCargo)) return false;
|
||||
if (m_MineCargo) return true;
|
||||
BaseMagazineComponent magazine = BaseMagazineComponent.Cast(m_Item.FindComponent(BaseMagazineComponent));
|
||||
return magazine.GetAmmoCount() == m_Rounds && magazine.GetMaxAmmoCount() == m_Capacity;
|
||||
}
|
||||
bool MineCargo() { return m_MineCargo; }
|
||||
bool Submitted() { return m_Submitted; }
|
||||
bool Delivery() { return m_Delivery; }
|
||||
IEntity Item() { return m_Item; }
|
||||
void Acknowledge(bool success)
|
||||
{
|
||||
if (!m_Submitted) return;
|
||||
if (!success) m_Ack = -1;
|
||||
else if (m_Ack == 0) m_Ack = 1;
|
||||
}
|
||||
protected bool StorageManaged(InventoryStorageManagerComponent inventory, BaseInventoryStorageComponent storage, IEntity owner)
|
||||
{
|
||||
if (!inventory || !storage || !owner || inventory.GetOwner() != owner) return false;
|
||||
if (!ERR_InfantryAmmoTransfer.StoredUnder(storage.GetOwner(), owner)) return false;
|
||||
array<BaseInventoryStorageComponent> storages = {}; inventory.GetStorages(storages, EStoragePurpose.PURPOSE_DEPOSIT);
|
||||
return storages.Count() <= 64 && storages.Contains(storage);
|
||||
}
|
||||
protected int Contains(InventoryStorageManagerComponent inventory)
|
||||
{
|
||||
if (!inventory || !m_Item) return -1;
|
||||
array<IEntity> items = {}; inventory.GetItems(items, EStoragePurpose.PURPOSE_DEPOSIT);
|
||||
if (items.Count() > 128) return -1;
|
||||
if (items.Contains(m_Item)) return 1;
|
||||
return 0;
|
||||
}
|
||||
protected bool Slot(InventoryStorageSlot slot)
|
||||
{
|
||||
if (!m_Item || !slot || slot.GetAttachedEntity() != m_Item) return false;
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(m_Item.FindComponent(InventoryItemComponent));
|
||||
return item && item.GetParentSlot() == slot;
|
||||
}
|
||||
protected bool OriginalProof()
|
||||
{
|
||||
return m_FromSlot && m_FromSlot.GetStorage() == m_FromStorage && StorageManaged(m_Source, m_FromStorage, m_From)
|
||||
&& Slot(m_FromSlot) && m_Destination && m_Destination.GetOwner() == m_To
|
||||
&& Contains(m_Source) == 1 && Contains(m_Destination) == 0;
|
||||
}
|
||||
bool PhysicalProof()
|
||||
{
|
||||
if (!m_Item || !m_Source || !m_From || m_Source.GetOwner() != m_From) return false;
|
||||
if (!StorageManaged(m_Destination, m_ToStorage, m_To) || Contains(m_Source) != 0 || Contains(m_Destination) != 1) return false;
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(m_Item.FindComponent(InventoryItemComponent));
|
||||
return item && item.GetParentSlot() && item.GetParentSlot().GetStorage() == m_ToStorage && Slot(item.GetParentSlot());
|
||||
}
|
||||
bool Unresolved()
|
||||
{
|
||||
if (!m_Submitted || !m_Item) return false;
|
||||
if (m_Ack == 1 && PhysicalProof()) return false;
|
||||
if (m_Ack < 0 && OriginalProof()) return false;
|
||||
return true;
|
||||
}
|
||||
void Stop() { if (m_State == 0) m_State = -1; }
|
||||
bool Confirmed() { return m_State == 1 && m_Ack == 1 && CargoUnchanged() && PhysicalProof(); }
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_State != 0) return m_State;
|
||||
if (!ERR_ForceRules.Finite(now) || now < 0 || now < m_Last) { Stop(); return m_State; }
|
||||
if (m_Started < 0) m_Started = now;
|
||||
m_Last = now;
|
||||
if (now - m_Started >= 8000 || !m_Owner || !m_Owner.TransferValid(this) || !CargoUnchanged()) { Stop(); return m_State; }
|
||||
if (!m_From || !m_To || m_From.FindComponent(InventoryStorageManagerComponent) != m_Source
|
||||
|| m_To.FindComponent(InventoryStorageManagerComponent) != m_Destination) { Stop(); return m_State; }
|
||||
if (!m_Submitted)
|
||||
{
|
||||
if (!m_Reservation || !OriginalProof()) { Stop(); return m_State; }
|
||||
m_ToStorage = m_Destination.FindStorageForItem(m_Item, EStoragePurpose.PURPOSE_DEPOSIT);
|
||||
if (!StorageManaged(m_Destination, m_ToStorage, m_To) || !m_Source.CanMoveItemToStorage(m_Item, m_ToStorage, -1)) { Stop(); return m_State; }
|
||||
if (!m_Delivery && !m_Reservation.Commit()) { Stop(); return m_State; }
|
||||
if (!m_Reservation.CommittedFor(m_Item)) { Stop(); return m_State; }
|
||||
if (m_Delivery && !m_Owner.ClaimDelivery(this)) { Stop(); return m_State; }
|
||||
m_Callback = new ERR_GroundDepotCallback(); m_Callback.Transfer = this;
|
||||
m_Submitted = true;
|
||||
bool accepted = m_Source.TryMoveItemToStorage(m_Item, m_ToStorage, -1, m_Callback);
|
||||
if (!accepted && m_Ack != 1) { Stop(); return m_State; }
|
||||
}
|
||||
// Native callbacks may synchronously cancel the owner or this exact leaf.
|
||||
if (m_State != 0) return m_State;
|
||||
if (!m_Owner || !m_Owner.TransferValid(this) || !CargoUnchanged()) { Stop(); return m_State; }
|
||||
if (m_Ack < 0) { Stop(); return m_State; }
|
||||
if (m_Ack == 1 && PhysicalProof()) m_State = 1;
|
||||
return m_State;
|
||||
}
|
||||
}
|
||||
+592
@@ -0,0 +1,592 @@
|
||||
// HQ-owned ground rendezvous using delivered positions and existing finite cargo.
|
||||
class ERR_InfantryResupplyRules
|
||||
{
|
||||
static bool Fresh(ERR_HQWatchEntry entry, float now)
|
||||
{
|
||||
return entry && !entry.Closed && entry.Count > 0 && entry.Fit > 0
|
||||
&& ERR_ForceRules.Finite(now) && entry.ObservedAt >= 0 && now >= entry.ObservedAt
|
||||
&& now - entry.ObservedAt <= 120000 && ERR_ForceRules.FiniteVector(entry.Position);
|
||||
}
|
||||
static bool Need(ERR_HQWatchEntry entry, float now)
|
||||
{
|
||||
return Fresh(entry, now) && entry.AmmoState && entry.AmmoState.Valid(entry.Count)
|
||||
&& entry.AmmoState.Complete && entry.AmmoState.Members == entry.Count
|
||||
&& entry.AmmoState.SmallArmsMuzzles > 0
|
||||
&& entry.AmmoState.SmallArmsRounds + entry.AmmoState.SmallArmsChambers <= entry.Count * 30;
|
||||
}
|
||||
// Only positively sampled reloadable magazine-well muzzles establish launcher demand.
|
||||
static bool LauncherNeed(ERR_HQWatchEntry entry, float now)
|
||||
{
|
||||
return Fresh(entry, now) && entry.AmmoState && entry.AmmoState.Valid(entry.Count)
|
||||
&& entry.AmmoState.LauncherComplete && entry.AmmoState.Members == entry.Count
|
||||
&& entry.AmmoState.LauncherMuzzles > 0
|
||||
&& entry.AmmoState.LauncherRounds == 0 && entry.AmmoState.LauncherChambers == 0;
|
||||
}
|
||||
static bool LauncherPair(ERR_HQWatchEntry recipient, ERR_HQWatchEntry donor, float now)
|
||||
{
|
||||
return LauncherNeed(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
|
||||
&& vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
|
||||
}
|
||||
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)
|
||||
{
|
||||
return Need(recipient, now) && Fresh(donor, now) && recipient.SourceId != donor.SourceId
|
||||
&& vector.DistanceXZ(recipient.Position, donor.Position) <= 200;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_InfantryResupply
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ConvoyTask m_Convoy;
|
||||
protected int m_ConvoySlot = -1;
|
||||
protected ERR_ConvoyVehicle m_Truck;
|
||||
protected ERR_HQUnit m_Recipient;
|
||||
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 bool m_LauncherTrip;
|
||||
protected int m_MineRecipientSlot = -1;
|
||||
protected bool m_EmptyMineReplacement;
|
||||
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, 7 unresolved item custody
|
||||
protected int m_Trip;
|
||||
protected int m_Token;
|
||||
protected int m_ReportId;
|
||||
protected int m_Delivered;
|
||||
protected int m_TripDelivered;
|
||||
protected float m_Deadline;
|
||||
protected float m_StockDeadline = -1;
|
||||
protected float m_HeldAt;
|
||||
protected float m_NextTick;
|
||||
protected bool m_Stopped;
|
||||
protected string m_Reason;
|
||||
void ERR_InfantryResupply(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq; m_Mode = mode; }
|
||||
// Bind before startup; every coordinator retains its exact finite convoy identity.
|
||||
bool Bind(ERR_ConvoyTask task, int slot, map<string, int> consumed, map<string, float> nextAttempt)
|
||||
{
|
||||
if (!task || !consumed || !nextAttempt || !m_HQ || !m_HQ.Convoys() || slot < 0 || slot >= 20
|
||||
|| slot >= m_HQ.Convoys().Count() || m_HQ.Convoys().At(slot) != task) return false;
|
||||
if (m_Stopped || m_Convoy || m_ConvoySlot >= 0 || m_State != 0 || m_Trip != 0 || m_NextTick != 0
|
||||
|| m_StockDeadline >= 0 || !m_Stock.IsEmpty() || m_Move || m_Truck) return false;
|
||||
m_Convoy = task; m_ConvoySlot = slot; m_Consumed = consumed; m_NextAttempt = nextAttempt;
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyTask Task() { return m_Convoy; }
|
||||
int State()
|
||||
{
|
||||
if (m_Mounted && m_Mounted.Busy()) return 6;
|
||||
return m_State;
|
||||
}
|
||||
ERR_MountedResupply Mounted() { return m_Mounted; }
|
||||
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_Move && !m_Move.Unresolved() && (m_State <= 1 || (m_State == 7 && m_LauncherTrip)))
|
||||
{
|
||||
m_Move = null;
|
||||
if (m_State == 7) Finish("launcher custody settled; original claims released without retry or refund");
|
||||
}
|
||||
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(); }
|
||||
// Admission uses the existing array object so an already-created mounted
|
||||
// coordinator observes this exact finite addition without a replacement ledger.
|
||||
protected int m_DepotImports;
|
||||
protected int m_DepotMineImports;
|
||||
bool DepotCurrent()
|
||||
{
|
||||
return Current() && m_State == 1 && !m_Move && !m_MineMove && !Unresolved()
|
||||
&& (!m_Mounted || !m_Mounted.Busy()) && TruckValid(true);
|
||||
}
|
||||
bool DepotAvailable() { return DepotCurrent() && m_Stock.Count() < 96 && m_Spent.Count() < 96; }
|
||||
bool DepotNeeded()
|
||||
{
|
||||
if (!DepotAvailable()) return false;
|
||||
int committed;
|
||||
foreach (IEntity item : m_Spent)
|
||||
if (item && m_Stock.Contains(item)) committed++;
|
||||
return committed > m_DepotImports;
|
||||
}
|
||||
// Mine imports replace only proved outgoing original mine commitments.
|
||||
// Keep the startup batch binding and both lifetime identity/spent ceilings.
|
||||
bool DepotMineAvailable()
|
||||
{
|
||||
return DepotCurrent() && CaptureMines() && m_MineStock.Count() < 96 && m_Spent.Count() < 96;
|
||||
}
|
||||
bool DepotMineNeeded()
|
||||
{
|
||||
if (!DepotMineAvailable()) return false;
|
||||
int committed;
|
||||
foreach (IEntity item : m_Spent)
|
||||
if (item && m_MineStock.Contains(item)) committed++;
|
||||
return committed > m_DepotMineImports;
|
||||
}
|
||||
bool AdmitDepotMine(ERR_GroundDepotLoading job, IEntity item)
|
||||
{
|
||||
if (!job || !DepotMineNeeded() || !ERR_InfantryMineTransfer.ReadyMine(item)
|
||||
|| m_MineStock.Contains(item) || m_Stock.Contains(item) || m_Spent.Contains(item)) return false;
|
||||
if (!job.DeliveredTo(this, item, true) || !Contains(m_Truck.Entity, item)
|
||||
|| !ERR_InfantryAmmoTransfer.StoredUnder(item, m_Truck.Entity)) return false;
|
||||
InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));
|
||||
if (!carried || !carried.GetParentSlot() || carried.GetParentSlot().GetAttachedEntity() != item) return false;
|
||||
m_MineStock.Insert(item); m_DepotMineImports++;
|
||||
Audit("one exact finite camp PMN4 admitted after proved physical courier delivery");
|
||||
return true;
|
||||
}
|
||||
ERR_ConvoyVehicle DepotTruck()
|
||||
{
|
||||
if (!DepotCurrent()) return null;
|
||||
return m_Truck;
|
||||
}
|
||||
bool AdmitDepot(ERR_GroundDepotLoading job, IEntity item)
|
||||
{
|
||||
if (!job || !DepotAvailable() || !item || m_Stock.Contains(item) || m_Spent.Contains(item)) return false;
|
||||
if (!job.DeliveredTo(this, item) || !Contains(m_Truck.Entity, item)) return false;
|
||||
m_Stock.Insert(item); m_DepotImports++; Audit("one exact finite camp magazine admitted after proved physical courier delivery");
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void Audit(string reason)
|
||||
{
|
||||
if (m_MineTrip) reason = "mine top-up: " + reason;
|
||||
if (m_LauncherTrip) reason = "depleted launcher magazine: " + 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);
|
||||
}
|
||||
protected bool Current()
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active();
|
||||
}
|
||||
protected bool Contains(IEntity owner, IEntity item)
|
||||
{
|
||||
if (!owner || !item) return false;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(owner.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return false;
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
return items.Count() <= 128 && items.Contains(item);
|
||||
}
|
||||
protected bool ConvoyCurrent()
|
||||
{
|
||||
if (!m_HQ || !m_Convoy) return false;
|
||||
if (m_ConvoySlot < 0) return m_HQ.ConvoyTask() == m_Convoy;
|
||||
return m_HQ.Convoys() && m_ConvoySlot < m_HQ.Convoys().Count() && m_HQ.Convoys().At(m_ConvoySlot) == m_Convoy;
|
||||
}
|
||||
protected bool TruckValid(bool allowArrived = false)
|
||||
{
|
||||
if (!m_Convoy || m_Convoy.Stopped || !m_Convoy.Ready || !m_Convoy.Receipt || !m_Convoy.Receipt.Consumed
|
||||
|| !ConvoyCurrent() || m_Convoy.Vehicles.Count() != 6 || m_Convoy.Vehicles[1] != m_Truck) return false;
|
||||
if (!m_Truck || m_Truck.Index != 1 || !m_Truck.Registered || m_Truck.Released || (m_Truck.Arrived && !allowArrived)
|
||||
|| !m_Truck.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(m_Truck.Entity, m_Truck.Group)) return false;
|
||||
ERR_ForceRecord record = m_Mode.Forces().FindGroup(m_Truck.Group);
|
||||
return record && record.Physical && !record.Eliminated && record.Kind == ERR_ForceKind.CONVOY
|
||||
&& m_Convoy.Receipt.Records.Count() >= 6 && record.Id == m_Convoy.Receipt.Records[1];
|
||||
}
|
||||
protected bool Quiet(ERR_HQUnit unit)
|
||||
{
|
||||
return unit && unit.Group && unit.Utility && !unit.Group.IsSlave() && unit.Group.GetPlayerCount(true) == 0
|
||||
&& unit.Group.GetFaction() == m_Mode.EnemyFaction()
|
||||
&& (!unit.Utility.m_Perception || unit.Utility.m_Perception.m_aTargets.IsEmpty());
|
||||
}
|
||||
protected bool Roster()
|
||||
{
|
||||
if (!m_Recipient || !Quiet(m_Recipient) || !m_Recipient.StatusSampler.OwnsRoster(m_Recipient.Group)) return false;
|
||||
int strength = m_Recipient.StatusSampler.InitialStrength();
|
||||
if (strength < 1 || strength > 32) return false;
|
||||
array<AIAgent> agents = {}; m_Recipient.Group.GetAgents(agents);
|
||||
if (agents.IsEmpty() || agents.Count() > 32) return false;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (!agent || !m_Members.Contains(agent.GetControlledEntity())) return false;
|
||||
bool living;
|
||||
foreach (IEntity actor : m_Members)
|
||||
{
|
||||
if (!actor || !ERR_Actors.Alive(actor)) continue;
|
||||
// Returning to the group cannot undo a sampler's permanent release of an
|
||||
// original slot after transfer or player control during this trip.
|
||||
bool attributed = false;
|
||||
for (int slot = 0; slot < strength; slot++)
|
||||
{
|
||||
if (m_Recipient.StatusSampler.InitialMember(slot) != actor) continue;
|
||||
attributed = m_Recipient.StatusSampler.CanAttribute(slot, actor);
|
||||
break;
|
||||
}
|
||||
if (!attributed) return false;
|
||||
if (ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != m_Recipient.Group
|
||||
|| SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction() || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
|
||||
if (ERR_Actors.CanFight(actor)) living = true;
|
||||
}
|
||||
return living;
|
||||
}
|
||||
protected void Stock(float now)
|
||||
{
|
||||
if (!m_Convoy) m_Convoy = m_HQ.ConvoyTask();
|
||||
if (m_Convoy && !ConvoyCurrent()) { Stop(); return; }
|
||||
if (!m_Convoy || !m_Convoy.Ready || m_Convoy.Vehicles.Count() != 6) return;
|
||||
m_Truck = m_Convoy.Vehicles[1];
|
||||
if (!TruckValid()) { Stop(); return; }
|
||||
// Cargo has a later, separately bounded lifecycle than native crew admission.
|
||||
// Supply convoys first request faction weapons, then their matching magazines.
|
||||
if (m_StockDeadline < 0) m_StockDeadline = now + 90000;
|
||||
if (now >= m_StockDeadline) { Stop(); return; }
|
||||
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));
|
||||
if (!inventory) { Stop(); return; }
|
||||
array<IEntity> items = {}; ERR_Inventory.Items(inventory, items);
|
||||
if (items.Count() > 128) { Stop(); return; }
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.FindComponent(BaseMagazineComponent)) continue;
|
||||
if (m_Stock.Count() >= 96) { Stop(); return; }
|
||||
m_Stock.Insert(item);
|
||||
}
|
||||
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, bool mineTrip = false, bool launcherTrip = false)
|
||||
{
|
||||
if (!Current() || m_State != 1 || m_Move || m_MineMove || (m_Mounted && m_Mounted.Busy()) || m_Trip >= 128 || !TruckValid()) return false;
|
||||
if ((mineTrip && launcherTrip) || 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 (mineTrip)
|
||||
{
|
||||
if (!ERR_InfantryResupplyRules.MinePair(demand, donorStatus, now)) return false;
|
||||
// Empty reports authorize only an original startup-designated engineer.
|
||||
// This reads retained entitlement, never live positions/inventory for planning.
|
||||
if (demand.AmmoState.ReadyMines == 0)
|
||||
{
|
||||
bool designated;
|
||||
for (int member = 0; member < recipient.StatusSampler.InitialStrength() && member < 32; member++)
|
||||
{
|
||||
IEntity original = recipient.StatusSampler.InitialMember(member);
|
||||
if (original && recipient.StatusSampler.CanAttribute(member, original)
|
||||
&& m_HQ.MineSupplyReady(recipient.Group, original, true)) { designated = true; break; }
|
||||
}
|
||||
if (!designated) return false;
|
||||
}
|
||||
}
|
||||
else if (launcherTrip)
|
||||
{
|
||||
if (!ERR_InfantryResupplyRules.LauncherPair(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;
|
||||
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 if (launcherTrip) available = LauncherStock(recipient);
|
||||
else available = CompatibleStock(recipient, false);
|
||||
if (m_Spent.Count() >= 96) return false;
|
||||
if (!available) return false;
|
||||
m_Members.Clear();
|
||||
for (int i = 0; i < recipient.StatusSampler.InitialStrength(); i++)
|
||||
{
|
||||
IEntity actor = recipient.StatusSampler.InitialMember(i);
|
||||
if (actor && recipient.StatusSampler.CanAttribute(i, actor)) m_Members.Insert(actor);
|
||||
}
|
||||
m_Recipient = recipient; m_Donor = donor;
|
||||
if (!Roster()) { m_Recipient = null; m_Donor = null; return false; }
|
||||
// Disjoint bounded token range from the existing mortar courier's trip IDs.
|
||||
int slot = m_ConvoySlot;
|
||||
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_LauncherTrip = launcherTrip; m_MineRecipientSlot = -1; m_Receiving = null; m_EmptyMineReplacement = false;
|
||||
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;
|
||||
string purpose = "stop existing ammo truck and report position";
|
||||
if (mineTrip) purpose += "; one finite PMN4 top-up for an existing carrier";
|
||||
if (launcherTrip) purpose += "; finite compatible magazine for a reported depleted launcher";
|
||||
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;
|
||||
}
|
||||
// Compatibility-only preflight of original own carriers; HQ still uses the
|
||||
// delivered report for demand and positions. No item is reserved or moved here.
|
||||
protected bool CompatibleStock(ERR_HQUnit recipient, bool launcherMagazine)
|
||||
{
|
||||
if (!recipient || !recipient.StatusSampler || m_Stock.Count() > 96) return false;
|
||||
int strength = recipient.StatusSampler.InitialStrength();
|
||||
if (strength < 1 || strength > 32) return false;
|
||||
int checks;
|
||||
foreach (IEntity magazine : m_Stock)
|
||||
{
|
||||
if (!magazine || m_Spent.Contains(magazine) || !Contains(m_Truck.Entity, magazine)) continue;
|
||||
for (int slot = 0; slot < strength; slot++)
|
||||
{
|
||||
if (++checks > 96 * 32) return false;
|
||||
IEntity actor = recipient.StatusSampler.InitialMember(slot);
|
||||
if (!actor || !recipient.StatusSampler.CanAttribute(slot, actor) || !ERR_Actors.CanFight(actor)) continue;
|
||||
if (ERR_InfantryAmmoTransfer.Compatible(actor, magazine, launcherMagazine)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool LauncherStock(ERR_HQUnit recipient)
|
||||
{
|
||||
return CompatibleStock(recipient, true);
|
||||
}
|
||||
bool TryPlanLauncher(ERR_HQUnit recipient, ERR_HQUnit donor, ERR_HQWatchEntry demand, ERR_HQWatchEntry donorStatus, float now)
|
||||
{
|
||||
return TryPlan(recipient, donor, demand, donorStatus, now, false, 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_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);
|
||||
}
|
||||
protected bool Waypoints()
|
||||
{
|
||||
m_MoveWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), ERR_Actors.Ground(m_Rendezvous)));
|
||||
m_HoldWaypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), ERR_Actors.Ground(m_Rendezvous)));
|
||||
if (!m_MoveWaypoint || !m_HoldWaypoint) return false;
|
||||
// The reported rendezvous is 3 m from the donor report. A 2 m move
|
||||
// radius allowed arrival 5 m away, beyond the transfer's physical reach.
|
||||
m_MoveWaypoint.SetCompletionRadius(0.5); m_HoldWaypoint.SetCompletionRadius(0.5);
|
||||
m_Recipient.Group.AddWaypointAt(m_HoldWaypoint, 0); m_Recipient.Group.AddWaypointAt(m_MoveWaypoint, 0);
|
||||
m_Recipient.Group.ActivateAI(); return true;
|
||||
}
|
||||
protected void ClearWaypoints()
|
||||
{
|
||||
if (m_MoveWaypoint) { if (m_Recipient && m_Recipient.Group) m_Recipient.Group.RemoveWaypoint(m_MoveWaypoint); ERR_Actors.DeleteSpawned(m_MoveWaypoint); }
|
||||
if (m_HoldWaypoint) { if (m_Recipient && m_Recipient.Group) m_Recipient.Group.RemoveWaypoint(m_HoldWaypoint); ERR_Actors.DeleteSpawned(m_HoldWaypoint); }
|
||||
m_MoveWaypoint = null; m_HoldWaypoint = null;
|
||||
}
|
||||
protected void Finish(string reason)
|
||||
{
|
||||
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_LauncherTrip && m_Move && m_Move.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_MineTrip = false; m_LauncherTrip = 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;
|
||||
IEntity carried;
|
||||
int count = ERR_InfantryMineTransfer.CarriedCount(actor, m_Recipient.Group, carried);
|
||||
if (count != 1 && (count != 0 || !m_HQ.MineSupplyReady(m_Recipient.Group, actor, true))) continue;
|
||||
m_EmptyMineReplacement = count == 0;
|
||||
m_Receiving = actor; m_MineRecipientSlot = slot; break;
|
||||
}
|
||||
if (!m_Receiving) return;
|
||||
}
|
||||
if (!m_HQ.MineSupplyReady(m_Recipient.Group, m_Receiving, m_EmptyMineReplacement)) return;
|
||||
IEntity carried;
|
||||
int count = ERR_InfantryMineTransfer.CarriedCount(m_Receiving, m_Recipient.Group, carried);
|
||||
if (!m_Recipient.StatusSampler.CanAttribute(m_MineRecipientSlot, m_Receiving, false)
|
||||
|| (m_EmptyMineReplacement && count != 0) || (!m_EmptyMineReplacement && count != 1))
|
||||
{ Finish("original mine recipient lost or its pinned zero/one stock changed"); 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_EmptyMineReplacement);
|
||||
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)
|
||||
{
|
||||
if (m_Move)
|
||||
{
|
||||
int result = m_Move.Tick(now);
|
||||
if (result == 0) return;
|
||||
if (result < 0) { Finish("native transfer rejected, expired or lost authority; no item retry"); return; }
|
||||
m_Move = null; m_Delivered++; m_TripDelivered++; m_Served.Insert(m_Receiving); m_Receiving = null;
|
||||
Audit("exact finite magazine confirmed in original soldier inventory");
|
||||
if (m_TripDelivered >= 6) { Finish("six-magazine trip allowance reached"); return; }
|
||||
}
|
||||
foreach (IEntity actor : m_Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(actor) || m_Served.Contains(actor) || vector.DistanceXZ(actor.GetOrigin(), m_Truck.Entity.GetOrigin()) > 4) continue;
|
||||
ERR_HQAmmoSnapshot local = new ERR_HQAmmoSnapshot(); local.Complete = true; local.LauncherComplete = true;
|
||||
ERR_HQAmmoSampler.Inspect(actor, m_Recipient.Group, local);
|
||||
if (m_LauncherTrip)
|
||||
{
|
||||
if (!local.Valid(1) || !local.LauncherComplete || local.Members != 1 || local.LauncherMuzzles <= 0
|
||||
|| local.LauncherRounds != 0 || local.LauncherChambers != 0) continue;
|
||||
}
|
||||
else if (!local.Complete || local.SmallArmsMuzzles <= 0 || local.SmallArmsRounds + local.SmallArmsChambers > 30) continue;
|
||||
foreach (IEntity magazine : m_Stock)
|
||||
{
|
||||
if (!magazine || m_Spent.Contains(magazine) || !Contains(m_Truck.Entity, magazine) || !ERR_InfantryAmmoTransfer.Compatible(actor, magazine, m_LauncherTrip)) continue;
|
||||
ERR_FiniteMagazineReservation reservation = new ERR_FiniteMagazineReservation(m_Stock, m_Spent, magazine);
|
||||
if (!reservation.Reserve()) continue;
|
||||
m_Receiving = actor;
|
||||
m_Move = new ERR_InfantryAmmoTransfer(m_Mode, m_Truck.Entity, m_Truck.Group, actor, m_Recipient.Group, magazine, !m_LauncherTrip, m_LauncherTrip);
|
||||
m_Move.Tick(now);
|
||||
if (m_Move.Submitted()) { reservation.Commit(); return; }
|
||||
// No native operation exists yet: approach geometry, stopped state or
|
||||
// capacity may improve while the same bounded rendezvous remains valid.
|
||||
// Keep every submitted identity spent, including a synchronous rejection.
|
||||
reservation.ReleaseUnsubmitted(); m_Move = null; m_Receiving = null;
|
||||
break; // At most one unsubmitted preflight per member on this slow tick.
|
||||
}
|
||||
}
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!ERR_ForceRules.Finite(now) || now < 0) return;
|
||||
// Stopped services retain uncertain leaf refs until exact late proof settles.
|
||||
if (m_Stopped) { PollTransfers(now); return; }
|
||||
if (!Current() || now < m_NextTick) return;
|
||||
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)
|
||||
{
|
||||
if (m_Move && !m_Move.Unresolved()) m_Move = null;
|
||||
return;
|
||||
}
|
||||
if (now >= m_Deadline || !TruckValid() || !Quiet(m_Donor) || !Roster() || m_Truck.Combat.Held
|
||||
|| m_HQ.LogisticsToken(m_Recipient.Group) != m_Token)
|
||||
{ Finish("rendezvous expired, contact appeared or original ownership changed"); return; }
|
||||
if (m_Truck.SupplyToken != m_Token) { Finish("truck hold authority lost"); return; }
|
||||
if (m_State == 2)
|
||||
{
|
||||
int delivered = m_Order.Advance(now, m_HQ.Journal());
|
||||
if (delivered == 0) return;
|
||||
if (delivered < 0 || m_Truck.SupplyToken != m_Token) { Finish("truck hold downlink interrupted or reservation lost"); return; }
|
||||
m_Truck.SupplyHeld = true; m_Truck.ClearOrder(); m_HeldAt = -1;
|
||||
m_Order = null; m_State = 3; Audit("truck received stop; waiting for delivered stopped-position report"); return;
|
||||
}
|
||||
if (m_State == 3)
|
||||
{
|
||||
// The delivered hold asks the original crew to report only after physically stopping.
|
||||
if (m_HeldAt < 0)
|
||||
{
|
||||
Physics physics = m_Truck.Entity.GetPhysics();
|
||||
if (!physics) return;
|
||||
float speed = physics.GetVelocity().Length();
|
||||
float angular = physics.GetAngularVelocity().Length();
|
||||
if (!(speed >= 0 && speed <= 0.25 && angular >= 0 && angular <= 0.1)) return;
|
||||
m_HeldAt = now; m_Donor.NextStatus = now; return;
|
||||
}
|
||||
ERR_HQWatchEntry stopped = m_HQ.Watch().Find(m_Donor.ForceSourceId);
|
||||
if (!ERR_InfantryResupplyRules.Fresh(stopped, now) || stopped.ObservedAt < m_HeldAt) return;
|
||||
m_Rendezvous = stopped.Position + "3 0 0";
|
||||
m_Order = m_HQ.Downlink(m_Recipient.Group, "INFANTRY SUPPLY RENDEZVOUS", string.Format("walk to reported stopped ammo truck at %1", m_Rendezvous), m_ReportId);
|
||||
if (!m_Order) { Finish("infantry radio downlink unavailable"); return; }
|
||||
m_State = 4; return;
|
||||
}
|
||||
if (m_State == 4)
|
||||
{
|
||||
int delivered = m_Order.Advance(now, m_HQ.Journal());
|
||||
if (delivered == 0) return;
|
||||
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)
|
||||
{
|
||||
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 allowance=%2; %3", m_MinesDelivered, m_MineStock.Count(), m_Reason);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
if (m_Mounted) m_Mounted.Stop();
|
||||
Finish("service stopped; original forces and all remaining cargo retained"); m_Stopped = true;
|
||||
}
|
||||
}
|
||||
+1164
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
"""Actual depot porter selection/planner/state transitions and final finite import; native boundaries doubled."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
BASE=Path(os.environ.get('ERR_DEPOT_ROOT',ROOT))
|
||||
P=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c'
|
||||
S=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
source=P.read_text()
|
||||
valid=body(P,'protected bool Valid(')
|
||||
assert 'ERR_GroundDepotTransfer.Cargo(m_Item, m_MineCargo)' in valid
|
||||
assert 'm_Service.DepotCurrent()' in valid and 'DepotAvailable()' not in valid and 'DepotNeeded()' not in valid
|
||||
for forbidden in ('Teleport(', 'TrySpawn', 'SetAmmoCount', 'SetOrigin(', 'SetTransform('):assert forbidden not in source
|
||||
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('ref ','').replace('null','nullptr')
|
||||
for t in ('ERR_HQUnit','ERR_InfantryResupply','ERR_ConvoyVehicle','ERR_HQWatchEntry','ERR_SiteRuntime','ERR_GroundDepotTransfer','ERR_GroundDepotLoading','ERR_FiniteMagazineReservation','SCR_ChimeraAIAgent'):
|
||||
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||
s=s.replace('ERR_FiniteMagazineReservation(m_Original, m_Spent, item)','ERR_FiniteMagazineReservation(&m_Original, &m_Spent, item)')
|
||||
for ns in ('ERR_ForceRules','ERR_InfantryResupplyRules','ERR_GroundDepotTransfer','ERR_InfantryAmmoTransfer','ERR_GroundDepotAccess','ERR_Actors','ERR_EnemyEquipmentRules','CompartmentAccessComponent','ERR_InfantryMineTransfer','Math'):
|
||||
s=s.replace(ns+'.',ns+'::')
|
||||
for n in ('m_HQ','m_Mode','m_Service','m_Move','m_Reservation','m_Order','m_Truck','m_Crew','m_Donor','m_Worker','m_Site','m_Cache','service','truck','donor','report','site','crew','operation','actor','agent'):
|
||||
s=re.sub(r'\b'+n+r'\.',n+'->',s)
|
||||
s=s.replace('crew->StatusSampler.', 'crew->StatusSampler->').replace('agent->m_UtilityComponent.', 'agent->m_UtilityComponent->')
|
||||
s=s.replace('SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor))', 'actor->agent')
|
||||
s=s.replace('InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent))','item->carried').replace('InventoryItemComponent carried','InventoryItem* carried').replace('carried.GetParentSlot()', 'carried->GetParentSlot()').replace('GetParentSlot().','GetParentSlot()->')
|
||||
s=s.replace('Sites().','Sites()->').replace('Watch().','Watch()->').replace('Forces().','Forces()->')
|
||||
s=s.replace('site->Responders.Group','site->Responders->Group').replace('site->Definition.Position','site->Definition->Position').replace('m_Site->Definition.Position','m_Site->Definition->Position')
|
||||
s=s.replace('m_Truck->Entity.GetOrigin()', 'm_Truck->Entity->GetOrigin()')
|
||||
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('string.Format','Format')
|
||||
s=s.replace('InventoryStorageManagerComponent.Cast(m_Cache->FindComponent(InventoryStorageManagerComponent))','m_Cache->inventory')
|
||||
s=s.replace('"3 0 0"','Vec{3}').replace('"2 0 0"','Vec{2}')
|
||||
return s
|
||||
methods='\n'.join(cpp(body(P,x)) for x in ('bool MineCargo(', 'protected bool SelectWorker(', 'void Plan(', 'void Tick(', 'protected void Abort(', 'void Stop(', 'bool TransferValid(', 'bool ClaimDelivery(', 'bool DeliveredTo('))
|
||||
admit=(cpp(body(S,'bool AdmitDepot('))+cpp(body(S,'bool AdmitDepotMine('))).replace('job.','job->').replace('m_Stock.Insert(item); Audit(', 'm_Stock.Insert(item); Audit(')
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
using string=std::string;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}void RemoveItem(T v){this->erase(std::remove(this->begin(),this->end(),v),this->end());}};
|
||||
struct Vec{float x=0;Vec operator+(Vec b){return {x+b.x};}static float DistanceXZ(Vec a,Vec b){return std::abs(a.x-b.x);}};
|
||||
template<class...T>string Format(string s,T...){return s;}
|
||||
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}};
|
||||
struct Math{static float Min(float a,float b){return std::min(a,b);}};
|
||||
struct Entity;struct Slot{Entity*attached=nullptr;Entity*GetAttachedEntity(){return attached;}};struct InventoryItem{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};
|
||||
struct Group{int faction=1;};struct Inventory{};struct Utility{bool m_AIInfo=true;};struct SCR_ChimeraAIAgent{Utility*m_UtilityComponent=nullptr;};
|
||||
struct Entity{Vec pos;Inventory*inventory=nullptr;bool stock=true,compatible=true,alive=true,player=false,aboard=false,attributed=true,equipmentEligible=true,mine=false,armed=false,defaultArmed=false,collision=true;InventoryItem*carried=nullptr;Entity*parent=nullptr;int faction=1,well=1,rounds=30,maxRounds=30;Group*group=nullptr;SCR_ChimeraAIAgent*agent=nullptr;Vec GetOrigin(){return pos;}};using IEntity=Entity*;
|
||||
struct Sampler{Array<IEntity>members;Group*group=nullptr;bool owns=true;int InitialStrength(){return members.Count();}bool OwnsRoster(Group*g){return owns&&g==group;}IEntity InitialMember(int i){return members[i];}bool CanAttribute(int i,IEntity a,bool){return a&&members[i]==a&&a->attributed&&a->group==group&&!a->player&&a->faction==group->faction;}};
|
||||
struct ERR_Actors{static bool CanFight(IEntity a){return a&&a->alive;}};
|
||||
struct CompartmentAccessComponent{static bool GetVehicleIn(IEntity a){return a&&a->aboard;}};
|
||||
struct Definition{Vec Position;};struct ERR_SiteRuntime{struct Definition*Definition=nullptr;ERR_SiteRuntime*Responders=nullptr;struct Group*Group=nullptr;IEntity Container=nullptr;Array<IEntity>items;};
|
||||
struct Sites{Array<ERR_SiteRuntime*>sites;int Count(){return sites.Count();}ERR_SiteRuntime*At(int i){return sites[i];}int DepotItemCount(ERR_SiteRuntime*s){return s?s->items.Count():0;}IEntity DepotItem(ERR_SiteRuntime*s,int i){return s->items[i];}};
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;int ForceSourceId=0,LogisticsReservation=0;float NextStatus=0;bool eligible=true,HomeGuard=false,ReservedForSupport=false,Assignment=false,Task=false,PendingTask=false,PendingOrder=false,Downlink=false,Waypoint=false;Sampler*StatusSampler=nullptr;};
|
||||
struct ERR_ConvoyTask{};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;IEntity Entity=nullptr;int SupplyToken=0;bool SupplyHeld=false;int clears=0;void ClearOrder(){clears++;}};
|
||||
struct ERR_HQWatchEntry{int SourceId=2,MessageId=1;float ObservedAt=0;Vec Position;bool valid=true;};
|
||||
struct ERR_InfantryResupplyRules{static bool Fresh(ERR_HQWatchEntry*r,float n){return r&&r->valid&&n>=r->ObservedAt&&n-r->ObservedAt<=120000;}};
|
||||
struct Watch{ERR_HQWatchEntry report;ERR_HQWatchEntry*Find(int){return &report;}};
|
||||
struct Ledger{void*FindGroup(Group*){return this;}};
|
||||
struct Mode{struct Sites sites;Ledger ledger;struct Sites*Sites(){return &sites;}Ledger*Forces(){return &ledger;}};
|
||||
struct ERR_EnemyEquipmentRules{static bool Eligible(Mode*,Group*g,IEntity a){return a&&a->equipmentEligible&&a->group==g&&a->faction==g->faction&&!a->player&&a->alive;}};
|
||||
struct ERR_GroundDepotLoading;struct ERR_InfantryResupply;
|
||||
struct ERR_FiniteMagazineReservation{Array<IEntity>*original,*spent;IEntity item;bool committed=false,closed=false;ERR_FiniteMagazineReservation(Array<IEntity>*a,Array<IEntity>*b,IEntity i):original(a),spent(b),item(i){}bool Reserve(){if(!item||!original->Contains(item)||spent->Contains(item)||spent->Count()>=96)return false;spent->Insert(item);return true;}bool ReleaseUnsubmitted(){if(closed||committed)return false;closed=true;spent->RemoveItem(item);return true;}bool CommittedFor(IEntity i){return committed&&i==item&&spent->Contains(i);}};
|
||||
struct Route{int outcome=1,calls=0;int Advance(float,void*){calls++;return outcome;}};
|
||||
struct HQ{Array<ERR_InfantryResupply*>services;ERR_HQUnit*crew=nullptr,*donor=nullptr;struct Watch watch;Route route;bool claim=true,radio=true;int orders=0,releases=0,claims=0;
|
||||
int GroundSupplyCount(){return services.Count();}ERR_InfantryResupply*GroundSupplyAt(int i){return services[i];}ERR_HQUnit*GroundUnit(Group*g){return crew&&crew->Group==g?crew:donor&&donor->Group==g?donor:nullptr;}struct Watch*Watch(){return &watch;}void*Journal(){return this;}
|
||||
bool ReserveGroundDepot(ERR_HQUnit*,ERR_HQUnit*,ERR_InfantryResupply*,int,bool=false);
|
||||
void ReleaseGroundDepot(ERR_HQUnit*c,ERR_HQUnit*d,ERR_ConvoyVehicle*t,int token){releases++;if(c&&c->LogisticsReservation==token)c->LogisticsReservation=0;if(d&&d->LogisticsReservation==token)d->LogisticsReservation=0;if(t&&t->SupplyToken==token){t->SupplyToken=0;t->SupplyHeld=false;}}
|
||||
Route*Downlink(Group*,string,string,int){orders++;return radio?&route:nullptr;}
|
||||
};
|
||||
// Native compatibility boundary double: pair-specific carried weapon well,
|
||||
// supported weapon flag, and sane positive ammo. Real Compatible's full native
|
||||
// weapon/muzzle/type/ownership logic remains covered by the transfer/planning suites.
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity i,IEntity owner){return i&&i->parent==owner;}static bool Compatible(IEntity a,IEntity i){return a&&i&&i->compatible&&a->compatible&&a->well>0&&a->well==i->well&&i->rounds>0&&i->maxRounds>0&&i->rounds<=i->maxRounds;}};
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity i){return i&&i->mine&&i->alive&&!i->armed&&!i->defaultArmed&&i->collision;}};
|
||||
struct ERR_GroundDepotAccess{static inline bool clear=true;static bool Clear(IEntity,IEntity){return clear;}};
|
||||
struct ERR_GroundDepotTransfer{static inline int constructed=0;bool mineCargo=false,delivery=false,submitted=true,unknown=true,confirmed=false;int result=0;IEntity item;ERR_FiniteMagazineReservation*r;
|
||||
ERR_GroundDepotTransfer(ERR_GroundDepotLoading*,IEntity,IEntity,IEntity i,ERR_FiniteMagazineReservation*q,bool d,bool m=false):mineCargo(m),delivery(d),item(i),r(q){constructed++;r->committed=true;}
|
||||
static bool Magazine(IEntity i){return i&&i->stock&&!i->mine;}static bool Cargo(IEntity i,bool mine=false){return mine?ERR_InfantryMineTransfer::ReadyMine(i):Magazine(i);}bool MineCargo(){return mineCargo;}bool Delivery(){return delivery;}IEntity Item(){return item;}bool Unresolved(){return submitted&&unknown;}bool Confirmed(){return confirmed&&Cargo(item,mineCargo);}void Stop(){result=-1;}int Tick(float){return result;}
|
||||
};
|
||||
struct ERR_InfantryResupply{bool available=true,needed=true,mineNeeded=false;int m_DepotImports=0,m_DepotMineImports=0;ERR_ConvoyVehicle*truck=nullptr;ERR_ConvoyTask task;Array<IEntity>m_Stock,m_MineStock,m_Spent;ERR_ConvoyVehicle*m_Truck=nullptr;
|
||||
bool DepotAvailable(){return available&&m_Stock.Count()<96;}bool DepotNeeded(){return DepotAvailable()&&needed;}bool DepotMineNeeded(){return available&&mineNeeded&&m_MineStock.Count()<96&&m_DepotMineImports==0;}ERR_ConvoyVehicle*DepotTruck(){return available?truck:nullptr;}ERR_ConvoyTask*Task(){return &task;}bool Contains(IEntity,IEntity i){return i!=nullptr;}void Audit(string){}bool AdmitDepot(ERR_GroundDepotLoading*,IEntity);bool AdmitDepotMine(ERR_GroundDepotLoading*,IEntity);
|
||||
};
|
||||
struct ERR_GroundDepotLoading{HQ*m_HQ;Mode*m_Mode;Array<IEntity>m_Original,m_Spent;bool active=true,valid=true,stationary=true,canWalk=true,m_Stopped=false,m_Captured=false,m_PickedUp=false,m_DeliveryClaimed=false,m_Imported=false,m_MineCargo=false;int m_State=0,m_Attempts=0,m_Delivered=0,m_Token=0,m_WorkerSlot=0,walks=0,cleanups=0,notes=0;float m_NextPlan=0,m_Deadline=0,m_Last=-1,m_NextTick=0,m_HeldAt=-1;Vec m_CachePosition,m_TruckPosition;
|
||||
Utility*m_Utility=nullptr;ERR_HQUnit*m_Crew=nullptr,*m_Donor=nullptr;ERR_SiteRuntime*m_Site=nullptr;IEntity m_Worker=nullptr,m_Cache=nullptr,m_Item=nullptr;Inventory*m_CacheInventory=nullptr;void*m_CrewRecord=nullptr;ERR_InfantryResupply*m_Service=nullptr;ERR_ConvoyTask*m_Task=nullptr;ERR_ConvoyVehicle*m_Truck=nullptr;Route*m_Order=nullptr;ERR_GroundDepotTransfer*m_Move=nullptr;ERR_FiniteMagazineReservation*m_Reservation=nullptr;
|
||||
bool Active(){return active&&!m_Stopped;}bool Busy(){return m_State!=0;}bool Unresolved(){return m_Move&&m_Move->Unresolved();}bool Valid(){return Active()&&valid&&(!m_Service||m_Service->available)&&ERR_GroundDepotTransfer::Cargo(m_Item,m_MineCargo);}bool Stationary(){return stationary;}bool Quiet(ERR_HQUnit*u){return u&&u->eligible;}bool Stored(IEntity,IEntity){return true;}void Capture(){m_Captured=true;}void Note(string){notes++;}void ClearAction(){}bool Walk(Vec){walks++;return canWalk;}
|
||||
void Cleanup(){cleanups++;if(m_Reservation)m_Reservation->ReleaseUnsubmitted();m_HQ->ReleaseGroundDepot(m_Crew,m_Donor,m_Truck,m_Token);m_State=0;m_Token=0;m_Move=nullptr;m_Order=nullptr;}
|
||||
METHODS
|
||||
};
|
||||
ADMISSION
|
||||
bool HQ::ReserveGroundDepot(ERR_HQUnit*c,ERR_HQUnit*d,ERR_InfantryResupply*s,int token,bool mineCargo){if(!(mineCargo?s->DepotMineNeeded():s->DepotNeeded())||!claim||c->LogisticsReservation||d->LogisticsReservation||s->truck->SupplyToken)return false;claims++;c->LogisticsReservation=d->LogisticsReservation=s->truck->SupplyToken=token;return true;}
|
||||
struct F{Mode mode;HQ hq;ERR_GroundDepotLoading job;Group cg,dg;ERR_HQUnit crew,donor;Definition def;ERR_SiteRuntime site,responders;Entity worker,second,cache,truck,item;Inventory inv;Slot itemSlot;InventoryItem carriedItem;Utility workerUtility,secondUtility;SCR_ChimeraAIAgent workerAgent,secondAgent;Sampler sampler;ERR_ConvoyVehicle vehicle;ERR_InfantryResupply service;
|
||||
F(){itemSlot.attached=&item;carriedItem.slot=&itemSlot;item.carried=&carriedItem;item.parent=&truck;worker.group=second.group=&cg;workerAgent.m_UtilityComponent=&workerUtility;secondAgent.m_UtilityComponent=&secondUtility;worker.agent=&workerAgent;second.agent=&secondAgent;sampler.group=&cg;sampler.members={&worker};crew.StatusSampler=&sampler;truck.pos={20};crew.Group=&cg;donor.Group=&dg;hq.crew=&crew;hq.donor=&donor;vehicle.Group=&dg;vehicle.Entity=&truck;service.truck=service.m_Truck=&vehicle;hq.services={&service};site.Definition=&def;site.Responders=&responders;responders.Group=&cg;site.Container=&cache;site.items={&item};cache.inventory=&inv;mode.sites.sites={&site};job.m_HQ=&hq;job.m_Mode=&mode;job.m_Worker=&worker;job.m_Original={&item};ERR_GroundDepotAccess::clear=true;}
|
||||
void Mine(){item.mine=true;service.mineNeeded=true;service.needed=false;worker.compatible=false;}
|
||||
void Start(){job.Plan(0);assert(job.m_State==1&&hq.claims==1&&hq.orders==1);}
|
||||
void Reach(){Start();job.Tick(0);assert(job.m_State==2&&vehicle.SupplyHeld&&vehicle.clears==1);job.Tick(1000);assert(donor.NextStatus==1000&&job.walks==0);job.Tick(2000);assert(job.m_State==2);hq.watch.report.ObservedAt=3000;job.Tick(3000);assert(job.m_State==3&&hq.orders==2&&job.walks==0);job.Tick(4000);assert(job.m_State==4&&job.walks==1);}
|
||||
};
|
||||
int main(){
|
||||
// A mine-only deficit chooses the original porter without rifle compatibility,
|
||||
// but still follows both delivered radio orders and exact two-leg proof.
|
||||
{F f;f.Mine();f.Reach();assert(f.job.MineCargo()&&f.job.m_Worker==&f.worker);f.job.Tick(5000);auto*pick=f.job.m_Move;assert(pick->MineCargo());pick->unknown=false;pick->confirmed=true;pick->result=1;f.job.Tick(6000);f.job.Tick(7000);auto*drop=f.job.m_Move;assert(drop->MineCargo());drop->unknown=false;drop->confirmed=true;drop->result=1;assert(!f.service.AdmitDepot(&f.job,&f.item));f.job.Tick(8000);assert(f.job.m_State==6&&f.service.m_Stock.empty()&&f.service.m_MineStock.Count()==1&&f.service.m_DepotMineImports==1&&f.service.m_DepotImports==0);f.job.Tick(9000);assert(!f.job.Busy());}
|
||||
// Armed or changed cargo aborts at each active stage; uncertain native moves
|
||||
// retain claims until proof settles, and never import through a late callback.
|
||||
for(int phase=1;phase<=5;phase++){F f;f.Mine();if(phase<=2){f.Start();if(phase==2)f.job.Tick(0);}else{f.Reach();if(phase==3)f.job.m_State=3;if(phase==5){f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);}}
|
||||
f.item.armed=true;f.job.Tick(10000);assert(!f.job.Busy()&&f.service.m_MineStock.empty()&&f.job.m_Delivered==0);}
|
||||
{F f;f.Mine();f.Reach();f.job.Tick(5000);auto*leaf=f.job.m_Move;f.item.armed=true;f.job.Tick(6000);assert(f.job.m_State==7&&f.crew.LogisticsReservation>0);leaf->unknown=false;leaf->confirmed=true;leaf->result=1;f.job.Tick(7000);assert(!f.job.Busy()&&f.service.m_MineStock.empty()&&f.job.m_Delivered==0&&f.job.m_Spent.Count()==1);}
|
||||
{F f;f.Mine();f.Reach();f.job.Tick(5000);f.job.m_Move->mineCargo=false;assert(!f.job.TransferValid(f.job.m_Move));}
|
||||
{F f;f.Mine();f.Reach();f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);f.service.mineNeeded=false;assert(!f.job.ClaimDelivery(f.job.m_Move));f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;f.job.m_Move->result=1;f.job.Tick(8000);assert(f.service.m_MineStock.empty()&&f.job.m_Delivered==0);}
|
||||
for(int kind=0;kind<5;kind++){F f;f.Mine();if(kind==0)f.item.armed=true;if(kind==1)f.item.defaultArmed=true;if(kind==2)f.item.alive=false;if(kind==3)f.item.collision=false;if(kind==4)f.service.mineNeeded=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0&&f.hq.claims==0&&f.job.m_Attempts==0);}
|
||||
|
||||
// Actual Plan must enumerate worker/item pairs: the first fit original can
|
||||
// carry the wrong weapon while a second original can use this exact magazine.
|
||||
{F f;f.worker.well=2;f.second.well=1;f.sampler.members={&f.worker,&f.second};f.job.Plan(0);assert(f.job.m_State==1&&f.hq.claims==1&&f.hq.orders==1&&"compatible second original must permit planning despite incompatible first original");assert(f.job.m_Worker==&f.second&&f.job.m_WorkerSlot==1&&f.job.m_Item==&f.item&&f.job.m_Attempts==1);}
|
||||
{F f;f.worker.well=2;f.second.well=1;f.sampler.members={&f.second,&f.worker};f.Start();assert(f.job.m_Worker==&f.second&&f.job.m_WorkerSlot==0);}
|
||||
// Bad first originals do not hide a later valid original; if no eligible
|
||||
// compatible member remains, planning must not reserve or issue a command.
|
||||
for(int k=0;k<11;k++)for(bool later:{false,true}){F f;Group foreign;f.sampler.members={&f.worker};
|
||||
if(k==0)f.sampler.members[0]=nullptr;
|
||||
if(k==1)f.worker.attributed=false;
|
||||
if(k==2)f.worker.group=&foreign;
|
||||
if(k==3)f.worker.player=true;
|
||||
if(k==4)f.worker.alive=false;
|
||||
if(k==5)f.worker.aboard=true;
|
||||
if(k==6)f.worker.agent=nullptr;
|
||||
if(k==7)f.workerAgent.m_UtilityComponent=nullptr;
|
||||
if(k==8)f.workerUtility.m_AIInfo=false;
|
||||
if(k==9)f.worker.faction=2;
|
||||
if(k==10)f.worker.equipmentEligible=false;
|
||||
if(later)f.sampler.members.Insert(&f.second);
|
||||
f.job.Plan(0);
|
||||
if(later){assert(f.job.Busy()&&f.job.m_Worker==&f.second&&f.job.m_WorkerSlot==1&&f.hq.claims==1&&f.hq.orders==1&&f.job.m_Attempts==1);}
|
||||
else{assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0&&f.job.m_Spent.empty()&&f.crew.LogisticsReservation==0&&f.donor.LogisticsReservation==0&&f.vehicle.SupplyToken==0);}
|
||||
}
|
||||
for(int k=0;k<5;k++){F f;f.sampler.members={&f.worker,&f.second};if(k==0)f.worker.well=f.second.well=2;if(k==1)f.item.rounds=0;if(k==2)f.item.maxRounds=0;if(k==3)f.item.rounds=31;if(k==4)f.worker.compatible=f.second.compatible=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0&&f.job.m_Spent.empty());}
|
||||
{F f;f.sampler.owns=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0);}
|
||||
{F f;f.sampler.members.resize(33,&f.worker);f.job.Plan(0);assert(!f.job.Busy()&&f.hq.claims==0&&f.hq.orders==0&&f.job.m_Attempts==0);}
|
||||
// No new stock or commands on missing capacity/camp/worker/report/claim.
|
||||
for(int k=0;k<8;k++){F f;if(k==0)f.service.available=false;if(k==1)f.site.items.clear();if(k==2)f.crew.eligible=false;if(k==3)f.hq.watch.report.valid=false;if(k==4)f.hq.claim=false;if(k==5)f.item.compatible=false;if(k==6)f.job.m_Original.clear();if(k==7)f.service.needed=false;f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0&&f.job.m_Spent.empty());}
|
||||
{F f;f.Start();f.hq.route.outcome=-1;f.job.Tick(0);assert(!f.job.Busy()&&f.job.walks==0&&!f.vehicle.SupplyHeld&&f.job.m_Spent.empty());}
|
||||
// Complete actual state sequence; first proof retires before delivery callback.
|
||||
{F f;f.Reach();f.job.Tick(5000);auto*pickup=f.job.m_Move;assert(pickup&&!pickup->Delivery());pickup->unknown=false;pickup->confirmed=true;pickup->result=1;f.job.Tick(6000);assert(f.job.m_State==5&&!f.job.m_Move&&f.job.walks==2);f.job.Tick(7000);auto*delivery=f.job.m_Move;assert(delivery&&delivery!=pickup&&delivery->Delivery());delivery->unknown=false;delivery->confirmed=true;delivery->result=1;f.job.Tick(8000);assert(f.service.m_Stock.Count()==1&&f.job.m_Delivered==1&&f.job.m_State==6);f.job.Tick(9000);assert(!f.job.Busy()&&!f.vehicle.SupplyHeld&&f.job.m_Spent.Count()==1);}
|
||||
for(bool second:{false,true}){F f;f.Reach();f.job.Tick(5000);if(second){f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);}auto*leaf=f.job.m_Move;int walks=f.job.walks;f.job.Stop();assert(f.job.m_State==7&&f.vehicle.SupplyHeld&&f.crew.LogisticsReservation>0);f.job.Tick(200000);assert(f.job.m_State==7&&f.service.m_Stock.empty()&&f.job.walks==walks);leaf->unknown=false;leaf->confirmed=true;f.job.Tick(200001);assert(!f.job.Busy()&&f.service.m_Stock.empty()&&f.job.m_Spent.Count()==1&&!f.vehicle.SupplyHeld);f.job.Plan(300000);assert(!f.job.Busy());}
|
||||
// Expiry never extends the job; late proof does not adopt cargo.
|
||||
{F f;f.Reach();f.job.Tick(5000);f.job.Tick(f.job.m_Deadline);assert(f.job.m_State==7);f.job.m_Move->unknown=false;f.job.Tick(999999);assert(!f.job.Busy()&&f.service.m_Stock.empty());}
|
||||
// Target lifetime96 allowance: no eviction of spent/deleted identities.
|
||||
{F f;f.service.m_Stock.resize(96,nullptr);f.job.Plan(0);assert(!f.job.Busy()&&f.hq.orders==0);}
|
||||
// Filling the final lifetime slot does not invalidate the return phase.
|
||||
{F f;f.service.m_Stock.resize(95,nullptr);f.Reach();f.job.Tick(5000);f.job.m_Move->unknown=false;f.job.m_Move->result=1;f.job.Tick(6000);f.job.Tick(7000);f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;f.job.m_Move->result=1;f.job.Tick(8000);assert(f.service.m_Stock.Count()==96&&f.job.m_State==6);f.truck.pos={3};f.job.Tick(9000);assert(f.job.m_State==6&&f.vehicle.SupplyHeld);f.truck.pos={20};f.job.Tick(10000);assert(!f.job.Busy());}
|
||||
// A completed physical move cannot import wrong/previously-spent/foreign identity.
|
||||
{F f;f.Reach();f.job.Tick(5000);auto*l=f.job.m_Move;l->unknown=false;l->result=1;f.job.Tick(6000);f.job.Tick(7000);f.job.m_Move->unknown=false;f.job.m_Move->confirmed=true;Entity foreign;assert(!f.service.AdmitDepot(&f.job,&foreign));f.service.m_Spent.Insert(&f.item);assert(!f.service.AdmitDepot(&f.job,&f.item));}
|
||||
}
|
||||
'''.replace('METHODS',methods).replace('ADMISSION',admit.replace('bool AdmitDepot(', 'bool ERR_InfantryResupply::AdmitDepot(').replace('bool AdmitDepotMine(', 'bool ERR_InfantryResupply::AdmitDepotMine(')))
|
||||
print('PASS: actual depot mixed-roster worker/item selection, ownership/refusal and radio/planning/two-leg state transitions, fixed deadline, late proof quarantine, finite import and no adoption after Stop')
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Actual HQ cargo-class reservation and service demand; native availability doubled."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
H=Path(os.environ.get('ERR_DEPOT_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotHQ.c'))
|
||||
S=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
def cpp(s):
|
||||
s=s.replace('protected ','').replace('null','nullptr').replace('int committed;', 'int committed = 0;')
|
||||
for typ in ('ERR_HQUnit','ERR_InfantryResupply','ERR_ConvoyVehicle'):
|
||||
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||
for name in ('m_Mode','crew','donor','service','truck'):
|
||||
s=re.sub(r'\b'+name+r'\.',name+'->',s)
|
||||
s=s.replace('Replication.IsServer()', 'server').replace('ERR_GameMode.Get()', 'current')
|
||||
s=s.replace('foreach (IEntity item : m_Spent)', 'for (IEntity item : m_Spent)')
|
||||
return s
|
||||
hq='\n'.join(cpp(body(H,x)) for x in ('bool ReserveGroundDepot(', 'void ReleaseGroundDepot('))
|
||||
demand='\n'.join(cpp(body(S,x)) for x in ('bool DepotNeeded(', 'bool DepotMineNeeded('))
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
struct Entity{};using IEntity=Entity*;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}};
|
||||
struct Group{};struct HQ;
|
||||
struct Mode{HQ*owner=nullptr;bool active=true;bool Active(){return active;}HQ*Command(){return owner;}};
|
||||
bool server=true;Mode*current=nullptr;
|
||||
struct ERR_HQUnit{struct Group*Group=nullptr;bool HomeGuard=false,ReservedForSupport=false,Task=false,PendingTask=false,PendingCheckin=false,PendingConvoyOrder=false;int LogisticsReservation=0;};
|
||||
struct ERR_ConvoyVehicle{struct Group*Group=nullptr;int SupplyToken=0;bool SupplyHeld=false;};
|
||||
struct ERR_InfantryResupply{bool magAvailable=true,mineAvailable=true,captured=true;int m_DepotImports=0,m_DepotMineImports=0;Array<IEntity>m_Stock,m_MineStock,m_Spent;ERR_ConvoyVehicle*truck=nullptr;bool DepotAvailable(){return magAvailable;}bool DepotMineAvailable(){return mineAvailable;}bool CaptureMines(){return captured;}ERR_ConvoyVehicle*DepotTruck(){return truck;}
|
||||
DEMAND
|
||||
};
|
||||
struct HQ{Mode*m_Mode=nullptr;bool m_Stopping=false,eligible=true,reserve=true;Array<ERR_HQUnit*>m_Units;int claims=0,releases=0;bool MountedLogisticsEligible(ERR_HQUnit*){return eligible;}bool ReserveLogistics(Group*g,int token){if(!reserve)return false;for(auto*u:m_Units)if(u->Group==g){if(u->LogisticsReservation)return false;u->LogisticsReservation=token;claims++;return true;}return false;}void ReleaseLogistics(Group*g,int token){for(auto*u:m_Units)if(u->Group==g&&u->LogisticsReservation==token){u->LogisticsReservation=0;releases++;}}
|
||||
HQMETHODS
|
||||
};
|
||||
struct F{Mode mode;HQ h;Group cg,dg;ERR_HQUnit crew,donor;ERR_ConvoyVehicle truck;ERR_InfantryResupply s;Entity mine,mag,foreign;F(){server=true;current=&mode;mode.owner=&h;h.m_Mode=&mode;crew.Group=&cg;donor.Group=truck.Group=&dg;h.m_Units={&crew,&donor};s.truck=&truck;s.m_Stock={&mag};s.m_MineStock={&mine};}void Unclaimed(){assert(h.claims==0&&crew.LogisticsReservation==0&&donor.LogisticsReservation==0&&truck.SupplyToken==0&&!truck.SupplyHeld);}};
|
||||
int main(){
|
||||
// Mine-only expenditure authorizes mine cargo only. Magazine-only is inverse.
|
||||
for(bool mines:{false,true})for(bool cargo:{false,true}){F f;f.s.m_Spent={mines?&f.mine:&f.mag};bool accepted=f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300001,cargo);assert(accepted==(mines==cargo));if(!accepted)f.Unclaimed();else{assert(f.h.claims==1&&f.crew.LogisticsReservation==300001&&f.donor.LogisticsReservation==300001&&f.truck.SupplyToken==300001);assert(!f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300002,cargo));f.truck.SupplyHeld=true;f.h.ReleaseGroundDepot(&f.crew,&f.donor,&f.truck,300002);assert(f.truck.SupplyToken==300001&&f.truck.SupplyHeld);f.h.ReleaseGroundDepot(&f.crew,&f.donor,&f.truck,300001);assert(!f.crew.LogisticsReservation&&!f.donor.LogisticsReservation&&!f.truck.SupplyToken&&!f.truck.SupplyHeld);}}
|
||||
// Null/deleted/unrelated spent identities and satisfied imported demand never count.
|
||||
for(bool mines:{false,true})for(int k=0;k<4;k++){F f;if(k==0){f.s.m_Spent={nullptr};f.s.m_Stock.push_back(nullptr);f.s.m_MineStock.push_back(nullptr);}if(k==1)f.s.m_Spent={&f.foreign};if(k==2){f.s.m_Spent={mines?&f.mine:&f.mag};if(mines)f.s.m_DepotMineImports=1;else f.s.m_DepotImports=1;}if(k==3){f.s.m_Spent={mines?&f.mine:&f.mag};if(mines)f.s.mineAvailable=false;else f.s.magAvailable=false;}assert(!f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300001,mines));f.Unclaimed();}
|
||||
// Existing authority, task ownership and token gates apply equally to mine jobs.
|
||||
for(int k=0;k<15;k++){F f;f.s.m_Spent={&f.mine};int token=300001;if(k==0)server=false;if(k==1)f.h.m_Stopping=true;if(k==2)current=nullptr;if(k==3)f.mode.active=false;if(k==4)f.mode.owner=nullptr;if(k==5)token=300000;if(k==6)token=300097;if(k==7)f.h.m_Units.clear();if(k==8)f.h.eligible=false;if(k==9)f.crew.HomeGuard=true;if(k==10)f.crew.ReservedForSupport=true;if(k==11)f.crew.PendingCheckin=true;if(k==12)f.crew.PendingConvoyOrder=true;if(k==13)f.h.reserve=false;if(k==14)f.truck.Group=&f.cg;assert(!f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,token,true));f.Unclaimed();}
|
||||
// Legacy default remains magazine demand; distinct import counters don't cross classes.
|
||||
{F f;f.s.m_Spent={&f.mag,&f.mine};f.s.m_DepotMineImports=1;assert(!f.s.DepotMineNeeded()&&f.s.DepotNeeded());assert(f.h.ReserveGroundDepot(&f.crew,&f.donor,&f.s,300001));}
|
||||
}
|
||||
'''.replace('DEMAND',demand).replace('HQMETHODS',hq))
|
||||
print('PASS: actual HQ mine/magazine claim separation, real demand methods, exact token cleanup and original authority gates; native availability doubled')
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Actual authored-stock capture/access and exact post-delivery stock admission."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,body,run
|
||||
from test_finite_magazine_reservation import translated_reservation
|
||||
B=Path(os.environ.get('ERR_GROUND_DEPOT_ROOT',ROOT))
|
||||
S=Path(os.environ.get('ERR_SITE_SERVICE_SOURCE',B/'addons/core/Scripts/Game/ERR/00_Core/ERR_SiteService.c'))
|
||||
I=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',B/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryResupply.c'))
|
||||
L=Path(os.environ.get('ERR_GROUND_DEPOT_LOADING_SOURCE',B/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotLoading.c'))
|
||||
assert 'int DepotItemCount(' in S.read_text() and 'bool AdmitDepotMine(' in I.read_text(), 'finite authored camp mine provenance/import API is absent'
|
||||
# Read the actual authored configuration, not a duplicated stock fixture.
|
||||
conf=(B/'addons/core/Configs/ERR/EveronSites.conf').read_text()
|
||||
pmn='{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et'
|
||||
camps=[]
|
||||
for match in re.finditer(r' ERR_SiteDefinition \{',conf):
|
||||
end=match.end();depth=1
|
||||
while depth:
|
||||
if conf[end]=='{':depth+=1
|
||||
elif conf[end]=='}':depth-=1
|
||||
end+=1
|
||||
block=conf[match.start():end]
|
||||
eligible=bool(re.search(r'^ Kind 3$',block,re.M)) and not re.search(r'^ RevealOnEscape 1$',block,re.M)
|
||||
stocks=re.findall(r'ERR_SiteStock \{ Prefab "([^"\n]+)" Count (\d+)([^}]*?) \}',block)
|
||||
mines=[row for row in stocks if row[0]==pmn]
|
||||
if eligible:
|
||||
assert len(mines)==1 and mines[0][1]=='2' and 'TargetId' not in mines[0][2]
|
||||
camps.append(re.search(r'Id "([^"]+)"',block).group(1))
|
||||
else:assert not mines
|
||||
assert len(camps)==5
|
||||
assert 'ERR_InfantryMineTransfer' not in S.read_text(), 'Core must not depend on the AI mine transfer layer'
|
||||
source=S.read_text();start=source.index(' if (spec.Kind == ERR_LocationKind.OUTPOST');capture=source[start:source.index(' placement.Item = item;',start)]
|
||||
access='\n'.join(body(S,k) for k in ('int DepotMagazineCount(', 'int DepotMineCount(', 'int DepotItemCount(', 'IEntity DepotItem(', 'void DepotStock(', 'IEntity DepotMagazine(', 'bool DepotOriginal('))
|
||||
admit='\n'.join(body(I,k) for k in ('bool DepotCurrent(', 'bool DepotAvailable(', 'bool DepotNeeded(', 'bool AdmitDepot(', 'bool DepotMineAvailable(', 'bool DepotMineNeeded(', 'bool AdmitDepotMine(', 'protected bool CaptureMines('))
|
||||
delivered=body(L,'bool DeliveredTo(')
|
||||
def adapt(s):
|
||||
s=s.replace('array<IEntity>','Array<IEntity>').replace('out Array<IEntity> items','Array<IEntity>& items').replace('ERR_SiteRuntime site','Site* site').replace('ERR_GroundDepotLoading job','Loading* job').replace('ERR_InfantryResupply service','Service* service')
|
||||
s=s.replace('protected ', '').replace('ERR_SupplyConvoyTask supply = ERR_SupplyConvoyTask.Cast(m_Convoy);','Supply* supply = m_Convoy;')
|
||||
s=s.replace('InventoryItemComponent carried = InventoryItemComponent.Cast(item.FindComponent(InventoryItemComponent));','InventoryItem* carried = item->inventoryItem;')
|
||||
s=s.replace('foreach (IEntity item : m_MineStock)', 'for (IEntity item : m_MineStock)')
|
||||
s=s.replace('ERR_InfantryMineTransfer.', 'ERR_InfantryMineTransfer::').replace('ERR_InfantryAmmoTransfer.', 'ERR_InfantryAmmoTransfer::')
|
||||
for name in ('supply','m_MineBatch','carried'):s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('GetParentSlot().','GetParentSlot()->')
|
||||
for name in ('site','spec','stock','item','m_Stock','m_Spent','m_Mounted','m_Truck','job','m_Move','m_Reservation'):
|
||||
if name not in ('m_Stock','m_Spent'): s=s.replace(name+'.',name+'->')
|
||||
s=s.replace('site->Definition.', 'site->Definition->').replace('site->Responders.', 'site->Responders->')
|
||||
s=s.replace('stock->TargetId.IsEmpty()','stock->TargetId.empty()').replace('item->FindComponent(BaseMagazineComponent)','item->magazine')
|
||||
s=s.replace('foreach (IEntity item : m_Spent)', 'for (IEntity item : m_Spent)').replace('int committed;', 'int committed=0;')
|
||||
s=s.replace('Replication.IsServer()','server').replace('ERR_PrepareResult.READY','1').replace('ERR_LocationKind.OUTPOST','3').replace('null','nullptr')
|
||||
return s
|
||||
run(r'''
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
using string=std::string;bool server=true;
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Insert(T x){this->push_back(x);}void RemoveItem(T x){this->erase(std::remove(this->begin(),this->end(),x),this->end());}};
|
||||
struct Entity;using IEntity=Entity*;
|
||||
struct Slot{IEntity attached=nullptr;IEntity GetAttachedEntity(){return attached;}};
|
||||
struct InventoryItem{Slot* slot=nullptr;Slot* GetParentSlot(){return slot;}};
|
||||
struct Entity{bool magazine=true,ready=false,stored=true;Slot slot;InventoryItem inventory;InventoryItem*inventoryItem=&inventory;Entity(){slot.attached=this;inventory.slot=&slot;}};
|
||||
// Native ready predicate and storage graph are doubled; actual readiness is separately tested by mine-transfer suites.
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity item){return item&&item->ready;}};
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity item,IEntity){return item&&item->stored;}};
|
||||
struct Def{int Kind=3;bool RevealOnEscape=false;};struct Stock{string TargetId,Prefab;};
|
||||
struct Responders{bool Group=true;};struct Site{Def* Definition=nullptr;IEntity Container=nullptr;struct Responders* Responders=nullptr;Array<IEntity>DepotMagazines,DepotMines;};
|
||||
struct Sites{bool m_Stopped=false;int m_State=0;Array<Site*>m_Sites;Array<IEntity>m_Entities;bool Owns(IEntity x){return m_Entities.Contains(x);}
|
||||
''' + adapt(access)+ '\nvoid Capture(Def* spec,Stock* stock,Site* site,IEntity item){'+adapt(capture)+r'''}
|
||||
};
|
||||
''' + translated_reservation()+r'''
|
||||
struct Service;
|
||||
struct Proof{bool done=true,delivery=true,mine=false;bool MineCargo(){return mine;}bool Confirmed(){return done;}bool Delivery(){return delivery;}};
|
||||
struct Loading{bool m_Imported=false,valid=true,m_MineCargo=false;int m_State=5;Service* m_Service=nullptr;IEntity m_Item=nullptr;Proof* m_Move=nullptr;ERR_FiniteMagazineReservation* m_Reservation=nullptr;bool Valid(){return valid;}bool DeliveredTo(Service*,IEntity,bool=false);};
|
||||
struct Mounted{bool busy=false;bool Busy(){return busy;}};
|
||||
struct Truck{IEntity Entity=nullptr;};
|
||||
struct Batch{bool complete=true;Array<IEntity>items;bool Complete(){return complete;}int Count(){return items.Count();}IEntity At(int index){return items[index];}};
|
||||
struct Supply{Batch*MineStock=nullptr;};
|
||||
struct Service{Supply*m_Convoy=nullptr;Batch*m_MineBatch=nullptr;bool m_MineStockCaptured=false;Array<IEntity>m_MineStock;int m_DepotMineImports=0;bool current=true,truckValid=true,unresolved=false,m_Move=false,m_MineMove=false;int m_State=1,audits=0,m_DepotImports=0;Array<IEntity>m_Stock,m_Spent,cargo;Mounted* m_Mounted=nullptr;Truck* m_Truck=nullptr;bool Current(){return current;}bool Unresolved(){return unresolved;}bool TruckValid(bool endpoint){assert(endpoint);return truckValid;}bool Contains(IEntity,IEntity item){return cargo.Contains(item);}void Audit(string){audits++;}
|
||||
''' + adapt(admit)+r'''};
|
||||
''' + adapt(delivered).replace('bool DeliveredTo(', 'bool Loading::DeliveredTo(').replace('bool mineCargo = false','bool mineCargo')+r'''
|
||||
struct F{Entity cache,item,foreign,truck;Def def;Stock stock;Responders responders;Site site;Sites sites;Array<IEntity>original,spent;ERR_FiniteMagazineReservation reservation;Service service;Loading job;Proof proof;Truck vehicle;Mounted mounted;
|
||||
F():reservation(&original,&spent,&item){site.Definition=&def;site.Container=&cache;site.Responders=&responders;sites.m_Sites={&site};sites.m_Entities={&cache};sites.Capture(&def,&stock,&site,&item);original={&item};assert(reservation.Reserve()&&reservation.Commit());job.m_Reservation=&reservation;job.m_Move=&proof;job.m_Item=&item;job.m_Service=&service;vehicle.Entity=&truck;service.m_Truck=&vehicle;service.m_Mounted=&mounted;service.cargo={&item};}
|
||||
};
|
||||
struct M:F{Entity initial;Batch batch;Supply supply;
|
||||
M(){item.magazine=false;item.ready=true;initial.magazine=false;initial.ready=true;batch.items={&initial};supply.MineStock=&batch;service.m_Convoy=&supply;service.m_MineBatch=&batch;service.m_Spent={&initial};job.m_MineCargo=true;proof.mine=true;}
|
||||
};
|
||||
int main(){
|
||||
// Authored inert mines join detached startup provenance, without replacing magazine APIs.
|
||||
{F f;f.sites.m_State=1;f.stock.Prefab="{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";Entity mine;mine.magazine=false;mine.ready=true;f.sites.Capture(&f.def,&f.stock,&f.site,&mine);assert(f.sites.DepotMagazineCount(&f.site)==1&&f.sites.DepotMineCount(&f.site)==1&&f.sites.DepotItemCount(&f.site)==2);assert(f.sites.DepotItem(&f.site,1)==&mine&&f.sites.DepotOriginal(&f.site,&mine));Array<IEntity>out;f.sites.DepotStock(&f.site,out);out.clear();assert(f.sites.DepotItemCount(&f.site)==2);Entity other;other.magazine=false;other.ready=true;assert(!f.sites.DepotOriginal(&f.site,&other));f.sites.Capture(&f.def,&f.stock,&f.site,&other);Entity third;third.magazine=false;third.ready=true;f.sites.Capture(&f.def,&f.stock,&f.site,&third);assert(f.sites.DepotMineCount(&f.site)==2&&!f.sites.DepotOriginal(&f.site,&third));}
|
||||
for(int bad=0;bad<4;bad++){F f;f.item.magazine=false;f.item.ready=true;f.stock.Prefab="{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";f.site.DepotMagazines.clear();if(bad==0)f.def.RevealOnEscape=true;if(bad==1)f.def.Kind=4;if(bad==2)f.stock.TargetId="radio";if(bad==3)f.stock.Prefab="foreign";f.sites.Capture(&f.def,&f.stock,&f.site,&f.item);assert(f.site.DepotMines.empty());}
|
||||
{M f;assert(f.service.DepotMineNeeded()&&!f.service.DepotNeeded());assert(f.service.AdmitDepotMine(&f.job,&f.item));assert(f.service.m_MineStock.Count()==2&&f.service.m_DepotMineImports==1&&f.service.m_Stock.empty());assert(!f.service.DepotMineNeeded()&&!f.service.AdmitDepotMine(&f.job,&f.item));assert(f.spent.Count()==1&&f.service.m_Spent.Count()==1);ERR_FiniteMagazineReservation next(&f.service.m_MineStock,&f.service.m_Spent,&f.item);assert(next.Reserve()&&next.Commit());assert(f.service.DepotMineNeeded());}
|
||||
// Known mine expenditure only; missing/deleted/magazine identities never become mine demand.
|
||||
{M f;f.service.m_Spent.clear();assert(!f.service.DepotMineNeeded());f.service.m_Spent={nullptr,&f.foreign};f.service.m_Stock={&f.foreign};assert(f.service.DepotNeeded()&&!f.service.DepotMineNeeded());f.service.m_MineStock={nullptr};assert(!f.service.DepotMineNeeded());}
|
||||
for(int bad=0;bad<21;bad++){M f;assert(f.service.DepotMineNeeded());if(bad==0)f.service.current=false;if(bad==1)f.service.m_State=7;if(bad==2)f.batch.complete=false;if(bad==3)f.supply.MineStock=nullptr;if(bad==4)f.service.m_MineBatch=nullptr;if(bad==5)f.service.m_MineStock.resize(96,&f.initial);if(bad==6)f.service.m_Spent.resize(96,&f.initial);if(bad==7)f.item.ready=false;if(bad==8)f.service.m_Stock.Insert(&f.item);if(bad==9)f.service.m_Spent.Insert(&f.item);if(bad==10)f.service.cargo.clear();if(bad==11)f.item.stored=false;if(bad==12)f.item.inventoryItem=nullptr;if(bad==13)f.item.inventory.slot=nullptr;if(bad==14)f.item.slot.attached=&f.foreign;if(bad==15)f.job.m_MineCargo=false;if(bad==16)f.proof.mine=false;if(bad==17)f.job.m_State=7;if(bad==18)f.job.valid=false;if(bad==19)f.proof.done=false;if(bad==20)f.service.m_Spent.clear();int before=f.service.m_MineStock.Count();assert(!f.service.AdmitDepotMine(&f.job,&f.item));assert(f.service.m_MineStock.Count()==before&&f.service.m_DepotMineImports==0);}
|
||||
{M f;f.batch.items.clear();f.service.m_Spent.clear();assert(!f.service.DepotMineNeeded()&&!f.service.AdmitDepotMine(&f.job,&f.item));}
|
||||
{M f;assert(f.service.DepotMineNeeded());f.service.m_MineStock.resize(95,&f.initial);assert(f.service.AdmitDepotMine(&f.job,&f.item));assert(f.service.m_MineStock.Count()==96&&f.service.DepotCurrent()&&!f.service.DepotMineAvailable());}
|
||||
|
||||
{F f;Array<IEntity>out;f.sites.DepotStock(&f.site,out);assert(out.empty());f.sites.m_State=1;f.sites.DepotStock(&f.site,out);assert(out.Count()==1&&out[0]==&f.item);out[0]=&f.foreign;out.clear();assert(f.sites.DepotOriginal(&f.site,&f.item)&&!f.sites.DepotOriginal(&f.site,&f.foreign));assert(f.sites.DepotMagazine(&f.site,-1)==nullptr&&f.sites.DepotMagazine(&f.site,1)==nullptr);f.sites.m_Stopped=true;assert(!f.sites.DepotOriginal(&f.site,&f.item));}
|
||||
for(int kind=0;kind<6;kind++){F f;f.site.DepotMagazines.clear();if(kind==0)f.def.RevealOnEscape=true;if(kind==1)f.def.Kind=4;if(kind==2)f.stock.TargetId="radio";if(kind==3)f.item.magazine=false;if(kind==4)f.site.DepotMagazines.resize(96,&f.foreign);f.sites.Capture(&f.def,&f.stock,&f.site,&f.item);assert(f.site.DepotMagazines.Contains(&f.item)==(kind==5));}
|
||||
for(int bad=0;bad<7;bad++){F f;f.sites.m_State=1;if(bad==0)server=false;if(bad==1)f.sites.m_Sites.clear();if(bad==2)f.site.Container=nullptr;if(bad==3)f.sites.m_Entities.clear();if(bad==4)f.site.Responders=nullptr;if(bad==5)f.responders.Group=false;if(bad==6)f.site.DepotMagazines.resize(97,&f.item);assert(f.sites.DepotMagazineCount(&f.site)==0);server=true;}
|
||||
{F f;assert(f.service.AdmitDepot(&f.job,&f.item));assert(f.service.m_Stock.Count()==1&&f.service.m_Stock[0]==&f.item&&f.service.audits==1);assert(!f.service.AdmitDepot(&f.job,&f.item));ERR_FiniteMagazineReservation outgoing(&f.service.m_Stock,&f.service.m_Spent,&f.item);assert(outgoing.Reserve()&&outgoing.Commit());assert(f.spent.Count()==1&&f.service.m_Spent.Count()==1);assert(!f.service.AdmitDepot(&f.job,&f.item));}
|
||||
// Deleted identities and mine-only spent entries cannot create magazine demand.
|
||||
{F f;f.service.m_Stock={nullptr};f.service.m_Spent={nullptr,&f.foreign};assert(!f.service.DepotNeeded());f.service.m_Stock.Insert(&f.foreign);assert(f.service.DepotNeeded());f.service.m_DepotImports=1;assert(!f.service.DepotNeeded());Entity another;f.service.m_Stock.Insert(&another);f.service.m_Spent.Insert(&another);assert(f.service.DepotNeeded());}
|
||||
// Final lifetime slot can complete delivery and departure; admission then closes.
|
||||
{F f;f.service.m_Stock.resize(95,&f.foreign);f.service.m_Spent={&f.foreign};assert(f.service.DepotNeeded());assert(f.service.AdmitDepot(&f.job,&f.item));assert(f.service.m_Stock.Count()==96&&f.service.m_DepotImports==1);assert(f.service.DepotCurrent()&&!f.service.DepotAvailable()&&!f.service.DepotNeeded());}
|
||||
for(int bad=0;bad<21;bad++){F f;
|
||||
if(bad==0)f.service.current=false;
|
||||
if(bad==1)f.service.m_State=7;
|
||||
if(bad==2)f.service.m_Move=true;
|
||||
if(bad==3)f.service.m_MineMove=true;
|
||||
if(bad==4)f.service.unresolved=true;
|
||||
if(bad==5)f.mounted.busy=true;
|
||||
if(bad==6)f.service.truckValid=false;
|
||||
if(bad==7)f.service.m_Stock.resize(96,&f.foreign);
|
||||
if(bad==8)f.service.m_Spent.resize(96,&f.foreign);
|
||||
if(bad==9)f.service.m_Spent={&f.item};
|
||||
if(bad==10)f.service.cargo.clear();
|
||||
if(bad==11)f.job.m_Imported=true;
|
||||
if(bad==12)f.job.m_State=7;
|
||||
if(bad==13)f.job.m_Service=nullptr;
|
||||
if(bad==14)f.job.m_Item=&f.foreign;
|
||||
if(bad==15)f.proof.done=false;
|
||||
if(bad==16)f.proof.delivery=false;
|
||||
if(bad==17)f.job.m_Move=nullptr;
|
||||
if(bad==18)f.job.m_Reservation=nullptr;
|
||||
if(bad==19)f.job.valid=false;
|
||||
if(bad==20)f.spent.clear();
|
||||
int before=f.service.m_Stock.Count();assert(!f.service.AdmitDepot(&f.job,&f.item));assert(f.service.m_Stock.Count()==before&&f.service.audits==0);}
|
||||
{F f;f.job.m_State=7;f.proof.done=true;assert(!f.service.AdmitDepot(&f.job,&f.item));f.job.m_State=5;f.job.valid=false;assert(!f.service.AdmitDepot(&f.job,&f.item));}
|
||||
}
|
||||
''')
|
||||
print('PASS: actual mine/magazine provenance and separated finite mine imports; actual authored identity capture, READY-only detached provenance, same shared stock admission, completed exact two-leg proof, lifetime caps and Stop no late adoption')
|
||||
@@ -0,0 +1,105 @@
|
||||
"""Execute complete actual finite two-leg transfer with native inventory boundaries."""
|
||||
import os,re
|
||||
from pathlib import Path
|
||||
from policy_harness import ROOT,run
|
||||
from test_finite_magazine_reservation import translated_reservation
|
||||
p=Path(os.environ.get('ERR_GROUND_DEPOT_TRANSFER_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_GroundDepotTransfer.c'))
|
||||
s=p.read_text();code=s[s.index('class ERR_GroundDepotTransfer\n'):]
|
||||
code=re.sub(r'//[^\n]*','',code).replace('protected ','').replace('ref ','').replace('class ','struct ').replace('void ERR_GroundDepotTransfer(','ERR_GroundDepotTransfer(').replace('array<','Array<').replace('null','nullptr')
|
||||
for kind,field in {'InventoryStorageManagerComponent':'inventory','InventoryItemComponent':'item','BaseMagazineComponent':'magazine','DamageManagerComponent':'damage'}.items():
|
||||
code=re.sub(kind+r'\.Cast\(([^;\n]*?)\.FindComponent\('+kind+r'\)\)',lambda m:m[1]+'.'+field,code)
|
||||
code=code.replace('.FindComponent('+kind+')','.'+field)
|
||||
for name in ('EStoragePurpose','ERR_ForceRules','ERR_InfantryAmmoTransfer','ERR_InfantryMineTransfer'):code=code.replace(name+'.',name+'::')
|
||||
for name in ('m_Owner','m_From','m_To','m_Item','m_Source','m_Destination','m_FromSlot','m_FromStorage','m_ToStorage','m_Reservation','m_Callback','source','destination','item','inventoryItem','magazine','damage','inventory','storage','slot'):
|
||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||
code=code.replace(').',')->').replace('ERR_FiniteMagazineReservation ', 'ERR_FiniteMagazineReservation* ').replace('ERR_GroundDepotCallback ', 'ERR_GroundDepotCallback* ')
|
||||
code=re.sub(r'\b(bool|int|IEntity|ERR_GroundDepotLoading|InventoryStorageManagerComponent|InventoryStorageSlot|BaseInventoryStorageComponent|ERR_FiniteMagazineReservation\*|ERR_GroundDepotCallback\*) (\w+);',r'\1 \2{};',code)
|
||||
code=code.replace('\n}\n','\n};\n')
|
||||
callbacks=[]
|
||||
for n in ('OnComplete','OnAllComplete','OnFailed','OnAllFailed'):
|
||||
b=re.search('protected override void '+n+r'\(\) (\{[^\n]+\})',s)[1]
|
||||
callbacks.append('void ERR_GroundDepotCallback::'+n+'() '+b.replace('Transfer.','Transfer->'))
|
||||
pre=r'''
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Insert(T x){this->push_back(x);}void RemoveItem(T x){this->erase(std::remove(this->begin(),this->end(),x),this->end());}};
|
||||
struct Entity;struct Inventory;struct Slot;struct Storage;struct Magazine;struct Owner;
|
||||
using IEntity=Entity*;using InventoryStorageManagerComponent=Inventory*;using InventoryStorageSlot=Slot*;using BaseInventoryStorageComponent=Storage*;using BaseMagazineComponent=Magazine*;using ERR_GroundDepotLoading=Owner*;
|
||||
struct Item{Slot*slot=nullptr;Slot*GetParentSlot(){return slot;}};using InventoryItemComponent=Item*;
|
||||
struct Damage{bool destroyed=false;bool IsDestroyed(){return destroyed;}};using DamageManagerComponent=Damage*;
|
||||
struct Entity{Entity*parent=nullptr;Item*item=nullptr;Magazine*magazine=nullptr;Damage*damage=nullptr;Inventory*inventory=nullptr;bool mine=false,armed=false,defaultArmed=false,collision=true;};
|
||||
struct Slot{Storage*storage=nullptr;IEntity attached=nullptr;Storage*GetStorage(){return storage;}IEntity GetAttachedEntity(){return attached;}};
|
||||
struct Storage{IEntity owner=nullptr;IEntity GetOwner(){return owner;}};
|
||||
struct Magazine{bool used=false,well=true;int rounds=30,capacity=30;bool IsUsed(){return used;}bool GetMagazineWell(){return well;}int GetAmmoCount(){return rounds;}int GetMaxAmmoCount(){return capacity;}};
|
||||
struct ERR_ForceRules{static bool Finite(float x){return std::isfinite(x);}};
|
||||
struct ERR_InfantryAmmoTransfer{static bool StoredUnder(IEntity e,IEntity root){for(int i=0;e&&i<16;i++,e=e->parent)if(e==root)return true;return false;}};
|
||||
// ReadyMine is an explicit native-component boundary; its complete implementation
|
||||
// is covered by infantry mine transfer tests. This models exact inert PMN4 state.
|
||||
struct ERR_InfantryMineTransfer{static bool ReadyMine(IEntity e){return e&&e->mine&&e->damage&&!e->damage->destroyed&&!e->armed&&!e->defaultArmed&&e->collision;}};
|
||||
struct EStoragePurpose{static const int PURPOSE_DEPOSIT=1;};
|
||||
struct ScriptedInventoryOperationCallback{virtual void OnComplete(){}virtual void OnAllComplete(){}virtual void OnFailed(){}virtual void OnAllFailed(){}virtual ~ScriptedInventoryOperationCallback()=default;};
|
||||
struct ERR_GroundDepotTransfer;
|
||||
struct ERR_GroundDepotCallback:ScriptedInventoryOperationCallback{ERR_GroundDepotTransfer*Transfer=nullptr;void OnComplete()override;void OnAllComplete()override;void OnFailed()override;void OnAllFailed()override;};
|
||||
struct Owner{bool valid=true,claimed=false;int claims=0;bool TransferValid(ERR_GroundDepotTransfer*){return valid;}bool ClaimDelivery(ERR_GroundDepotTransfer*){claims++;if(claimed)return false;claimed=true;return true;}};
|
||||
struct Inventory{IEntity owner=nullptr;Array<IEntity>items;Array<Storage*>storages;Storage*selected=nullptr;bool allow=true,accept=true;int calls=0,ack=0;ScriptedInventoryOperationCallback*last=nullptr;std::function<void()>effect;
|
||||
IEntity GetOwner(){return owner;}void GetItems(Array<IEntity>&a,int){a=items;}void GetStorages(Array<Storage*>&a,int){a=storages;}Storage*FindStorageForItem(IEntity,int){return selected;}
|
||||
bool CanMoveItemToStorage(IEntity,Storage*,int){return allow;}
|
||||
bool TryMoveItemToStorage(IEntity,Storage*,int,ScriptedInventoryOperationCallback*cb){calls++;last=cb;if(effect)effect();if(ack==1)cb->OnComplete();if(ack==-1)cb->OnFailed();return accept;}};
|
||||
'''
|
||||
main=r'''
|
||||
struct Fixture{bool mine=false;Owner owner;Entity cache,porter,truck,mag,foreign;Inventory source,carry,cargo;Storage cs,ps,ts;Slot cslot,pslot,tslot;Item item;Magazine magazine;Damage damage;Array<IEntity>original,spent;ERR_FiniteMagazineReservation reservation;
|
||||
Fixture():reservation(&original,&spent,&mag){cache.inventory=&source;porter.inventory=&carry;truck.inventory=&cargo;source.owner=&cache;carry.owner=&porter;cargo.owner=&truck;cs.owner=&cache;ps.owner=&porter;ts.owner=&truck;source.storages={&cs};carry.storages={&ps};cargo.storages={&ts};carry.selected=&ps;cargo.selected=&ts;cslot.storage=&cs;pslot.storage=&ps;tslot.storage=&ts;cslot.attached=&mag;item.slot=&cslot;mag.item=&item;mag.magazine=&magazine;mag.damage=&damage;source.items={&mag};original={&mag};assert(reservation.Reserve());}
|
||||
ERR_GroundDepotTransfer Pickup(){return {&owner,&cache,&porter,&mag ,&reservation,false,mine};}
|
||||
ERR_GroundDepotTransfer Delivery(){return {&owner,&porter,&truck,&mag ,&reservation,true,mine};}
|
||||
void Pick(){source.items.clear();carry.items={&mag};cslot.attached=nullptr;pslot.attached=&mag;item.slot=&pslot;}
|
||||
void Deliver(){carry.items.clear();cargo.items={&mag};pslot.attached=nullptr;tslot.attached=&mag;item.slot=&tslot;}
|
||||
};
|
||||
int main(){
|
||||
// Ready inert PMN4 uses the same exact native two-leg custody protocol,
|
||||
// shared reservation and isolated callbacks, without a magazine component.
|
||||
{Fixture f;f.mine=f.mag.mine=true;f.mag.magazine=nullptr;auto a=f.Pickup();assert(a.MineCargo()&&a.Tick(0)==0&&a.Unresolved());f.Pick();f.source.last->OnComplete();assert(a.Tick(1)==1&&a.Confirmed());auto b=f.Delivery();assert(b.MineCargo()&&b.Tick(2)==0&&f.owner.claims==1);f.Deliver();f.carry.last->OnComplete();assert(b.Tick(3)==1&&b.Confirmed()&&!b.Unresolved()&&f.spent.Count()==1);}
|
||||
// Every phase refuses changed/armed/default-armed/non-PMN4/destroyed identity.
|
||||
for(int phase=0;phase<3;phase++)for(int kind=0;kind<5;kind++){Fixture f;f.mine=f.mag.mine=true;f.mag.magazine=nullptr;auto a=f.Pickup();
|
||||
if(phase==1)assert(a.Tick(0)==0);
|
||||
if(phase==2){assert(a.Tick(0)==0);f.Pick();f.source.last->OnComplete();assert(a.Tick(1)==1);}
|
||||
if(kind==0)f.mag.armed=true;
|
||||
if(kind==1)f.mag.defaultArmed=true;
|
||||
if(kind==2)f.mag.mine=false;
|
||||
if(kind==3)f.damage.destroyed=true;
|
||||
if(kind==4)f.mag.collision=false;
|
||||
if(phase==2){auto b=f.Delivery();assert(b.Tick(2)==-1&&!b.Submitted()&&f.carry.calls==0&&!a.Confirmed());}
|
||||
else{assert(a.Tick(1)==-1);assert(f.source.calls==(phase==1?1:0));if(phase==1)assert(a.Unresolved());}
|
||||
}
|
||||
// Inline native mutation after initial ready check cannot promote success.
|
||||
{Fixture f;f.mine=f.mag.mine=true;auto a=f.Pickup();f.source.ack=1;f.source.effect=[&]{f.Pick();f.mag.armed=true;};assert(a.Tick(0)==-1&&!a.Confirmed()&&!a.Unresolved());}
|
||||
{Fixture f;f.mine=f.mag.mine=true;auto a=f.Pickup();assert(a.Tick(0)==0);a.Stop();f.Pick();f.source.last->OnComplete();assert(a.Tick(1)==-1&&!a.Confirmed()&&!a.Unresolved()&&f.source.calls==1&&f.spent.Count()==1);}
|
||||
{Fixture f;f.mine=f.mag.mine=true;f.source.accept=false;auto a=f.Pickup();assert(a.Tick(0)==-1&&a.Unresolved());f.damage.destroyed=true;assert(a.Unresolved());a.m_Item=nullptr;assert(!a.Unresolved()&&f.spent.Count()==1);}
|
||||
{Fixture f;f.mine=f.mag.mine=true;f.mag.magazine=nullptr;auto wrong=ERR_GroundDepotTransfer(&f.owner,&f.cache,&f.porter,&f.mag,&f.reservation,false,false);assert(wrong.Tick(0)==-1&&!wrong.Submitted()&&f.source.calls==0);}
|
||||
|
||||
{Fixture f;auto a=f.Pickup();a.Acknowledge(true);assert(a.m_Ack==0);assert(a.Tick(0)==0&&a.Submitted()&&a.Unresolved());assert(f.reservation.CommittedFor(&f.mag));f.source.last->OnComplete();assert(a.Tick(1)==0);f.Pick();assert(a.Tick(2)==1&&a.Confirmed()&&!a.Unresolved());auto b=f.Delivery();assert(b.Tick(3)==0&&f.owner.claims==1);f.source.last->OnAllComplete();assert(b.Tick(4)==0);f.Deliver();assert(b.Tick(5)==0);f.carry.last->OnAllComplete();assert(b.Tick(6)==1&&b.Confirmed()&&!b.Unresolved());assert(f.source.calls==1&&f.carry.calls==1&&f.spent.Count()==1);}
|
||||
{Fixture f;f.source.ack=1;f.source.accept=false;f.source.effect=[&]{assert(f.reservation.CommittedFor(&f.mag));assert(!f.reservation.ReleaseUnsubmitted());f.Pick();};auto a=f.Pickup();assert(a.Tick(0)==1&&!a.Unresolved());}
|
||||
{Fixture f;f.source.accept=false;auto a=f.Pickup();assert(a.Tick(0)==-1&&a.Unresolved());a.Stop();f.Pick();f.source.last->OnComplete();assert(!a.Unresolved()&&!a.Confirmed()&&a.Tick(1)==-1);}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);a.Stop();f.Pick();f.source.last->OnComplete();assert(!a.Unresolved()&&!a.Confirmed()&&a.Tick(1)==-1);}
|
||||
{Fixture f;auto a=f.Pickup();f.source.ack=1;f.source.effect=[&]{a.Stop();f.Pick();};assert(a.Tick(0)==-1&&!a.Confirmed()&&!a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();f.source.ack=1;f.source.effect=[&]{f.owner.valid=false;f.Pick();};assert(a.Tick(0)==-1&&!a.Confirmed()&&!a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.source.last->OnComplete();f.source.last->OnFailed();assert(a.Tick(1)==-1&&!a.Unresolved());f.source.last->OnAllComplete();assert(a.Tick(2)==-1);}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.item.slot=nullptr;f.source.last->OnFailed();assert(a.Tick(1)==-1&&a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);assert(a.Tick(8000)==-1&&a.Unresolved());assert(f.source.calls==1&&!f.reservation.ReleaseUnsubmitted());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(10);assert(a.Tick(9)==-1&&a.Unresolved());}
|
||||
{Fixture f;auto a=f.Pickup();a.Stop();assert(a.Tick(0)==-1&&!a.Submitted()&&f.reservation.ReleaseUnsubmitted());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);a.m_Item=nullptr;assert(a.Tick(1)==-1&&!a.Unresolved()&&f.spent.Count()==1);}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.damage.destroyed=true;assert(a.Tick(1)==-1&&a.Unresolved());}
|
||||
for(int bad=0;bad<15;bad++){Fixture f;auto a=f.Pickup();switch(bad){case0:break;}
|
||||
if(bad==0)f.magazine.used=true;if(bad==1)f.magazine.rounds=0;if(bad==2)f.magazine.capacity=100001;if(bad==3)f.damage.destroyed=true;if(bad==4)f.owner.valid=false;if(bad==5)f.source.allow=false;if(bad==6)f.carry.selected=nullptr;if(bad==7)f.cslot.attached=&f.foreign;if(bad==8)f.item.slot=&f.pslot;if(bad==9)f.source.items.clear();if(bad==10)f.carry.items={&f.mag};if(bad==11)f.carry.storages.clear();if(bad==12)f.carry.owner=&f.foreign;if(bad==13)f.source.items.resize(129,&f.mag);if(bad==14)f.source.storages.resize(65,&f.cs);
|
||||
assert(a.Tick(0)==-1&&!a.Submitted()&&f.source.calls==0&&f.reservation.ReleaseUnsubmitted());}
|
||||
{Fixture f;auto a=f.Pickup();a.Tick(0);f.magazine.rounds--;assert(a.Tick(1)==-1&&a.Unresolved());}
|
||||
{Fixture f;f.Pick();assert(f.reservation.Commit());auto a=f.Delivery();a.Tick(0);auto b=f.Delivery();assert(b.Tick(1)==-1&&!b.Submitted()&&f.carry.calls==1);}
|
||||
}
|
||||
'''.replace('switch(bad){case0:break;}','')
|
||||
main=main.replace(';if(bad', ';\n if(bad')
|
||||
run(pre+translated_reservation()+code+'\n'.join(callbacks)+main)
|
||||
print('PASS: actual full magazine/inert-PMN4 depot leaf, exact custody, isolated callbacks, finite reservation, false/unknown and Stop quarantine, once-only delivery and caps; native movement unqualified')
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user