Add mission customization, HQ support and aggression-driven AI
Integrate faction and random settings menus, generated objectives, scouts, convoys, air support, mortar stations and body-intel recovery. Add identified-body aggression, finite hunter UAZ deployment, dedicated objective defence and maximum AI skill with separate reduced aim accuracy. Include policy tests, native probes, evidence and documentation.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
ERR_ContentDefinition {
|
||||
Options {
|
||||
ERR_ContentOption {
|
||||
Id "rhs"
|
||||
Label "RHS: Status Quo"
|
||||
DefaultEnabled 1
|
||||
EquipmentGuids { "A1D5833FF10C6503" "695AAA77F84EB238" "333A794F0B55C019" "8D4BAA235F7EDBEA" }
|
||||
}
|
||||
ERR_ContentOption {
|
||||
Id "rhs_usmc"
|
||||
Label "RHS / US Marines"
|
||||
Parent "rhs"
|
||||
DefaultEnabled 1
|
||||
NativeFactionKey "RHS_USAF"
|
||||
GuardGroup "{19843E954790DF28}Prefabs/Groups/BLUFOR/RHS_USAF/RHS_USAF_USMC_MEF/Group_USAF_USMC_MEF_FireTeam.et"
|
||||
}
|
||||
ERR_ContentOption {
|
||||
Id "rhs_afrf"
|
||||
Label "RHS / Russian Armed Forces"
|
||||
Parent "rhs"
|
||||
DefaultEnabled 1
|
||||
NativeFactionKey "RHS_AFRF"
|
||||
GuardGroup "{AB905EF799E0A0AE}Prefabs/Groups/OPFOR/RHS_AFRF/MSV/Flora/Group_RHS_RF_MSV_Flora_FireGroup.et"
|
||||
}
|
||||
ERR_ContentOption {
|
||||
Id "rhs_ion"
|
||||
Label "RHS / ION"
|
||||
Parent "rhs"
|
||||
DefaultEnabled 1
|
||||
NativeFactionKey "RHS_ION"
|
||||
GuardGroup "{516C8C7400DF2C4D}Prefabs/Groups/INDFOR/RHS_ION/RHS_ION_COY/Group_RHS_ION_COY_StaticSecurityTeam.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{64BF38F9F92CE016}Configs/ERR/Content/Registry.conf"
|
||||
Configurations {
|
||||
ResourceClass PC { }
|
||||
ResourceClass HEADLESS : PC { }
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
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 "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
Container "{D93480F29E73C1AB}Prefabs/ERR/PrisonBackpack.et"
|
||||
ContainerOffset -4 0 0
|
||||
Stock {
|
||||
ERR_SiteStock { Prefab "{C0F7DD85A86B2900}Prefabs/Weapons/Handguns/PM/Handgun_PM.et" Count 5 }
|
||||
@@ -19,6 +21,7 @@ ERR_ScenarioDefinition {
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-supply-1"
|
||||
AllowFactionPool 1
|
||||
Label "Southern supply cache"
|
||||
Kind 4
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
@@ -38,11 +41,20 @@ ERR_ScenarioDefinition {
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-radio-1"
|
||||
RandomClear 1
|
||||
RootHeight 0.8
|
||||
Label "Radio station"
|
||||
DiscoverRadius 8
|
||||
Kind 1
|
||||
Prefab "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
|
||||
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
|
||||
}
|
||||
@@ -51,44 +63,12 @@ ERR_ScenarioDefinition {
|
||||
Label "Initial helicopter pickup site"
|
||||
Kind 2
|
||||
Prefab "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
Position 7000 0 7500
|
||||
Position 4800.953 0 11487.412
|
||||
AircraftOffset 0 0 0
|
||||
RequireAirfield 1
|
||||
Guarded 1
|
||||
GuardOffset 30 0 0
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-supply-2"
|
||||
Label "Western supply cache (vehicle maintenance)"
|
||||
Kind 4
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 5960 0 7220
|
||||
Guarded 1
|
||||
GuardOffset 16 0 0
|
||||
Patrol {
|
||||
ERR_SitePatrolPoint { Offset 18 0 0 }
|
||||
ERR_SitePatrolPoint { Offset 18 0 24 }
|
||||
ERR_SitePatrolPoint { Offset -6 0 24 }
|
||||
ERR_SitePatrolPoint { Offset -6 0 0 }
|
||||
}
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
DiscoverRadius 25
|
||||
RevealOnEscape 1
|
||||
Transport ERR_SiteTransport {
|
||||
Prefab "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
|
||||
Offset 0 0 -8
|
||||
Heading 180
|
||||
ClearanceRadius 4
|
||||
MaximumTerrainRange 1
|
||||
}
|
||||
Stock {
|
||||
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 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 4 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 6 }
|
||||
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_SiteDefinition {
|
||||
Id "everon-medical-1"
|
||||
Label "Medical stash"
|
||||
@@ -99,8 +79,9 @@ ERR_ScenarioDefinition {
|
||||
GuardOffset 16 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
DiscoverRadius 25
|
||||
RevealOnEscape 0
|
||||
RevealOnEscape 1
|
||||
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 6 }
|
||||
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||
@@ -136,5 +117,41 @@ ERR_ScenarioDefinition {
|
||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 4 0 0 Angles 90 0 0 }
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
ERR_ScenarioDefinition {
|
||||
MinimumRadioSources 2
|
||||
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
|
||||
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" }
|
||||
}
|
||||
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 -4 0 2 }
|
||||
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 4 0 3 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -3 0 -4 }
|
||||
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 3 0 -4 }
|
||||
}
|
||||
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 7000 0 7500
|
||||
Guarded 1
|
||||
GuardOffset 30 0 0
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-supply-2"
|
||||
AllowFactionPool 1
|
||||
Label "Western supply cache (vehicle maintenance)"
|
||||
Kind 4
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 5960 0 7220
|
||||
Guarded 1
|
||||
GuardOffset 16 0 0
|
||||
Patrol {
|
||||
ERR_SitePatrolPoint { Offset 18 0 0 }
|
||||
ERR_SitePatrolPoint { Offset 18 0 24 }
|
||||
ERR_SitePatrolPoint { Offset -6 0 24 }
|
||||
ERR_SitePatrolPoint { Offset -6 0 0 }
|
||||
}
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
DiscoverRadius 25
|
||||
RevealOnEscape 1
|
||||
Transport ERR_SiteTransport {
|
||||
Prefab "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
|
||||
Offset 0 0 -8
|
||||
Heading 180
|
||||
ClearanceRadius 4
|
||||
MaximumTerrainRange 1
|
||||
}
|
||||
Stock {
|
||||
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 }
|
||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 4 }
|
||||
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 8 }
|
||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 6 }
|
||||
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_SiteDefinition {
|
||||
Id "everon-medical-1"
|
||||
Label "Medical stash"
|
||||
Kind 4
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 6340 0 7400
|
||||
Guarded 0
|
||||
GuardOffset 16 0 0
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
DiscoverRadius 25
|
||||
RevealOnEscape 0
|
||||
Stock {
|
||||
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 6 }
|
||||
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 3 }
|
||||
}
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-outpost-1"
|
||||
Label "Eastern guard outpost"
|
||||
Kind 3
|
||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Position 6740 0 7300
|
||||
Guarded 1
|
||||
GuardOffset 16 0 0
|
||||
Patrol {
|
||||
ERR_SitePatrolPoint { Offset 18 0 0 }
|
||||
ERR_SitePatrolPoint { Offset 18 0 24 }
|
||||
ERR_SitePatrolPoint { Offset -6 0 24 }
|
||||
ERR_SitePatrolPoint { Offset -6 0 0 }
|
||||
}
|
||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
DiscoverRadius 35
|
||||
RevealOnEscape 0
|
||||
Stock {
|
||||
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 }
|
||||
}
|
||||
Props {
|
||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 0 0 -4 }
|
||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset -4 0 0 Angles 90 0 0 }
|
||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 4 0 0 Angles 90 0 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
ERR_AssaultParachute : "{9ADD554959C09197}Prefabs/DeployedParachutes/Core/DeployedParachuteMK4.et" {
|
||||
ID "C31F210ABE0781A1"
|
||||
components {
|
||||
SCR_AIVehicleUsageComponent "{C31F210ABE0781A2}" {
|
||||
m_eVehicleType AIRCRAFT_HELICOPTER
|
||||
m_bCanBePiloted 0
|
||||
}
|
||||
}
|
||||
m_DebugMode 0
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{C31F210ABE0781A0}Prefabs/ERR/AssaultParachute.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,27 @@
|
||||
SCR_FactionManager : "{A242612865F2A48E}Prefabs/MP/Managers/Factions/FactionManager_Base.et" {
|
||||
ID "9D8B772E349A5ED5"
|
||||
m_bCanChangeFactionsPlayable 1
|
||||
Factions {
|
||||
SCR_Faction : "{8053DB656DD18B14}Configs/Factions/FIA.conf" {
|
||||
m_aFriendlyFactionsIds { }
|
||||
}
|
||||
SCR_Faction : "{5C67A1DDE0044806}Configs/ERR/Factions/US.conf" {
|
||||
m_sFactionBackground "{F8AC305446D5D480}UI/Textures/DeployMenu/Loadout-BG/Loadout_US_BG.imageset"
|
||||
}
|
||||
SCR_Faction : "{D7A5CFA62A36480D}Configs/ERR/Factions/USSR.conf" {
|
||||
m_sFactionBackground "{21533F2089005056}UI/Textures/DeployMenu/Loadout-BG/Loadout_BG_USSR.imageset"
|
||||
}
|
||||
SCR_Faction : "{F33C7BF22CBF3A8A}Configs/Factions/RHS_US_USMC.conf" {
|
||||
m_aFriendlyFactionsIds { }
|
||||
m_bIsPlayable 0
|
||||
}
|
||||
SCR_Faction : "{627A8CDE681D223D}Configs/Factions/RHS_RF_MSV.conf" {
|
||||
m_aFriendlyFactionsIds { }
|
||||
m_bIsPlayable 0
|
||||
}
|
||||
SCR_Faction : "{CB76A36EACFBDF35}Configs/Factions/RHS_ION.conf" {
|
||||
m_aFriendlyFactionsIds { }
|
||||
m_bIsPlayable 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
GenericEntity : "{41A9C55B61F375F0}Prefabs/Items/Equipment/Backpacks/Backpack_Kolobok.et" {
|
||||
ID "ED14790A26F68453"
|
||||
components {
|
||||
InventoryStorageManagerComponent "{BF182CDF2D660373}" {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{D93480F29E73C1AB}Prefabs/ERR/PrisonBackpack.et"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
SCR_AIGroup : "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et" {
|
||||
ID "B3AC840A219FD076"
|
||||
m_faction "USSR"
|
||||
m_bSpawnImmediately 0
|
||||
m_aUnitPrefabSlots {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
GenericEntity : "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et" {
|
||||
ID "B7AD3B63842692C1"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{47D725F56A904ACE}Prefabs/ERR/RadioCamp.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
SCR_AIGroup : "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et" {
|
||||
ID "C31F210ABE078196"
|
||||
m_faction "USSR"
|
||||
m_bSpawnImmediately 0
|
||||
m_aUnitPrefabSlots {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{C31F210ABE078195}Prefabs/ERR/ReconCrew.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
Vehicle : "{7BD282AF716ED639}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_armed.et" {
|
||||
ID "C31F210ABE078193"
|
||||
components {
|
||||
ERR_ReconFlightComponent "{C31F210ABE078194}" {
|
||||
}
|
||||
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
|
||||
Simulation Helicopter "{51FAED9EF37A2D29}" {
|
||||
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
|
||||
StartUpTime 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{C31F210ABE078192}Prefabs/ERR/ReconHelicopter.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
SCR_ChimeraCharacter : "{A62FA97C4EC64F14}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_HeliPilot.et" {
|
||||
ID "C31F210ABE078198"
|
||||
components {
|
||||
BaseLoadoutManagerComponent "{520EA1D2DB11821E}" {
|
||||
Slots {
|
||||
LoadoutSlotInfo Back {
|
||||
Prefab "{54C68E438DD34265}Prefabs/Items/Equipment/Radios/Radio_R107M.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{C31F210ABE078197}Prefabs/ERR/ReconPilotEast.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
SCR_ChimeraCharacter : "{42A502E3BB727CEB}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_HeliPilot.et" {
|
||||
ID "C31F210ABE07819A"
|
||||
components {
|
||||
BaseLoadoutManagerComponent "{520EA1D2DB11821E}" {
|
||||
Slots {
|
||||
LoadoutSlotInfo Back {
|
||||
Prefab "{54C68E438DD34265}Prefabs/Items/Equipment/Radios/Radio_R107M.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{C31F210ABE078199}Prefabs/ERR/ReconPilotWest.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
SCR_ChimeraCharacter : "{976AC400219898FA}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Sharpshooter.et" {
|
||||
ID "EF871ED195094E29"
|
||||
components {
|
||||
SCR_CharacterInventoryStorageComponent "{520EA1D2DB118DAC}" {
|
||||
components {
|
||||
SCR_EquipmentStorageComponent "{56B49DA9722B635D}" {
|
||||
InitialStorageSlots {
|
||||
SCR_EquipmentStorageSlot BinocularSlot {
|
||||
Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BaseLoadoutManagerComponent "{520EA1D2DB11821E}" {
|
||||
Slots {
|
||||
LoadoutSlotInfo Back {
|
||||
Prefab "{CF862ED195094E23}Prefabs/ERR/ScoutRadioPack.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{CF862ED195094E28}Prefabs/ERR/ScoutEast.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
SCR_AIGroup : "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et" {
|
||||
ID "EF871ED195094E22"
|
||||
m_faction "USSR"
|
||||
m_bSpawnImmediately 0
|
||||
m_aUnitPrefabSlots {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{DF862ED195094E21}Prefabs/ERR/ScoutGroup.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
GenericEntity : "{41A9C55B61F375F0}Prefabs/Items/Equipment/Backpacks/Backpack_Kolobok.et" {
|
||||
ID "EF871ED195094E24"
|
||||
components {
|
||||
BaseRadioComponent "{EF871ED195094E25}" {
|
||||
"Turned on" 1
|
||||
Transceivers {
|
||||
RadioTransceiver "{EF871ED195094E26}" {
|
||||
ChannelFrequency 48000
|
||||
"Transmitting Range" 2000
|
||||
"Min tunable frequency" 20000
|
||||
"Max tunable frequency" 76000
|
||||
"Frequency resolution" 200
|
||||
}
|
||||
}
|
||||
}
|
||||
SCR_RadioComponent "{EF871ED195094E27}" {
|
||||
m_bCanBeHeld 0
|
||||
m_iRadioCategory MANPACK
|
||||
m_iRadioType R107M
|
||||
}
|
||||
SCR_UniversalInventoryStorageComponent "{5222CB03A40528FA}" {
|
||||
Attributes SCR_ItemAttributeCollection "{5222CB039E9715D1}" {
|
||||
ItemDisplayName UIInfo "{5222CB03904B414C}" {
|
||||
Name "Scout radio pack"
|
||||
Description "Long-range field radio with a compartment for spare rifle magazines."
|
||||
}
|
||||
ItemPhysAttributes ItemPhysicalAttributes PhysicalAttributes {
|
||||
Weight 10
|
||||
}
|
||||
}
|
||||
MaxCumulativeVolume 6000
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{CF862ED195094E23}Prefabs/ERR/ScoutRadioPack.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
SCR_ChimeraCharacter : "{0F6689B491641155}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Sniper.et" {
|
||||
ID "EF871ED195094E2B"
|
||||
components {
|
||||
SCR_CharacterInventoryStorageComponent "{520EA1D2DB118DAC}" {
|
||||
components {
|
||||
SCR_EquipmentStorageComponent "{56B49DA9722B635D}" {
|
||||
InitialStorageSlots {
|
||||
SCR_EquipmentStorageSlot BinocularSlot {
|
||||
Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BaseLoadoutManagerComponent "{520EA1D2DB11821E}" {
|
||||
Slots {
|
||||
LoadoutSlotInfo Back {
|
||||
Prefab "{CF862ED195094E23}Prefabs/ERR/ScoutRadioPack.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{CF862ED195094E2A}Prefabs/ERR/ScoutWest.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,27 @@ class ERR_Actors
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
Math3D.AnglesToMatrix(angles, params.Transform);
|
||||
params.Transform[3] = position;
|
||||
return GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
|
||||
IEntity entity = GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && entity)
|
||||
{
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(entity);
|
||||
if (group && (prefab.Contains("Prefabs/ERR/ConvoyCrew.et") || prefab.Contains("Prefabs/ERR/MortarCrew.et"))) group.SetFaction(mode.EnemyFaction());
|
||||
if (prefab == mode.Factions().PrisonerPrefab) SetFaction(entity, mode.PlayerFaction());
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
|
||||
static bool SetFaction(IEntity entity, Faction faction)
|
||||
{
|
||||
if (!Replication.IsServer() || !entity || !faction) return false;
|
||||
// Characters derive directly from the native base, not SCR_FactionAffiliationComponent.
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(entity.FindComponent(FactionAffiliationComponent));
|
||||
if (!affiliation) return false;
|
||||
affiliation.SetAffiliatedFaction(faction);
|
||||
return affiliation.GetAffiliatedFaction() == faction;
|
||||
}
|
||||
|
||||
static void DeleteSpawned(IEntity entity)
|
||||
{
|
||||
if (!Replication.IsServer() || !entity || Player(entity)) return;
|
||||
|
||||
@@ -8,12 +8,12 @@ class ERR_AdminMenuFields
|
||||
{
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres"};
|
||||
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount"};
|
||||
}
|
||||
static bool Available(string key, bool hq, bool intel)
|
||||
{
|
||||
if (key == "aiIntelChancePercent") return intel;
|
||||
if (key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
|
||||
if (key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
|
||||
return Keys().Contains(key);
|
||||
}
|
||||
// A short journal has no older off-screen lines, even if its slider is zero.
|
||||
@@ -30,6 +30,7 @@ class ERR_AdminMenuFields
|
||||
}
|
||||
static string Display(string key, string value)
|
||||
{
|
||||
if (value == "-1" || value == "-2") return ERR_Settings.SelectionText(value.ToInt());
|
||||
if (key == "startTimeMinutes") return ERR_TimeService.Clock(value.ToInt());
|
||||
if (key == "startDateCode") return ERR_EnvironmentService.Date(value.ToInt());
|
||||
if (key == "weatherProfile") return ERR_EnvironmentService.ProfileName(value.ToInt());
|
||||
@@ -48,6 +49,7 @@ class ERR_AdminMenuFields
|
||||
else if (key == "reportIntervalSeconds") prefix = "hq interval";
|
||||
else if (key == "supportSquadLimit") prefix = "hq squads";
|
||||
else if (key == "responseRadiusMetres") prefix = "hq radius";
|
||||
else if (key == "convoyCount") prefix = "hq convoy count";
|
||||
value = value.Trim();
|
||||
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
|
||||
return prefix + " set " + value;
|
||||
@@ -96,8 +98,13 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||
if (!m_Controller) { Close(); return; }
|
||||
array<string> names = {"Close", "OpenHQ", "BackSettings", "OpenJournal", "BackHQ", "PauseJournal", "LatestJournal", "IntelSummary", "RerollIntel"};
|
||||
foreach (string key : ERR_AdminMenuFields.Keys()) names.Insert("Apply_" + key);
|
||||
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "LatestJournal", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"};
|
||||
names.Insert("RandomDay_startTimeMinutes");
|
||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||
{
|
||||
names.Insert("Apply_" + key);
|
||||
names.Insert("Random_" + key);
|
||||
}
|
||||
foreach (string name : names)
|
||||
{
|
||||
Widget widget = GetRootWidget().FindAnyWidget(name);
|
||||
@@ -177,7 +184,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
|
||||
if (edit && edit.IsInWriteMode()) return;
|
||||
}
|
||||
if (m_Page > 0) ShowPage(m_Page - 1);
|
||||
if (m_Page != 0) ShowPage(0);
|
||||
else Close();
|
||||
}
|
||||
protected void SetText(string name, string value)
|
||||
@@ -185,28 +192,35 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
TextWidget widget = TextWidget.Cast(GetRootWidget().FindAnyWidget(name));
|
||||
if (widget) widget.SetText(value);
|
||||
}
|
||||
// Stable page order is also used by the shared navigation bar and native probe.
|
||||
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage"}; }
|
||||
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus"}; }
|
||||
protected void ShowPage(int page)
|
||||
{
|
||||
array<string> pages = Pages();
|
||||
array<string> navigation = Navigation();
|
||||
if (page < 0 || page >= pages.Count()) return;
|
||||
m_Page = page;
|
||||
GetRootWidget().FindAnyWidget("SettingsPage").SetVisible(page == 0);
|
||||
GetRootWidget().FindAnyWidget("HQPage").SetVisible(page == 1);
|
||||
GetRootWidget().FindAnyWidget("JournalPage").SetVisible(page == 2);
|
||||
if (page == 0) SetText("Title", "Mission settings");
|
||||
if (page == 1) SetText("Title", "HQ and mission status");
|
||||
if (page == 2) { SetText("Title", "HQ activity journal"); m_LogPoll = 1; }
|
||||
for (int i = 0; i < pages.Count(); i++)
|
||||
{
|
||||
GetRootWidget().FindAnyWidget(pages[i]).SetVisible(i == page);
|
||||
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(i == page);
|
||||
}
|
||||
if (page == 2) m_LogPoll = 1;
|
||||
}
|
||||
protected void UpdateActions()
|
||||
{
|
||||
foreach (SCR_ButtonBaseComponent button : m_Buttons)
|
||||
{
|
||||
string name = button.GetRootWidget().GetName();
|
||||
if (name.IndexOf("Apply_") == 0)
|
||||
if (name.IndexOf("Apply_") == 0 || name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0)
|
||||
{
|
||||
string key = name.Substring(6, name.Length() - 6);
|
||||
int prefix = name.IndexOf("_") + 1;
|
||||
string key = name.Substring(prefix, name.Length() - prefix);
|
||||
button.SetEnabled(m_Ready && m_AvailableFields.Contains(key) && ERR_AdminMenuFields.Available(key, m_HQ, m_Intel));
|
||||
}
|
||||
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
|
||||
else if (name == "OpenJournal" || name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
|
||||
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
|
||||
}
|
||||
}
|
||||
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||
@@ -221,6 +235,12 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
int divider = entry.IndexOf("=");
|
||||
if (divider <= 0) continue;
|
||||
string key = entry.Substring(0, divider);
|
||||
if (key.IndexOf("resolved_") == 0)
|
||||
{
|
||||
string resolvedKey = key.Substring(9, key.Length() - 9);
|
||||
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
|
||||
continue;
|
||||
}
|
||||
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
|
||||
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
|
||||
m_AvailableFields.Insert(key);
|
||||
@@ -290,9 +310,8 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
if (!button || !IsFocused()) return;
|
||||
string name = button.GetRootWidget().GetName();
|
||||
if (name == "Close") { Close(); return; }
|
||||
if (name == "OpenHQ" || name == "BackHQ") { ShowPage(1); return; }
|
||||
if (name == "BackSettings") { ShowPage(0); return; }
|
||||
if (name == "OpenJournal") { if (m_Ready && m_HQ) ShowPage(2); return; }
|
||||
int page = Navigation().Find(name);
|
||||
if (page >= 0) { ShowPage(page); return; }
|
||||
if (name == "PauseJournal")
|
||||
{
|
||||
m_LogPaused = !m_LogPaused;
|
||||
@@ -319,6 +338,21 @@ class ERR_AdminMenu : ChimeraMenuBase
|
||||
if (pause) pause.SetText("Pause live");
|
||||
return;
|
||||
}
|
||||
if (name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0)
|
||||
{
|
||||
int prefix = name.IndexOf("_") + 1;
|
||||
string key = name.Substring(prefix, name.Length() - prefix);
|
||||
if (!m_AvailableFields.Contains(key) || !ERR_AdminMenuFields.Available(key, m_HQ, m_Intel)) return;
|
||||
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
|
||||
if (edit)
|
||||
{
|
||||
string choice = "Random";
|
||||
if (name.IndexOf("RandomDay_") == 0) choice = "Random day";
|
||||
edit.SetValue(choice);
|
||||
SetText("Feedback", choice + " selected. Press Apply to save.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
string command;
|
||||
if (name == "IntelSummary" && m_Intel) command = "intel show";
|
||||
if (name == "RerollIntel" && m_Intel) command = "intel reroll";
|
||||
|
||||
@@ -78,8 +78,15 @@ class ERR_AdminPlayerTools
|
||||
args = args.Trim(); args.ToLower();
|
||||
bool godCommand = args == "help god" || args == "god" || args == "ungod" || args.StartsWith("god ");
|
||||
bool itemCommand = args == "help cheat" || args == "cheat" || args.StartsWith("cheat ");
|
||||
if (!godCommand && !itemCommand) return false;
|
||||
bool killCommand = args == "killais" || args == "help killais";
|
||||
if (!godCommand && !itemCommand && !killCommand) return false;
|
||||
if (args == "help killais") { replies.Insert("/esc killais: kill AI characters within 1000 m of your living character. Players and vehicles are preserved."); return true; }
|
||||
IEntity actor = CurrentBody(mode, controller);
|
||||
if (killCommand && actor)
|
||||
{
|
||||
replies.Insert(string.Format("Killed %1 AI characters within 1000 m.", ERR_DebugAI.KillNearby(mode, actor)));
|
||||
return true;
|
||||
}
|
||||
if (!actor)
|
||||
{
|
||||
Stop();
|
||||
@@ -127,8 +134,14 @@ class ERR_AdminPlayerTools
|
||||
}
|
||||
string item;
|
||||
if (args.Length() > 6) item = args.Substring(6, args.Length() - 6);
|
||||
if (item == "intel")
|
||||
{
|
||||
if (ERR_DebugIntel.SpawnSix(mode, actor)) replies.Insert("Spawned 6 intel documents on the ground nearby. Pick them up to recover intel normally.");
|
||||
else replies.Insert("Could not place 6 intel documents. Stand on clear ground outside a vehicle, with room nearby.");
|
||||
return true;
|
||||
}
|
||||
ResourceName prefab = ItemPrefab(item);
|
||||
if (prefab.IsEmpty()) { replies.Insert("Use /esc cheat map | /esc cheat helmet | /esc cheat uniform (vanilla paper map, PASGT helmet or BDU jacket)."); return true; }
|
||||
if (prefab.IsEmpty()) { replies.Insert("Use /esc cheat map | /esc cheat helmet | /esc cheat uniform | /esc cheat intel (6 documents on nearby ground; other items: vanilla paper map, PASGT helmet or BDU jacket)."); return true; }
|
||||
if (m_Grants.TimedOut) { replies.Insert("An inventory insertion is unresolved; further item grants are blocked for this character."); return true; }
|
||||
if (m_Grants.Operation) { replies.Insert("An item grant is already pending; wait for its result."); return true; }
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(actor.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
|
||||
@@ -8,6 +8,15 @@ class ERR_CommandArguments
|
||||
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
|
||||
return "";
|
||||
}
|
||||
static bool Selection(string key, string text, out int value)
|
||||
{
|
||||
if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; }
|
||||
if ((text == "random-day" || text == "random day") && key == "startTimeMinutes") { value = -2; return true; }
|
||||
if (key == "startTimeMinutes") return ERR_TimeService.ParseClock(text, value);
|
||||
if (key == "startDateCode") return ERR_EnvironmentService.ParseDate(text, value);
|
||||
if (key == "weatherProfile") return ERR_EnvironmentService.ParseProfile(text, value);
|
||||
return Integer(text, value);
|
||||
}
|
||||
static bool Integer(string text, out int value)
|
||||
{
|
||||
if (text.IsEmpty() || text.Length() > 6) return false;
|
||||
@@ -79,35 +88,36 @@ class ERR_AdminService
|
||||
if (parts.Count() == 3 && parts[2] == "show")
|
||||
{
|
||||
if (mode.Time()) replies.Insert(mode.Time().Status());
|
||||
replies.Insert("Next round: " + ERR_TimeService.Clock(mode.Settings().Get("startTimeMinutes")) + ", " + mode.Settings().Get("timeMultiplier").ToString() + "x progression.");
|
||||
replies.Insert("Next round: " + ERR_AdminMenuFields.Display("startTimeMinutes", mode.Settings().Get("startTimeMinutes").ToString()) + ", " + ERR_Settings.SelectionText(mode.Settings().Get("timeMultiplier")) + " progression.");
|
||||
return true;
|
||||
}
|
||||
int value;
|
||||
bool valid;
|
||||
if (parts.Count() == 4 && parts[2] == "set")
|
||||
{
|
||||
if (key == "startTimeMinutes") valid = ERR_TimeService.ParseClock(parts[3], value);
|
||||
else valid = ERR_CommandArguments.Integer(parts[3], value);
|
||||
valid = ERR_CommandArguments.Selection(key, parts[3], value);
|
||||
}
|
||||
if (parts.Count() == 5 && parts[2] == "set" && parts[3] == "random" && parts[4] == "day")
|
||||
valid = ERR_CommandArguments.Selection(key, "random day", value);
|
||||
if (valid && mode.Settings().Set(key, value))
|
||||
replies.Insert("Saved for next round: " + ERR_TimeService.Clock(mode.Settings().Get("startTimeMinutes")) + ", " + mode.Settings().Get("timeMultiplier").ToString() + "x progression. Current round clock unchanged.");
|
||||
else replies.Insert("Invalid time or settings could not be saved. Use mission time set HH:MM (00:00-23:59) or mission speed set N (1-24). Previous setting retained.");
|
||||
replies.Insert("Saved for next round: " + ERR_AdminMenuFields.Display("startTimeMinutes", mode.Settings().Get("startTimeMinutes").ToString()) + ", " + ERR_Settings.SelectionText(mode.Settings().Get("timeMultiplier")) + " progression. Current round clock unchanged.");
|
||||
else replies.Insert("Invalid time or settings could not be saved. Use mission time set HH:MM (00:00-23:59)|random|random-day or mission speed set N (1-24)|random. Previous setting retained.");
|
||||
return true;
|
||||
}
|
||||
protected string SettingHelp(string key)
|
||||
{
|
||||
if (key == "intelRevealChancePercent")
|
||||
return "Use /esc intel reveal set N (0-100). This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
|
||||
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
|
||||
return "";
|
||||
}
|
||||
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
|
||||
{
|
||||
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + settings.Get(key).ToString()); return; }
|
||||
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + ERR_Settings.SelectionText(settings.Get(key))); return; }
|
||||
string help = SettingHelp(key);
|
||||
if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; }
|
||||
int value;
|
||||
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Integer(parts[3], value) && settings.Set(key, value))
|
||||
replies.Insert(key + "=" + value.ToString() + " saved");
|
||||
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection(key, parts[3], value) && settings.Set(key, value))
|
||||
replies.Insert(key + "=" + ERR_Settings.SelectionText(value) + " saved");
|
||||
else
|
||||
{
|
||||
string failure = "Invalid value or settings could not be saved; previous setting retained.";
|
||||
@@ -121,9 +131,9 @@ class ERR_AdminService
|
||||
if (provider.Help(topic, replies)) return;
|
||||
if (topic == "time" || topic == "mission")
|
||||
{
|
||||
replies.Insert("/esc mission radio show|set N | mission abandon");
|
||||
replies.Insert("/esc time show|set HH:MM | mission speed show|set N; saved changes apply next round.");
|
||||
replies.Insert("/esc mission date show|set YYYY-MM-DD|authored | mission weather show|set authored|clear|overcast|rain");
|
||||
replies.Insert("/esc mission radio show|set N|random | mission abandon");
|
||||
replies.Insert("/esc time show|set HH:MM|random|random-day | mission speed show|set N|random; saved changes apply next round.");
|
||||
replies.Insert("/esc mission date show|set YYYY-MM-DD|authored|random | mission weather show|set authored|clear|overcast|rain|random");
|
||||
return;
|
||||
}
|
||||
if (topic == "intel")
|
||||
@@ -137,8 +147,8 @@ class ERR_AdminService
|
||||
replies.Insert("This command topic is unavailable in the loaded mission: " + topic + ".");
|
||||
return;
|
||||
}
|
||||
replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god");
|
||||
replies.Insert("/esc mission abandon | mission radio show|set N | time show|set HH:MM");
|
||||
replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god|killais");
|
||||
replies.Insert("/esc mission abandon | mission radio show|set N|random | time show|set HH:MM|random|random-day");
|
||||
replies.Insert("/esc mission date|weather|speed show|set VALUE | intel reveal show|set N|help");
|
||||
replies.Insert("/esc spectate next|previous; optional systems add their own command topics.");
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ class ERR_AdminSession
|
||||
array<string> status = {};
|
||||
if (!m_Mode.Settings()) { status.Insert("Mission settings are not ready."); m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status); return; }
|
||||
foreach (string key : ERR_Settings.Keys()) values.Insert(key + "=" + m_Mode.Settings().Get(key).ToString());
|
||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||
{
|
||||
if (key == "convoyCount" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile") continue;
|
||||
if (m_Mode.Settings().Get(key) == -1) values.Insert("resolved_" + key + "=" + m_Mode.Settings().Runtime(key).ToString());
|
||||
}
|
||||
m_Mode.Admin().Capabilities(values);
|
||||
m_Mode.Admin().Execute(m_Mode, true, "status", status);
|
||||
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// Scenario authors choose a pad on their island's airfield. Never relocate an
|
||||
// obstructed aircraft into nearby woods: refuse preparation before spawning it.
|
||||
class ERR_AirfieldPlacement
|
||||
{
|
||||
protected bool m_NearAirport;
|
||||
protected bool Airport(IEntity entity)
|
||||
{
|
||||
if (!entity || !entity.GetPrefabData()) return true;
|
||||
string path = entity.GetPrefabData().GetPrefabName(); path.ToLower();
|
||||
if (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/locations/eden/airport.et")) m_NearAirport = true;
|
||||
return !m_NearAirport;
|
||||
}
|
||||
static bool Clear(vector position)
|
||||
{
|
||||
if (!Replication.IsServer() || position[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
|
||||
ERR_AirfieldPlacement survey = new ERR_AirfieldPlacement();
|
||||
GetGame().GetWorld().QueryEntitiesBySphere(position, 600, survey.Airport, null, EQueryEntitiesFlags.ALL);
|
||||
if (!survey.m_NearAirport) return false;
|
||||
for (int edge = 0; edge < 4; edge++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
vector sample = position + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
|
||||
if (Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(sample) - position[1]) > 0.8) return false;
|
||||
}
|
||||
if (!SCR_WorldTools.TraceCilinderUtil(position + "0 2.5 0", 40, 4, TraceFlags.ENTS)) return false;
|
||||
TraceBox clearance = new TraceBox();
|
||||
clearance.Start = position + "0 3 0"; clearance.End = position + "0 60 0";
|
||||
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40";
|
||||
clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
return GetGame().GetWorld().TraceMove(clearance, null) >= 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Modder-facing registry. New packs/factions and their menu rows need data only.
|
||||
[BaseContainerProps()]
|
||||
class ERR_ContentOption
|
||||
{
|
||||
[Attribute()] string Id;
|
||||
[Attribute()] string Label;
|
||||
[Attribute("", UIWidgets.EditBox, "Parent pack ID; empty for a pack")]
|
||||
string Parent;
|
||||
[Attribute("1", UIWidgets.CheckBox)] bool DefaultEnabled;
|
||||
[Attribute("", UIWidgets.EditBox, "Native faction key; empty for a pack")]
|
||||
string NativeFactionKey;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
||||
ResourceName GuardGroup;
|
||||
[Attribute("", UIWidgets.EditBox, "Exact resource GUIDs for optional equipment owned by this pack")]
|
||||
ref array<string> EquipmentGuids;
|
||||
}
|
||||
|
||||
[BaseContainerProps()]
|
||||
class ERR_ContentDefinition
|
||||
{
|
||||
[Attribute()] ref array<ref ERR_ContentOption> Options;
|
||||
ERR_ContentOption Find(string id)
|
||||
{
|
||||
if (!Options) return null;
|
||||
foreach (ERR_ContentOption option : Options)
|
||||
if (option && option.Id == id) return option;
|
||||
return null;
|
||||
}
|
||||
bool Valid()
|
||||
{
|
||||
if (!Options || Options.IsEmpty() || Options.Count() > 64) return false;
|
||||
set<string> ids = new set<string>();
|
||||
set<string> factions = new set<string>();
|
||||
set<ResourceName> groups = new set<ResourceName>();
|
||||
set<string> resources = new set<string>();
|
||||
foreach (ERR_ContentOption option : Options)
|
||||
{
|
||||
if (!option || option.Id.IsEmpty() || option.Id.Length() > 48 || option.Label.IsEmpty() || ids.Contains(option.Id)) return false;
|
||||
for (int i = 0; i < option.Id.Length(); i++)
|
||||
if ("abcdefghijklmnopqrstuvwxyz0123456789_".IndexOf(option.Id.Substring(i, 1)) < 0) return false;
|
||||
ids.Insert(option.Id);
|
||||
if (!option.Parent.IsEmpty())
|
||||
{
|
||||
ERR_ContentOption parent = Find(option.Parent);
|
||||
if (!parent || !parent.Parent.IsEmpty() || !parent.NativeFactionKey.IsEmpty()) return false;
|
||||
}
|
||||
if (!option.NativeFactionKey.IsEmpty())
|
||||
{
|
||||
if (option.Parent.IsEmpty() || option.GuardGroup.IsEmpty() || factions.Contains(option.NativeFactionKey) || groups.Contains(option.GuardGroup)) return false;
|
||||
factions.Insert(option.NativeFactionKey);
|
||||
groups.Insert(option.GuardGroup);
|
||||
}
|
||||
else if (!option.GuardGroup.IsEmpty()) return false;
|
||||
if (option.EquipmentGuids)
|
||||
foreach (string guid : option.EquipmentGuids)
|
||||
{
|
||||
if (guid.Length() != 16 || resources.Contains(guid)) return false;
|
||||
for (int j = 0; j < guid.Length(); j++)
|
||||
if ("0123456789ABCDEF".IndexOf(guid.Substring(j, 1)) < 0) return false;
|
||||
resources.Insert(guid);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_ContentRegistry
|
||||
{
|
||||
protected static ref ERR_ContentDefinition s_Definition;
|
||||
static ERR_ContentDefinition Definition()
|
||||
{
|
||||
if (!s_Definition) s_Definition = ERR_ContentDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{64BF38F9F92CE016}Configs/ERR/Content/Registry.conf"));
|
||||
return s_Definition;
|
||||
}
|
||||
static array<string> Ids()
|
||||
{
|
||||
array<string> ids = {};
|
||||
ERR_ContentDefinition definition = Definition();
|
||||
if (definition && definition.Valid())
|
||||
foreach (ERR_ContentOption option : definition.Options) ids.Insert(option.Id);
|
||||
return ids;
|
||||
}
|
||||
static string Key(string id) { return "content_" + id; }
|
||||
static string Label(string id)
|
||||
{
|
||||
ERR_ContentOption option = Definition().Find(id);
|
||||
if (option) return option.Label;
|
||||
return id;
|
||||
}
|
||||
static string State(bool enabled)
|
||||
{
|
||||
if (enabled) return "Enabled";
|
||||
return "Disabled";
|
||||
}
|
||||
static string ResourcePack(ResourceName prefab)
|
||||
{
|
||||
foreach (ERR_ContentOption option : Definition().Options)
|
||||
if (option.EquipmentGuids)
|
||||
foreach (string guid : option.EquipmentGuids)
|
||||
if (prefab.Contains("{" + guid + "}")) return option.Id;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
modded class ERR_AdminMenu
|
||||
{
|
||||
protected ref map<string, Widget> m_ModRows = new map<string, Widget>();
|
||||
protected ref map<string, int> m_ModSaved = new map<string, int>();
|
||||
protected ref map<string, int> m_ModActive = new map<string, int>();
|
||||
static void OpenMods()
|
||||
{
|
||||
Open();
|
||||
ERR_AdminMenu menu = GetOpenMenu();
|
||||
if (menu) menu.ShowPage(3);
|
||||
}
|
||||
override void OnMenuOpen()
|
||||
{
|
||||
super.OnMenuOpen();
|
||||
Widget list = GetRootWidget().FindAnyWidget("ModList");
|
||||
foreach (string pack : ERR_ContentRegistry.Ids())
|
||||
{
|
||||
Widget row = GetGame().GetWorkspace().CreateWidgets("{F092D45656A8E02B}UI/layouts/ERR/ContentRow.layout", list);
|
||||
if (!row) { Close(); return; }
|
||||
m_ModRows.Set(pack, row);
|
||||
TextWidget label = TextWidget.Cast(row.FindAnyWidget("PackLabel"));
|
||||
if (label) label.SetText(ERR_ContentRegistry.Label(pack));
|
||||
SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row);
|
||||
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
|
||||
if (!enable || !disable) { Close(); return; }
|
||||
enable.m_OnClicked.Insert(OnModButton); disable.m_OnClicked.Insert(OnModButton);
|
||||
}
|
||||
UpdateActions();
|
||||
}
|
||||
override void OnMenuClose()
|
||||
{
|
||||
foreach (string pack, Widget row : m_ModRows)
|
||||
{
|
||||
SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row);
|
||||
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
|
||||
if (enable) enable.m_OnClicked.Remove(OnModButton);
|
||||
if (disable) disable.m_OnClicked.Remove(OnModButton);
|
||||
}
|
||||
m_ModRows.Clear(); m_ModSaved.Clear(); m_ModActive.Clear();
|
||||
super.OnMenuClose();
|
||||
}
|
||||
protected override void UpdateActions()
|
||||
{
|
||||
super.UpdateActions();
|
||||
foreach (string pack, Widget row : m_ModRows)
|
||||
{
|
||||
bool ready = m_Ready && m_ModSaved.Contains(pack) && m_ModActive.Contains(pack);
|
||||
SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row);
|
||||
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
|
||||
if (enable) enable.SetEnabled(ready && m_ModSaved.Get(pack) == 0);
|
||||
if (disable) disable.SetEnabled(ready && m_ModSaved.Get(pack) == 1);
|
||||
TextWidget state = TextWidget.Cast(row.FindAnyWidget("PackState"));
|
||||
if (!state) continue;
|
||||
if (!ready) state.SetText("Waiting for server settings...");
|
||||
else state.SetText("Current round: " + ERR_ContentRegistry.State(m_ModActive.Get(pack) == 1) + " | Saved preference: " + ERR_ContentRegistry.State(m_ModSaved.Get(pack) == 1));
|
||||
}
|
||||
}
|
||||
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||
{
|
||||
m_ModSaved.Clear(); m_ModActive.Clear();
|
||||
if (authorized && values)
|
||||
foreach (string pack : ERR_ContentRegistry.Ids())
|
||||
foreach (string entry : values)
|
||||
{
|
||||
if (entry == ERR_ContentRegistry.Key(pack) + "=0") m_ModSaved.Set(pack, 0);
|
||||
if (entry == ERR_ContentRegistry.Key(pack) + "=1") m_ModSaved.Set(pack, 1);
|
||||
if (entry == "activeMod_" + pack + "=0") m_ModActive.Set(pack, 0);
|
||||
if (entry == "activeMod_" + pack + "=1") m_ModActive.Set(pack, 1);
|
||||
}
|
||||
super.ReceiveSnapshot(authorized, values, status);
|
||||
}
|
||||
protected void OnModButton(SCR_ButtonBaseComponent button)
|
||||
{
|
||||
if (!button || !IsFocused() || !m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
|
||||
foreach (string pack, Widget row : m_ModRows)
|
||||
{
|
||||
string action;
|
||||
if (button.GetRootWidget() == row.FindAnyWidget("Enable")) action = "enable";
|
||||
if (button.GetRootWidget() == row.FindAnyWidget("Disable")) action = "disable";
|
||||
if (action.IsEmpty() || !m_ModSaved.Contains(pack) || !m_ModActive.Contains(pack)) continue;
|
||||
m_ClickDelay = 0.3;
|
||||
SetText("Feedback", "Saving selection on the server...");
|
||||
m_Controller.ERR_AdminSubmit("mods " + pack + " " + action, true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add one mission-specific item to the existing Esc menu. The authenticated
|
||||
// snapshot and command handlers still enforce access on the server.
|
||||
modded class PauseMenuUI
|
||||
{
|
||||
protected SCR_ButtonTextComponent m_ERRModsButton;
|
||||
override void OnMenuOpen()
|
||||
{
|
||||
super.OnMenuOpen();
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (!ERR_GameMode.Get() || !controller || !SCR_Global.IsAdmin(controller.GetPlayerId())) return;
|
||||
Widget settings = GetRootWidget().FindAnyWidget("Settings");
|
||||
if (!settings) return;
|
||||
Widget widget = GetGame().GetWorkspace().CreateWidgets("{F0351A624C87DE90}UI/layouts/ERR/PauseModsButton.layout", settings.GetParent());
|
||||
if (!widget) return;
|
||||
m_ERRModsButton = SCR_ButtonTextComponent.Cast(widget.FindHandler(SCR_ButtonTextComponent));
|
||||
if (m_ERRModsButton) m_ERRModsButton.m_OnClicked.Insert(ERR_OpenMods);
|
||||
}
|
||||
protected void ERR_OpenMods()
|
||||
{
|
||||
// Unpause local simulation before requesting an authenticated server snapshot.
|
||||
Close();
|
||||
ERR_AdminMenu.Open();
|
||||
}
|
||||
override void OnMenuClose()
|
||||
{
|
||||
if (m_ERRModsButton) m_ERRModsButton.m_OnClicked.Remove(ERR_OpenMods);
|
||||
m_ERRModsButton = null;
|
||||
super.OnMenuClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
class ERR_ContentPolicy
|
||||
{
|
||||
protected bool m_FactionsPrepared;
|
||||
protected int m_GuardOffset;
|
||||
protected ref map<string, bool> m_Active = new map<string, bool>();
|
||||
void ERR_ContentPolicy(ERR_ContentSettings settings)
|
||||
{
|
||||
foreach (string pack : ERR_ContentRegistry.Ids())
|
||||
m_Active.Set(pack, settings && settings.Enabled(pack));
|
||||
int count;
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (definition && definition.Valid())
|
||||
foreach (ERR_ContentOption option : definition.Options)
|
||||
if (!option.NativeFactionKey.IsEmpty() && Enabled(option.Id)) count++;
|
||||
if (count > 0) m_GuardOffset = Math.RandomInt(0, count);
|
||||
}
|
||||
bool Enabled(string pack)
|
||||
{
|
||||
if (pack.IsEmpty()) return true; // Base game / owned mission content.
|
||||
if (!m_Active.Contains(pack) || !m_Active.Get(pack)) return false;
|
||||
ERR_ContentOption option = ERR_ContentRegistry.Definition().Find(pack);
|
||||
return option && (option.Parent.IsEmpty() || (m_Active.Contains(option.Parent) && m_Active.Get(option.Parent)));
|
||||
}
|
||||
bool Allows(ResourceName prefab) { return Enabled(ERR_ContentRegistry.ResourcePack(prefab)); }
|
||||
bool Enemy(Faction faction)
|
||||
{
|
||||
if (!faction) return false;
|
||||
foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options)
|
||||
if (!option.NativeFactionKey.IsEmpty() && Enabled(option.Id) && option.NativeFactionKey == faction.GetFactionKey()) return true;
|
||||
return false;
|
||||
}
|
||||
ResourceName GuardGroup(int index)
|
||||
{
|
||||
array<ResourceName> groups = {};
|
||||
foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options)
|
||||
if (!option.NativeFactionKey.IsEmpty() && Enabled(option.Id)) groups.Insert(option.GuardGroup);
|
||||
if (groups.IsEmpty()) return "";
|
||||
return groups[(index + m_GuardOffset) % groups.Count()];
|
||||
}
|
||||
string GuardFaction(ResourceName prefab)
|
||||
{
|
||||
foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options)
|
||||
if (option.GuardGroup == prefab && Enabled(option.Id)) return option.NativeFactionKey;
|
||||
return "";
|
||||
}
|
||||
bool PrepareFactions(ERR_GameMode mode)
|
||||
{
|
||||
if (m_FactionsPrepared) return true;
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (!definition || !definition.Valid()) return false;
|
||||
SCR_FactionManager manager = SCR_FactionManager.Cast(GetGame().GetFactionManager());
|
||||
if (!manager) return false;
|
||||
array<SCR_Faction> factions = {};
|
||||
SCR_Faction player = SCR_Faction.Cast(mode.PlayerFaction());
|
||||
SCR_Faction primary = SCR_Faction.Cast(mode.EnemyFaction());
|
||||
if (!player || !primary) return false;
|
||||
factions.Insert(player); factions.Insert(primary);
|
||||
foreach (ERR_ContentOption option : definition.Options)
|
||||
{
|
||||
if (option.NativeFactionKey.IsEmpty() || !Enabled(option.Id)) continue;
|
||||
SCR_Faction faction = SCR_Faction.Cast(manager.GetFactionByKey(option.NativeFactionKey));
|
||||
Resource group = Resource.Load(option.GuardGroup);
|
||||
if (!faction || !group || !group.IsValid()) return false;
|
||||
if (factions.Contains(faction)) continue;
|
||||
factions.Insert(faction);
|
||||
}
|
||||
// The manager mirrors both directions and persists relationships for JIP.
|
||||
// Call both orders as an inherited one-way friendship may exist.
|
||||
foreach (SCR_Faction a : factions)
|
||||
{
|
||||
manager.SetFactionsFriendly(a, a, -1, false);
|
||||
foreach (SCR_Faction b : factions)
|
||||
{
|
||||
if (a == b) continue;
|
||||
manager.SetFactionsHostile(a, b, -1, false);
|
||||
manager.SetFactionsHostile(b, a, -1, false);
|
||||
if (!a.IsFactionEnemy(b) || !b.IsFactionEnemy(a)) return false;
|
||||
}
|
||||
}
|
||||
SCR_FactionManager.RequestUpdateAllTargetsFactions();
|
||||
m_FactionsPrepared = true;
|
||||
return true;
|
||||
}
|
||||
void Snapshot(array<string> values)
|
||||
{
|
||||
foreach (string pack : ERR_ContentRegistry.Ids())
|
||||
{
|
||||
int enabled = 0;
|
||||
if (Enabled(pack)) enabled = 1;
|
||||
values.Insert("activeMod_" + pack + "=" + enabled.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Commands inherit the existing authenticated controller/session boundary.
|
||||
class ERR_ContentAdminProvider : ERR_AdminProvider
|
||||
{
|
||||
protected ERR_ContentPolicy m_Policy;
|
||||
void ERR_ContentAdminProvider(ERR_ContentPolicy policy) { m_Policy = policy; }
|
||||
override string Id() { return "mods"; }
|
||||
override void Capabilities(array<string> values)
|
||||
{
|
||||
m_Policy.Snapshot(values);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.ContentSettings()) mode.ContentSettings().Snapshot(values);
|
||||
}
|
||||
override bool Help(string topic, array<string> replies)
|
||||
{
|
||||
if (topic != "mods") return false;
|
||||
replies.Insert("Use /esc mods show, or /esc mods <id> enable|disable. Saved selections apply after mission restart. A disabled pack also disables its factions. Workshop dependencies remain installed.");
|
||||
return true;
|
||||
}
|
||||
override bool Execute(ERR_GameMode mode, string arguments, array<string> replies)
|
||||
{
|
||||
array<string> parts = {};
|
||||
arguments.Split(" ", parts, true);
|
||||
if (parts.IsEmpty() || parts[0] != "mods") return false;
|
||||
if (arguments == "mods show")
|
||||
{
|
||||
foreach (string pack : ERR_ContentRegistry.Ids())
|
||||
replies.Insert(ERR_ContentRegistry.Label(pack) + ": current round " + ERR_ContentRegistry.State(m_Policy.Enabled(pack)) + "; saved preference " + ERR_ContentRegistry.State(mode.ContentSettings().Enabled(pack)) + ".");
|
||||
return true;
|
||||
}
|
||||
if (parts.Count() == 3 && ERR_ContentRegistry.Ids().Contains(parts[1]) && (parts[2] == "enable" || parts[2] == "disable"))
|
||||
{
|
||||
int value = 0;
|
||||
if (parts[2] == "enable") value = 1;
|
||||
if (mode.ContentSettings().Set(parts[1], value))
|
||||
replies.Insert(ERR_ContentRegistry.Label(parts[1]) + ": " + ERR_ContentRegistry.State(value == 1) + " after mission restart. Current round unchanged.");
|
||||
else replies.Insert("Could not save mod selection. Previous setting retained.");
|
||||
return true;
|
||||
}
|
||||
Help("mods", replies);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Separate, config-driven preferences. Missing newly installed options use their
|
||||
// authored defaults. Failed writes preserve both previous state and backup.
|
||||
class ERR_ContentSettings
|
||||
{
|
||||
static const string PATH = "$profile:EscapistsContent.json";
|
||||
protected string m_Path;
|
||||
protected bool m_ReadOnly;
|
||||
protected ref map<string, int> m_Values = new map<string, int>();
|
||||
void ERR_ContentSettings(string path = PATH)
|
||||
{
|
||||
m_Path = path;
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (!definition || !definition.Valid()) { m_ReadOnly = true; return; }
|
||||
foreach (ERR_ContentOption option : definition.Options)
|
||||
{
|
||||
int value = 0;
|
||||
if (option.DefaultEnabled) value = 1;
|
||||
m_Values.Set(option.Id, value);
|
||||
}
|
||||
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
|
||||
map<string, int> loaded;
|
||||
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
|
||||
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||
{
|
||||
m_ReadOnly = true;
|
||||
Print("Escapists: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
|
||||
}
|
||||
}
|
||||
bool Enabled(string id) { return m_Values.Contains(id) && m_Values.Get(id) == 1; }
|
||||
protected bool Read(string path, out map<string, int> values)
|
||||
{
|
||||
JsonLoadContext json = new JsonLoadContext();
|
||||
if (!json.LoadFromFile(path)) return false;
|
||||
values = new map<string, int>();
|
||||
foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options)
|
||||
{
|
||||
int fallback = 0;
|
||||
if (option.DefaultEnabled) fallback = 1;
|
||||
if (!json.DoesKeyExist(option.Id)) { values.Set(option.Id, fallback); continue; }
|
||||
float number;
|
||||
if (!json.ReadValue(option.Id, number) || (number != 0 && number != 1)) return false;
|
||||
values.Set(option.Id, Math.Round(number));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected bool Matches(map<string, int> values, string changed, int value)
|
||||
{
|
||||
if (!values) return false;
|
||||
foreach (string id, int original : m_Values)
|
||||
{
|
||||
int expected = original;
|
||||
if (id == changed) expected = value;
|
||||
if (!values.Contains(id) || values.Get(id) != expected) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Set(string id, int value)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ReadOnly || !m_Values.Contains(id) || (value != 0 && value != 1)) return false;
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
foreach (string option, int original : m_Values)
|
||||
{
|
||||
int saved = original;
|
||||
if (option == id) saved = value;
|
||||
json.WriteValue(option, saved);
|
||||
}
|
||||
map<string, int> checked;
|
||||
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Matches(checked, id, value)) return false;
|
||||
map<string, int> previous;
|
||||
bool hadPrevious = Read(m_Path, previous);
|
||||
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
|
||||
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked) || !Matches(checked, id, value))
|
||||
{
|
||||
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
|
||||
else if (!hadPrevious) FileIO.DeleteFile(m_Path);
|
||||
return false;
|
||||
}
|
||||
m_Values = checked;
|
||||
return true;
|
||||
}
|
||||
void Snapshot(array<string> values)
|
||||
{
|
||||
foreach (string id, int saved : m_Values) values.Insert(ERR_ContentRegistry.Key(id) + "=" + saved.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Invoked only through the authenticated, current-body admin tool boundary.
|
||||
class ERR_DebugAI
|
||||
{
|
||||
static int KillNearby(ERR_GameMode mode, IEntity center)
|
||||
{
|
||||
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !center || !GetGame().GetAIWorld()) return 0;
|
||||
array<ref ERR_PlayerLife> lives = {}; mode.Players().Participants(lives);
|
||||
array<IEntity> protectedBodies = {};
|
||||
foreach (ERR_PlayerLife life : lives) if (life.Actor) protectedBodies.Insert(life.Actor);
|
||||
array<AIAgent> agents = {}; GetGame().GetAIWorld().GetAIAgents(agents);
|
||||
array<IEntity> targets = {};
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(agent);
|
||||
if (group)
|
||||
{
|
||||
array<AIAgent> members = {}; group.GetAgents(members);
|
||||
foreach (AIAgent member : members) if (member) Add(member.GetControlledEntity(), center, protectedBodies, targets);
|
||||
}
|
||||
Add(agent.GetControlledEntity(), center, protectedBodies, targets);
|
||||
}
|
||||
int killed;
|
||||
foreach (IEntity target : targets)
|
||||
{
|
||||
// Recheck ownership immediately before damage; possession can change.
|
||||
if (!Eligible(target, center, protectedBodies)) continue;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!damage) continue;
|
||||
damage.Kill(Instigator.CreateInstigator(center));
|
||||
if (damage.IsDestroyed()) killed++;
|
||||
}
|
||||
return killed;
|
||||
}
|
||||
static bool Eligible(IEntity target, IEntity center, array<IEntity> protectedBodies)
|
||||
{
|
||||
return target && ChimeraCharacter.Cast(target) && target != center && ERR_Actors.Alive(target) && !ERR_Actors.Player(target) && !protectedBodies.Contains(target) && vector.Distance(target.GetOrigin(), center.GetOrigin()) <= 1000;
|
||||
}
|
||||
protected static void Add(IEntity target, IEntity center, array<IEntity> protectedBodies, array<IEntity> targets)
|
||||
{
|
||||
if (Eligible(target, center, protectedBodies) && !targets.Contains(target)) targets.Insert(target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
class ERR_DebugIntel
|
||||
{
|
||||
static bool SpawnSix(ERR_GameMode mode, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !mode || mode != ERR_GameMode.Get() || !mode.Active() || !ERR_Actors.CanFight(actor) || !mode.Players().CurrentActor(actor)) return false;
|
||||
ERR_SpawnPlacement placement = new ERR_SpawnPlacement();
|
||||
map<int, vector> positions = new map<int, vector>();
|
||||
// Survey all six spots first, including solid obstructions and the path from
|
||||
// the player. This also refuses airborne/seated players and deep water.
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
vector position;
|
||||
if (!placement.Near(actor, positions, position)) return false;
|
||||
positions.Set(i, position);
|
||||
}
|
||||
array<IEntity> spawned = {};
|
||||
for (int j = 0; j < 6; j++)
|
||||
{
|
||||
IEntity document = ERR_Actors.Spawn(ERR_Resources.IntelFolder(), positions.Get(j));
|
||||
if (!document)
|
||||
{
|
||||
foreach (IEntity partial : spawned) ERR_Actors.DeleteSpawned(partial);
|
||||
return false;
|
||||
}
|
||||
spawned.Insert(document);
|
||||
}
|
||||
foreach (IEntity intel : spawned) mode.Own(intel);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,38 @@ class ERR_DiscoveryMapRules
|
||||
// that boundary so native edit/drag cannot move a discovered objective.
|
||||
class ERR_DiscoveryMarker : SCR_MapMarkerBase
|
||||
{
|
||||
protected string m_Icon = "supplies";
|
||||
static string Icon(ERR_LocationKind kind, string id)
|
||||
{
|
||||
if (kind == ERR_LocationKind.EXTRACTION) return "helicopters";
|
||||
if (kind == ERR_LocationKind.RADIO) return "radio";
|
||||
if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission";
|
||||
if (kind == ERR_LocationKind.OUTPOST) return "building";
|
||||
if (id.Contains("medical")) return "medical";
|
||||
return "supplies";
|
||||
}
|
||||
void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2)
|
||||
{
|
||||
m_Icon = "mark-question";
|
||||
if (knowledge >= 2) m_Icon = Icon(kind, id);
|
||||
ApplyIcon();
|
||||
}
|
||||
protected void ApplyIcon()
|
||||
{
|
||||
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
|
||||
if (!GetRootWidget() || !widget) return;
|
||||
if (m_Icon == "mark-question")
|
||||
{
|
||||
widget.SetImage("{E23427CAC80DA8B7}UI/Textures/Icons/icons_mapMarkersUI.imageset", m_Icon);
|
||||
widget.SetGlowImage("{67B3A6DC2D712B52}UI/Textures/Icons/icons_mapMarkersUI-glow.imageset", m_Icon);
|
||||
}
|
||||
else
|
||||
{
|
||||
widget.SetImage("{3262679C50EF4F01}UI/Textures/Icons/icons_wrapperUI.imageset", m_Icon);
|
||||
widget.SetGlowImage("{00FE3DBDFD15227B}UI/Textures/Icons/icons_wrapperUI-glow.imageset", m_Icon);
|
||||
}
|
||||
widget.SetColor(Color.FromSRGBA(35, 45, 50, 255));
|
||||
}
|
||||
void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); }
|
||||
void SetDiscoveryPosition(vector position, int knowledge)
|
||||
{
|
||||
@@ -25,6 +57,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
|
||||
// Text is scenario-authored, never user input. Avoid delayed UGC callbacks
|
||||
// overwriting a newer discovery label after the location becomes identified.
|
||||
super.OnCreateMarker(true);
|
||||
ApplyIcon();
|
||||
}
|
||||
override void OnDelete()
|
||||
{
|
||||
@@ -73,17 +106,26 @@ modded class ERR_LocationComponent
|
||||
}
|
||||
protected void ERR_UpdateMarker()
|
||||
{
|
||||
if (!GetGame().GetPlayerController() || Knowledge() <= 0 || (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel)) return;
|
||||
if (!GetGame().GetPlayerController()) return;
|
||||
if (Kind() == ERR_LocationKind.PRISON || Knowledge() < 1)
|
||||
{
|
||||
SCR_MapMarkerManagerComponent oldManager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (m_ERRMarker && oldManager) oldManager.ERR_RemoveDiscoveryMarker(m_ERRMarker);
|
||||
else if (m_ERRMarker) m_ERRMarker.OnDelete();
|
||||
m_ERRMarker = null; m_ERRShown = 0; m_ERRShownLabel = "";
|
||||
return;
|
||||
}
|
||||
if (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel) return;
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!manager || !manager.GetMarkerConfig()) return;
|
||||
string label = "Search area (500 m square)";
|
||||
string label = "Unknown location (500 m area)";
|
||||
if (Knowledge() == 2)
|
||||
{
|
||||
if (!Label().IsEmpty()) label = Label();
|
||||
else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
|
||||
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Initial helicopter pickup site";
|
||||
else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache";
|
||||
else if (Kind() == ERR_LocationKind.PRISON) label = "Prison";
|
||||
else if (Kind() == ERR_LocationKind.MORTAR) label = "Mortar site";
|
||||
else label = Id();
|
||||
}
|
||||
if (!m_ERRMarker)
|
||||
@@ -92,10 +134,12 @@ modded class ERR_LocationComponent
|
||||
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
|
||||
m_ERRMarker.SetMarkerConfigID(0);
|
||||
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge());
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
manager.InsertStaticMarker(m_ERRMarker, true);
|
||||
}
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
|
||||
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge());
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
// Reactivate a previously culled rough marker if the exact point moves
|
||||
// into the open map's view. Native update performs normal visibility checks.
|
||||
|
||||
@@ -57,18 +57,24 @@ class ERR_DiscoveryService
|
||||
m_Mode.Locations(locations);
|
||||
array<ERR_LocationComponent> eligible = {};
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
|
||||
string sealedTarget = m_Mode.Evidence().Target(folder);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
if (!location || location.Knowledge() >= 2) continue;
|
||||
if (!sealedTarget.IsEmpty() && location.Id() != sealedTarget) continue;
|
||||
if (document)
|
||||
{
|
||||
if (location.Id() == document.TargetId()) eligible.Insert(location);
|
||||
}
|
||||
else if (location.Kind() != ERR_LocationKind.PRISON && location.Kind() != ERR_LocationKind.EXTRACTION) eligible.Insert(location);
|
||||
}
|
||||
if (eligible.IsEmpty()) continue;
|
||||
if (eligible.IsEmpty())
|
||||
{
|
||||
if (!document && MarkSpent(folder)) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
|
||||
continue;
|
||||
}
|
||||
ERR_LocationComponent chosen = eligible.GetRandomElement();
|
||||
if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Get("intelRevealChancePercent"))) continue;
|
||||
if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Runtime("intelRevealChancePercent"))) continue;
|
||||
m_Spent.Insert(folder);
|
||||
PrintFormat("Reintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
|
||||
}
|
||||
|
||||
@@ -66,12 +66,40 @@ class ERR_EnvironmentService
|
||||
if (state && state.GetStateName() == name) return true;
|
||||
return false;
|
||||
}
|
||||
static int DateFromOrdinal(int year, int ordinal)
|
||||
{
|
||||
if (year < 1900 || year > 2050 || ordinal < 0) return -1;
|
||||
for (int month = 1; month <= 12; month++)
|
||||
{
|
||||
int days = 31;
|
||||
if (month == 4 || month == 6 || month == 9 || month == 11) days = 30;
|
||||
if (month == 2)
|
||||
{
|
||||
days = 28;
|
||||
if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) days = 29;
|
||||
}
|
||||
if (ordinal < days) return (year - 1900) * 10000 + month * 100 + ordinal + 1;
|
||||
ordinal -= days;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
static int RandomDate(int year)
|
||||
{
|
||||
int days = 365;
|
||||
if (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)) days = 366;
|
||||
return DateFromOrdinal(year, Math.RandomInt(0, days));
|
||||
}
|
||||
bool Start(ERR_GameMode mode)
|
||||
{
|
||||
if (!Replication.IsServer() || !mode || !mode.Settings()) return false;
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
if (!world) return false;
|
||||
return StartOn(world.GetTimeAndWeatherManager(), mode.Settings().Get("startDateCode"), mode.Settings().Get("weatherProfile"));
|
||||
if (m_Started) return !m_Stopped;
|
||||
TimeAndWeatherManagerEntity manager = world.GetTimeAndWeatherManager();
|
||||
if (!manager) return false;
|
||||
int dateCode = mode.Settings().Get("startDateCode");
|
||||
if (dateCode == -1) dateCode = RandomDate(manager.GetYear());
|
||||
return StartOn(manager, dateCode, mode.Settings().Runtime("weatherProfile"));
|
||||
}
|
||||
protected bool StartOn(TimeAndWeatherManagerEntity manager, int dateCode, int profile)
|
||||
{
|
||||
@@ -144,7 +172,7 @@ class ERR_EnvironmentCommands
|
||||
if (parts.Count() == 3 && parts[2] == "show")
|
||||
{
|
||||
if (environment) replies.Insert(environment.Status());
|
||||
replies.Insert("Next round: date " + ERR_EnvironmentService.Date(settings.Get("startDateCode")) + ", weather " + ERR_EnvironmentService.ProfileName(settings.Get("weatherProfile")) + ".");
|
||||
replies.Insert("Next round: date " + ERR_AdminMenuFields.Display("startDateCode", settings.Get("startDateCode").ToString()) + ", weather " + ERR_AdminMenuFields.Display("weatherProfile", settings.Get("weatherProfile").ToString()) + ".");
|
||||
return true;
|
||||
}
|
||||
int value;
|
||||
@@ -153,12 +181,11 @@ class ERR_EnvironmentCommands
|
||||
if (parts[1] == "weather") key = "weatherProfile";
|
||||
if (parts.Count() == 4 && parts[2] == "set")
|
||||
{
|
||||
if (key == "startDateCode") valid = ERR_EnvironmentService.ParseDate(parts[3], value);
|
||||
else valid = ERR_EnvironmentService.ParseProfile(parts[3], value);
|
||||
valid = ERR_CommandArguments.Selection(key, parts[3], value);
|
||||
}
|
||||
if (valid && settings.Set(key, value))
|
||||
replies.Insert("Saved for next round: date " + ERR_EnvironmentService.Date(settings.Get("startDateCode")) + ", weather " + ERR_EnvironmentService.ProfileName(settings.Get("weatherProfile")) + ". Current round unchanged.");
|
||||
else replies.Insert("Invalid environment or settings could not be saved. Use mission date set YYYY-MM-DD (1900-2050) or authored; mission weather set authored|clear|overcast|rain. Previous setting retained.");
|
||||
replies.Insert("Saved for next round: date " + ERR_AdminMenuFields.Display("startDateCode", settings.Get("startDateCode").ToString()) + ", weather " + ERR_AdminMenuFields.Display("weatherProfile", settings.Get("weatherProfile").ToString()) + ". Current round unchanged.");
|
||||
else replies.Insert("Invalid environment or settings could not be saved. Use mission date set YYYY-MM-DD (1900-2050) or authored|random; mission weather set authored|clear|overcast|rain|random. Previous setting retained.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
// Uses only authenticated server snapshots for eligibility and saved role choices.
|
||||
modded class ERR_AdminMenu
|
||||
{
|
||||
protected ref array<string> m_FactionIds = {};
|
||||
protected ref array<SCR_ButtonTextComponent> m_FactionButtons = {};
|
||||
protected ref map<string, string> m_FactionSaved = new map<string, string>();
|
||||
protected ref map<string, string> m_FactionActive = new map<string, string>();
|
||||
protected bool m_FactionFieldsInitialized;
|
||||
override void OnMenuOpen()
|
||||
{
|
||||
super.OnMenuOpen();
|
||||
array<string> names = {"FactionsApply", "FactionsRandomAll"};
|
||||
foreach (string name : names)
|
||||
{
|
||||
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(name, GetRootWidget());
|
||||
if (!button) { Close(); return; }
|
||||
button.m_OnClicked.Insert(OnFactionButton); m_FactionButtons.Insert(button);
|
||||
}
|
||||
UpdateActions();
|
||||
}
|
||||
override void OnMenuClose()
|
||||
{
|
||||
foreach (SCR_ButtonTextComponent button : m_FactionButtons) if (button) button.m_OnClicked.Remove(OnFactionButton);
|
||||
m_FactionButtons.Clear(); m_FactionIds.Clear(); m_FactionSaved.Clear(); m_FactionActive.Clear(); m_FactionFieldsInitialized = false;
|
||||
super.OnMenuClose();
|
||||
}
|
||||
protected override void UpdateActions()
|
||||
{
|
||||
super.UpdateActions();
|
||||
bool ready = m_Ready && m_FactionSaved.Count() == 3 && m_FactionActive.Count() == 3;
|
||||
foreach (SCR_ButtonTextComponent button : m_FactionButtons)
|
||||
if (button) button.SetEnabled(ready);
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
|
||||
if (combo) combo.SetEnabled(ready);
|
||||
if (ready) SetText("FactionState_" + role, "Current: " + ERR_FactionChoices.Label(m_FactionActive.Get(role)) + " | Saved: " + ERR_FactionChoices.Label(m_FactionSaved.Get(role)));
|
||||
}
|
||||
}
|
||||
override void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||
{
|
||||
m_FactionSaved.Clear(); m_FactionActive.Clear();
|
||||
array<string> available = {"random"};
|
||||
if (authorized && values)
|
||||
foreach (string value : values)
|
||||
{
|
||||
array<string> pair = {}; value.Split("=", pair, true);
|
||||
if (pair.Count() != 2) continue;
|
||||
if (pair[0] == "factionAvailable" && ERR_FactionChoices.Find(pair[1]) && !available.Contains(pair[1])) available.Insert(pair[1]);
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
if (pair[0] == "factionSaved_" + role) m_FactionSaved.Set(role, pair[1]);
|
||||
if (pair[0] == "factionActive_" + role) m_FactionActive.Set(role, pair[1]);
|
||||
}
|
||||
}
|
||||
bool changed = available.Count() != m_FactionIds.Count();
|
||||
if (!changed) for (int i = 0; i < available.Count(); i++) if (available[i] != m_FactionIds[i]) changed = true;
|
||||
if (authorized && m_FactionSaved.Count() == 3 && (changed || !m_FactionFieldsInitialized))
|
||||
{
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
|
||||
if (!combo) continue;
|
||||
string draft = m_FactionSaved.Get(role);
|
||||
int previous = combo.GetCurrentIndex();
|
||||
if (m_FactionFieldsInitialized && previous >= 0 && previous < m_FactionIds.Count()) draft = m_FactionIds[previous];
|
||||
combo.ClearAll();
|
||||
foreach (string id : available) combo.AddItem(ERR_FactionChoices.Label(id));
|
||||
combo.SetCurrentItem(Math.Max(0, available.Find(draft)), false, false, false);
|
||||
}
|
||||
m_FactionIds.Copy(available); m_FactionFieldsInitialized = true;
|
||||
}
|
||||
super.ReceiveSnapshot(authorized, values, status);
|
||||
}
|
||||
protected void OnFactionButton(SCR_ButtonBaseComponent button)
|
||||
{
|
||||
if (!button) return;
|
||||
string name = button.GetRootWidget().GetName();
|
||||
if (!m_Ready || !IsFocused() || !m_Controller || m_Age > 6 || m_ClickDelay > 0 || !m_FactionFieldsInitialized) return;
|
||||
if (name == "FactionsRandomAll")
|
||||
{
|
||||
foreach (string role : ERR_FactionChoices.Roles()) SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget()).SetCurrentItem(0);
|
||||
return;
|
||||
}
|
||||
if (name != "FactionsApply") return;
|
||||
string command = "factions set";
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
int index = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget()).GetCurrentIndex();
|
||||
if (index < 0 || index >= m_FactionIds.Count()) return;
|
||||
command += " " + m_FactionIds[index];
|
||||
}
|
||||
m_ClickDelay = 0.3; SetText("Feedback", "Saving faction selections...");
|
||||
m_Controller.ERR_AdminSubmit(command, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
// Saved role choices are stable registry IDs, never menu indexes. Random resolves
|
||||
// once per round on the server, without replacement across the three roles.
|
||||
class ERR_FactionChoice
|
||||
{
|
||||
string Id;
|
||||
string Label;
|
||||
string Key;
|
||||
ResourceName Group;
|
||||
void ERR_FactionChoice(string id, string label, string key, ResourceName group)
|
||||
{ Id = id; Label = label; Key = key; Group = group; }
|
||||
}
|
||||
class ERR_FactionChoices
|
||||
{
|
||||
static array<string> Roles() { return {"escapists", "enemy", "independent"}; }
|
||||
static array<ref ERR_FactionChoice> All()
|
||||
{
|
||||
array<ref ERR_FactionChoice> choices = {};
|
||||
choices.Insert(new ERR_FactionChoice("us", "US Army", "US", "{84E5BBAB25EA23E5}Prefabs/Groups/BLUFOR/Group_US_FireTeam.et"));
|
||||
choices.Insert(new ERR_FactionChoice("ussr", "Soviet Army", "USSR", "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"));
|
||||
choices.Insert(new ERR_FactionChoice("fia", "FIA", "FIA", "{5BEA04939D148B1D}Prefabs/Groups/INDFOR/Group_FIA_FireTeam.et"));
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (definition && definition.Valid())
|
||||
foreach (ERR_ContentOption option : definition.Options)
|
||||
{
|
||||
if (option.NativeFactionKey.IsEmpty()) continue;
|
||||
bool duplicate;
|
||||
foreach (ERR_FactionChoice existing : choices) if (existing.Id == option.Id || existing.Key == option.NativeFactionKey) duplicate = true;
|
||||
if (!duplicate) choices.Insert(new ERR_FactionChoice(option.Id, option.Label, option.NativeFactionKey, option.GuardGroup));
|
||||
}
|
||||
return choices;
|
||||
}
|
||||
static ERR_FactionChoice Find(string id)
|
||||
{
|
||||
foreach (ERR_FactionChoice choice : All()) if (choice.Id == id) return choice;
|
||||
return null;
|
||||
}
|
||||
static string Label(string id)
|
||||
{
|
||||
if (id == "random") return "Random";
|
||||
ERR_FactionChoice choice = Find(id);
|
||||
if (choice) return choice.Label;
|
||||
return "Unavailable (" + id + ")";
|
||||
}
|
||||
static bool Enabled(string id, ERR_ContentSettings settings)
|
||||
{
|
||||
if (id == "us" || id == "ussr" || id == "fia") return true;
|
||||
ERR_ContentOption option = ERR_ContentRegistry.Definition().Find(id);
|
||||
return option && settings && settings.Enabled(id) && (option.Parent.IsEmpty() || settings.Enabled(option.Parent));
|
||||
}
|
||||
static bool Resolve(array<string> available, map<string, string> saved, map<string, string> resolved)
|
||||
{
|
||||
resolved.Clear();
|
||||
if (available.Count() < 3 || !Distinct(saved)) return false;
|
||||
// Reserve every explicit choice first, so Random cannot steal a later role.
|
||||
foreach (string fixedRole : Roles())
|
||||
{
|
||||
string fixedId = saved.Get(fixedRole);
|
||||
if (fixedId == "random" || !available.Contains(fixedId)) continue;
|
||||
resolved.Set(fixedRole, fixedId); available.RemoveItem(fixedId);
|
||||
}
|
||||
foreach (string randomRole : Roles())
|
||||
{
|
||||
if (resolved.Contains(randomRole)) continue;
|
||||
if (available.IsEmpty()) return false;
|
||||
int index = Math.RandomInt(0, available.Count());
|
||||
resolved.Set(randomRole, available[index]); available.Remove(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static bool Distinct(map<string, string> selections)
|
||||
{
|
||||
array<string> fixed = {};
|
||||
foreach (string role : Roles())
|
||||
{
|
||||
if (!selections.Contains(role)) return false;
|
||||
string id = selections.Get(role);
|
||||
if (id == "random") continue;
|
||||
if (!Find(id) || fixed.Contains(id)) return false;
|
||||
fixed.Insert(id);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_FactionSelection
|
||||
{
|
||||
static const string PATH = "$profile:EscapistsFactions.json";
|
||||
protected string m_Path;
|
||||
protected bool m_ReadOnly;
|
||||
protected bool m_Ready;
|
||||
protected ref map<string, string> m_Saved = new map<string, string>();
|
||||
protected ref map<string, string> m_Resolved = new map<string, string>();
|
||||
void ERR_FactionSelection(string path = PATH)
|
||||
{
|
||||
m_Path = path;
|
||||
m_Saved.Set("escapists", "us"); m_Saved.Set("enemy", "ussr"); m_Saved.Set("independent", "fia");
|
||||
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
|
||||
map<string, string> loaded;
|
||||
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Saved = loaded;
|
||||
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||
{ m_ReadOnly = true; Print("Escapists: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); }
|
||||
}
|
||||
protected bool Read(string path, out map<string, string> values)
|
||||
{
|
||||
JsonLoadContext json = new JsonLoadContext();
|
||||
if (!json.LoadFromFile(path)) return false;
|
||||
values = new map<string, string>();
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
string id;
|
||||
if (!json.ReadValue(role, id) || (id != "random" && !ERR_FactionChoices.Find(id))) return false;
|
||||
values.Set(role, id);
|
||||
}
|
||||
return ERR_FactionChoices.Distinct(values);
|
||||
}
|
||||
protected bool Same(map<string, string> a, map<string, string> b)
|
||||
{
|
||||
if (!a || !b) return false;
|
||||
foreach (string role : ERR_FactionChoices.Roles()) if (!a.Contains(role) || !b.Contains(role) || a.Get(role) != b.Get(role)) return false;
|
||||
return true;
|
||||
}
|
||||
string Saved(string role) { return m_Saved.Get(role); }
|
||||
string Active(string role) { return m_Resolved.Get(role); }
|
||||
bool Ready() { return m_Ready; }
|
||||
ERR_FactionChoice Choice(string role) { return ERR_FactionChoices.Find(Active(role)); }
|
||||
ResourceName Soldier()
|
||||
{
|
||||
ERR_FactionChoice enemy = Choice("enemy");
|
||||
if (!enemy) return "";
|
||||
Resource resource = Resource.Load(enemy.Group);
|
||||
if (!resource || !resource.IsValid()) return "";
|
||||
IEntitySource source = resource.GetResource().ToEntitySource();
|
||||
array<ResourceName> members = {};
|
||||
if (!source || !source.Get("m_aUnitPrefabSlots", members) || members.IsEmpty()) return "";
|
||||
return members[0];
|
||||
}
|
||||
bool Save(array<string> ids, ERR_ContentSettings content)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ReadOnly || !ids || ids.Count() != 3) return false;
|
||||
map<string, string> values = new map<string, string>();
|
||||
array<string> roles = ERR_FactionChoices.Roles();
|
||||
for (int i = 0; i < roles.Count(); i++)
|
||||
{
|
||||
if (ids[i] != "random" && !ERR_FactionChoices.Enabled(ids[i], content)) return false;
|
||||
values.Set(roles[i], ids[i]);
|
||||
}
|
||||
if (!ERR_FactionChoices.Distinct(values)) return false;
|
||||
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
|
||||
foreach (string role : roles) json.WriteValue(role, values.Get(role));
|
||||
map<string, string> checked;
|
||||
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Same(values, checked)) return false;
|
||||
map<string, string> previous;
|
||||
bool hadPrevious = Read(m_Path, previous);
|
||||
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
|
||||
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked) || !Same(values, checked))
|
||||
{
|
||||
if (hadPrevious) FileIO.CopyFile(m_Path + ".bak", m_Path);
|
||||
else FileIO.DeleteFile(m_Path);
|
||||
return false;
|
||||
}
|
||||
m_Saved = checked; return true;
|
||||
}
|
||||
bool Prepare(ERR_GameMode mode)
|
||||
{
|
||||
if (m_Ready) return true;
|
||||
if (!Replication.IsServer() || !mode || !mode.Factions()) return false;
|
||||
m_Resolved.Clear();
|
||||
array<string> available = {};
|
||||
foreach (ERR_FactionChoice option : ERR_FactionChoices.All())
|
||||
{
|
||||
if (!ERR_FactionChoices.Enabled(option.Id, mode.ContentSettings())) continue;
|
||||
if (!GetGame().GetFactionManager().GetFactionByKey(option.Key)) continue;
|
||||
Resource resource = Resource.Load(option.Group);
|
||||
if (resource && resource.IsValid()) available.Insert(option.Id);
|
||||
}
|
||||
if (available.Count() < 3) return false;
|
||||
if (!ERR_FactionChoices.Resolve(available, m_Saved, m_Resolved)) return false;
|
||||
mode.Factions().PlayerFactionKey = Choice("escapists").Key;
|
||||
mode.Factions().EnemyFactionKey = Choice("enemy").Key;
|
||||
mode.Factions().GuardGroupPrefab = Choice("enemy").Group;
|
||||
SCR_FactionManager manager = SCR_FactionManager.Cast(GetGame().GetFactionManager());
|
||||
if (!manager) return false;
|
||||
foreach (string a : ERR_FactionChoices.Roles())
|
||||
foreach (string b : ERR_FactionChoices.Roles())
|
||||
{
|
||||
SCR_Faction first = SCR_Faction.Cast(manager.GetFactionByKey(Choice(a).Key));
|
||||
SCR_Faction second = SCR_Faction.Cast(manager.GetFactionByKey(Choice(b).Key));
|
||||
if (a == b) manager.SetFactionsFriendly(first, second, -1, false);
|
||||
else manager.SetFactionsHostile(first, second, -1, false);
|
||||
}
|
||||
SCR_Faction player = SCR_Faction.Cast(manager.GetFactionByKey(Choice("escapists").Key));
|
||||
player.SetIsPlayable(true);
|
||||
if (!player.IsPlayable()) return false;
|
||||
m_Ready = true;
|
||||
mode.PublishFactionRoles(Choice("escapists").Key, Choice("enemy").Key);
|
||||
PrintFormat("Escapists factions: players=%1 enemy=%2 independent=%3", Choice("escapists").Key, Choice("enemy").Key, Choice("independent").Key);
|
||||
return true;
|
||||
}
|
||||
void Snapshot(ERR_ContentSettings content, array<string> values)
|
||||
{
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
values.Insert("factionSaved_" + role + "=" + Saved(role));
|
||||
values.Insert("factionActive_" + role + "=" + Active(role));
|
||||
}
|
||||
foreach (ERR_FactionChoice option : ERR_FactionChoices.All())
|
||||
if (ERR_FactionChoices.Enabled(option.Id, content)) values.Insert("factionAvailable=" + option.Id);
|
||||
}
|
||||
}
|
||||
class ERR_FactionAdminProvider : ERR_AdminProvider
|
||||
{
|
||||
override string Id() { return "factions"; }
|
||||
override void Capabilities(array<string> values)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection()) mode.FactionSelection().Snapshot(mode.ContentSettings(), values);
|
||||
}
|
||||
override bool Execute(ERR_GameMode mode, string arguments, array<string> replies)
|
||||
{
|
||||
array<string> parts = {}; arguments.Split(" ", parts, true);
|
||||
if (parts.IsEmpty() || parts[0] != "factions") return false;
|
||||
if (parts.Count() == 5 && parts[1] == "set")
|
||||
{
|
||||
array<string> ids = {parts[2], parts[3], parts[4]};
|
||||
if (mode.FactionSelection().Save(ids, mode.ContentSettings())) replies.Insert("Faction selections saved for next round. Current factions unchanged.");
|
||||
else replies.Insert("Could not save factions. Choose three distinct enabled factions, or Random. Previous selections retained.");
|
||||
return true;
|
||||
}
|
||||
foreach (string role : ERR_FactionChoices.Roles()) replies.Insert(role + ": " + ERR_FactionChoices.Label(mode.FactionSelection().Active(role)) + "; next round: " + ERR_FactionChoices.Label(mode.FactionSelection().Saved(role)));
|
||||
replies.Insert("Use the Factions menu, or /esc factions set <escapists> <enemy> <independent>; each accepts a faction ID or random.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Only the live mission policy follows role selections. Isolated policy probes
|
||||
// and registry tooling keep the generic content eligibility behavior.
|
||||
modded class ERR_ContentPolicy
|
||||
{
|
||||
protected ERR_FactionSelection Roles()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.Content() == this && mode.FactionSelection() && mode.FactionSelection().Ready()) return mode.FactionSelection();
|
||||
return null;
|
||||
}
|
||||
override bool Enemy(Faction faction)
|
||||
{
|
||||
ERR_FactionSelection roles = Roles();
|
||||
if (roles) return faction && faction.GetFactionKey() == roles.Choice("independent").Key;
|
||||
return super.Enemy(faction);
|
||||
}
|
||||
override ResourceName GuardGroup(int index)
|
||||
{
|
||||
ERR_FactionSelection roles = Roles();
|
||||
if (!roles) return super.GuardGroup(index);
|
||||
if (index % 2 == 0) return roles.Choice("independent").Group;
|
||||
return roles.Choice("enemy").Group;
|
||||
}
|
||||
override string GuardFaction(ResourceName prefab)
|
||||
{
|
||||
ERR_FactionSelection roles = Roles();
|
||||
if (roles)
|
||||
{
|
||||
if (prefab == roles.Choice("independent").Group) return roles.Choice("independent").Key;
|
||||
if (prefab == roles.Choice("enemy").Group) return roles.Choice("enemy").Key;
|
||||
return "";
|
||||
}
|
||||
return super.GuardFaction(prefab);
|
||||
}
|
||||
}
|
||||
@@ -104,9 +104,9 @@ class ERR_ForceLedger
|
||||
if (group && FindGroup(group)) return null;
|
||||
// Retired source IDs also occupy the finite pool: re-registering a removed
|
||||
// formation cannot replace its casualties with a fresh full-strength unit.
|
||||
if (m_Records.Count() >= 128 || m_Sources.Count() >= 128)
|
||||
if (m_Records.Count() >= 256 || m_Sources.Count() >= 256)
|
||||
{
|
||||
if (!m_CapLogged) Print("Reintroduced [ledger]: 128-formation registration cap reached.");
|
||||
if (!m_CapLogged) Print("Reintroduced [ledger]: 256-formation registration cap reached.");
|
||||
m_CapLogged = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
[RplProp(), Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; configured from authored scenario")]
|
||||
protected vector m_PrisonPosition;
|
||||
[RplProp()] protected float m_PrisonHeading;
|
||||
float PrisonHeading() { return m_PrisonHeading; }
|
||||
[Attribute("6090 0 7160", UIWidgets.Coords, "Centre of extraction boundary")]
|
||||
protected vector m_EscapeCenter;
|
||||
[RplProp(), Attribute("9000", UIWidgets.EditBox, "Helicopter must cross this radius to escape")]
|
||||
@@ -14,7 +16,18 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
[RplProp()]
|
||||
protected string m_PreparationError;
|
||||
protected ref ERR_FactionCatalog m_Factions;
|
||||
protected ref ERR_FactionSelection m_FactionSelection;
|
||||
[RplProp()] protected string m_SelectedPlayerFaction;
|
||||
[RplProp()] protected string m_SelectedEnemyFaction;
|
||||
ERR_FactionSelection FactionSelection() { return m_FactionSelection; }
|
||||
void PublishFactionRoles(string player, string enemy)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe();
|
||||
}
|
||||
protected ref ERR_Settings m_Settings;
|
||||
protected ref ERR_ContentPolicy m_Content;
|
||||
protected ref ERR_ContentSettings m_ContentSettings;
|
||||
protected ref ERR_TimeService m_Time = new ERR_TimeService();
|
||||
protected ref ERR_EnvironmentService m_Environment = new ERR_EnvironmentService();
|
||||
protected ref ERR_DiscoveryService m_Discovery;
|
||||
@@ -60,6 +73,12 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
ERR_PlayerRoster Players() { return m_Roster; }
|
||||
ERR_AdminService Admin() { return m_Admin; }
|
||||
ERR_Settings Settings() { return m_Settings; }
|
||||
ERR_ContentPolicy Content() { return m_Content; }
|
||||
ERR_ContentSettings ContentSettings() { return m_ContentSettings; }
|
||||
bool IsMissionEnemy(Faction faction)
|
||||
{
|
||||
return faction && (faction == EnemyFaction() || (m_Content && m_Content.Enemy(faction)));
|
||||
}
|
||||
ERR_DiscoveryService Discovery() { return m_Discovery; }
|
||||
ERR_TimeService Time() { return m_Time; }
|
||||
ERR_EnvironmentService Environment() { return m_Environment; }
|
||||
@@ -112,11 +131,13 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
Faction PlayerFaction()
|
||||
{
|
||||
if (!m_Factions) return null;
|
||||
if (!m_SelectedPlayerFaction.IsEmpty()) return GetGame().GetFactionManager().GetFactionByKey(m_SelectedPlayerFaction);
|
||||
return m_Factions.PlayerFaction();
|
||||
}
|
||||
Faction EnemyFaction()
|
||||
{
|
||||
if (!m_Factions) return null;
|
||||
if (!m_SelectedEnemyFaction.IsEmpty()) return GetGame().GetFactionManager().GetFactionByKey(m_SelectedEnemyFaction);
|
||||
return m_Factions.EnemyFaction();
|
||||
}
|
||||
string PreparationError() { return m_PreparationError; }
|
||||
@@ -164,10 +185,11 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
bool Active() { return !m_Stopped && !m_NativeEndHandled && m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
|
||||
protected ERR_CommandService CreateCommandService() { return null; }
|
||||
|
||||
void ConfigureStart(vector position)
|
||||
void ConfigureStart(vector position, float heading = 0)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING) return;
|
||||
m_PrisonPosition = position;
|
||||
m_PrisonHeading = heading;
|
||||
m_EscapeCenter = position;
|
||||
Replication.BumpMe();
|
||||
}
|
||||
@@ -226,6 +248,13 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
}
|
||||
}
|
||||
if (m_Command) m_Command.RegisterGroup(group);
|
||||
ERR_ForceRecord auditRecord = m_Forces.FindGroup(group);
|
||||
string auditSource = "mission-owned hostile faction";
|
||||
if (auditRecord) auditSource = auditRecord.SourceId;
|
||||
else if (Sites() && Sites().FindGroup(group)) auditSource = Sites().FindGroup(group).Definition.Id;
|
||||
string auditFaction;
|
||||
if (group.GetFaction()) auditFaction = group.GetFaction().GetFactionKey();
|
||||
PrintFormat("Reintroduced [SPAWN GROUP]: source=%1 faction=%2 group=%3 position=%4 living=%5", auditSource, auditFaction, group, group.GetOrigin(), ForceLiving(group));
|
||||
m_OnGroup.Invoke(group);
|
||||
}
|
||||
|
||||
@@ -358,6 +387,11 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
|
||||
if (!Replication.IsServer()) return;
|
||||
m_Settings = new ERR_Settings(SettingsPath());
|
||||
m_ContentSettings = new ERR_ContentSettings();
|
||||
m_Content = new ERR_ContentPolicy(m_ContentSettings);
|
||||
m_FactionSelection = new ERR_FactionSelection();
|
||||
m_Admin.Register(new ERR_ContentAdminProvider(m_Content));
|
||||
m_Admin.Register(new ERR_FactionAdminProvider());
|
||||
OnStop().Insert(m_Environment.Stop);
|
||||
m_Recovery = new ERR_RecoveryService(this);
|
||||
m_Recovery.OnChanged().Insert(RecoveryChanged);
|
||||
@@ -405,7 +439,9 @@ class ERR_GameMode : SCR_BaseGameMode
|
||||
protected void StartScenario()
|
||||
{
|
||||
if (m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
|
||||
if (!m_FactionSelection || !m_FactionSelection.Prepare(this)) { FailPreparation("Three enabled faction profiles are required for the mission roles."); return; }
|
||||
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
|
||||
if (!m_Content || !m_Content.PrepareFactions(this)) { FailPreparation("The content registry or enabled faction profiles are invalid. Check faction keys, guard groups and the mission faction manager."); return; }
|
||||
// Scenario content is created through a separate authoring component.
|
||||
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
|
||||
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
// Clear-ground sampling adapted from the owner's improvised-prison extension (see migration provenance).
|
||||
// Searches create no entities and finish before any site starts spawning.
|
||||
class ERR_CampTerrain
|
||||
{
|
||||
protected ref array<vector> m_Airfields = {};
|
||||
protected bool Airport(IEntity entity)
|
||||
{
|
||||
if (!entity || !entity.GetPrefabData()) return false;
|
||||
string path = entity.GetPrefabData().GetPrefabName(); path.ToLower();
|
||||
return path.Contains("prefabs/structures/") && (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/controltower"));
|
||||
}
|
||||
protected bool AddAirport(IEntity entity) { m_Airfields.Insert(entity.GetOrigin()); return true; }
|
||||
void Collect(vector low, vector high)
|
||||
{
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(Vector(low[0], -1000, low[2]), Vector(high[0], 5000, high[2]), AddAirport, Airport, EQueryEntitiesFlags.ALL);
|
||||
}
|
||||
static vector Rotate(vector offset, float heading)
|
||||
{
|
||||
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
|
||||
return rotation[0] * offset[0] + rotation[1] * offset[1] + rotation[2] * offset[2];
|
||||
}
|
||||
static vector Local(vector offset, float heading)
|
||||
{
|
||||
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
|
||||
return Vector(vector.Dot(offset, rotation[0]), offset[1], vector.Dot(offset, rotation[2]));
|
||||
}
|
||||
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest)
|
||||
{
|
||||
lowest = float.MAX; float highest = -float.MAX;
|
||||
for (int x = -1; x <= 1; x++)
|
||||
for (int z = -1; z <= 1; z++)
|
||||
{
|
||||
vector corner = center + Rotate(Vector(x * halfSize, 0, z * halfSize), heading);
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), corner)) return false;
|
||||
}
|
||||
// A 2m grid covers the walls, door approach, furniture and all interior slots.
|
||||
for (float dx = -halfSize; dx <= halfSize; dx += 2)
|
||||
for (float dz = -halfSize; dz <= halfSize; dz += 2)
|
||||
{
|
||||
vector sample = center + Rotate(Vector(dx, 0, dz), heading);
|
||||
float height = GetGame().GetWorld().GetSurfaceY(sample[0], sample[2]);
|
||||
lowest = Math.Min(lowest, height); highest = Math.Max(highest, height);
|
||||
if (lowest < 1 || highest - lowest > maximumRange) return false;
|
||||
}
|
||||
for (float cx = -halfSize + 1; cx < halfSize; cx += 2)
|
||||
for (float cz = -halfSize + 1; cz < halfSize; cz += 2)
|
||||
{
|
||||
vector cell = center + Rotate(Vector(cx, 0, cz), heading);
|
||||
float ground = GetGame().GetWorld().GetSurfaceY(cell[0], cell[2]);
|
||||
TraceBox trace = new TraceBox();
|
||||
trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + "0 4 0";
|
||||
// Bounding the rotated cell conservatively also protects the exit corridor.
|
||||
trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
|
||||
{
|
||||
bool prison = site.Kind == ERR_LocationKind.PRISON;
|
||||
if (prison)
|
||||
{
|
||||
foreach (vector airport : m_Airfields) if (vector.DistanceXZ(position, airport) < 1500) return false;
|
||||
}
|
||||
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||
if (!aiWorld || !aiWorld.GetRoadNetworkManager()) return false;
|
||||
BaseRoad road; float distance;
|
||||
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
|
||||
if (road && distance < 12) return false;
|
||||
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
||||
float guardHeight;
|
||||
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_GeneratedSites
|
||||
{
|
||||
protected ERR_ScenarioDefinition m_Definition;
|
||||
protected ref ERR_CampTerrain m_Terrain = new ERR_CampTerrain();
|
||||
protected ref array<ERR_SiteDefinition> m_Pending = {};
|
||||
protected vector m_Low, m_High;
|
||||
protected int m_Index, m_Attempts, m_Began;
|
||||
protected bool m_Started;
|
||||
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
|
||||
protected string m_Error;
|
||||
string Error() { return m_Error; }
|
||||
bool Begin(ERR_ScenarioDefinition definition)
|
||||
{
|
||||
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
|
||||
m_Started = true; m_Definition = definition;
|
||||
// Prison resolves first, irrespective of authored array order.
|
||||
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
|
||||
foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio);
|
||||
foreach (ERR_SiteDefinition mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar);
|
||||
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
||||
if (!m_Pending.IsEmpty()) m_Terrain.Collect(m_Low, m_High);
|
||||
m_Began = System.GetTickCount();
|
||||
return true;
|
||||
}
|
||||
protected bool Separated(ERR_SiteDefinition candidate, vector position, bool fallback)
|
||||
{
|
||||
foreach (ERR_SiteDefinition other : m_Definition.Sites)
|
||||
{
|
||||
if (other == candidate) continue;
|
||||
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.RADIO && other.RandomClear) continue;
|
||||
float minimum = 300;
|
||||
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.MORTAR) minimum = 1000;
|
||||
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.PRISON) minimum = 500;
|
||||
if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000;
|
||||
if (vector.DistanceXZ(position, other.Position) < minimum) return false;
|
||||
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.EXTRACTION && vector.DistanceXZ(position, other.Position) + 100 >= ERR_GameMode.Get().EscapeRadius()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
ERR_PrepareResult Tick()
|
||||
{
|
||||
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
||||
if (!m_Started || !Replication.IsServer()) return ERR_PrepareResult.FAILED;
|
||||
int tick = System.GetTickCount();
|
||||
while (m_Index < m_Pending.Count())
|
||||
{
|
||||
ERR_SiteDefinition site = m_Pending[m_Index];
|
||||
bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000;
|
||||
vector position = site.Position; float heading = site.Heading;
|
||||
if (!fallback)
|
||||
{
|
||||
position = Vector(Math.RandomFloat(m_Low[0] + 50, m_High[0] - 50), 0, Math.RandomFloat(m_Low[2] + 50, m_High[2] - 50));
|
||||
heading = Math.RandomFloat(0, 360); m_Attempts++;
|
||||
}
|
||||
float floor;
|
||||
if (Separated(site, position, fallback) && m_Terrain.Site(site, position, heading, floor))
|
||||
{
|
||||
position[1] = floor + 0.02;
|
||||
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
|
||||
PrintFormat("Escapists [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
|
||||
m_Index++; m_Attempts = 0;
|
||||
}
|
||||
else if (fallback)
|
||||
{
|
||||
m_Error = "No safe terrain or valid authored fallback for " + site.Id;
|
||||
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
||||
}
|
||||
if (System.GetTickCount() - tick >= 8) return ERR_PrepareResult.PENDING;
|
||||
}
|
||||
m_Result = ERR_PrepareResult.READY; return m_Result;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT }
|
||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR }
|
||||
|
||||
[ComponentEditorProps(category: "Reintroduced", description: "Server-owned location with replicated discovery state")]
|
||||
class ERR_LocationComponentClass : ScriptComponentClass {}
|
||||
|
||||
@@ -1,9 +1,47 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class PauseMenuUI
|
||||
{
|
||||
static bool ERR_ProbeModsEntry()
|
||||
{
|
||||
return s_Instance && s_Instance.m_ERRModsButton;
|
||||
}
|
||||
static void ERR_ProbeOpenMods() { if (s_Instance) s_Instance.ERR_OpenMods(); }
|
||||
}
|
||||
// Explicit local fixture: real dialog, widgets and owner RPCs; no mouse/remote claim.
|
||||
modded class ERR_AdminMenu
|
||||
{
|
||||
bool ERR_ProbeReady() { return m_Ready && m_AvailableFields.Count() == 10; }
|
||||
void ERR_ProbePage(int page) { ShowPage(page); }
|
||||
bool ERR_ProbeReady() { return m_Ready && m_AvailableFields.Count() == ERR_AdminMenuFields.Keys().Count(); }
|
||||
void ERR_ProbePage(int page)
|
||||
{
|
||||
OnButton(SCR_ButtonTextComponent.GetButtonText(Navigation()[page], GetRootWidget()));
|
||||
}
|
||||
bool ERR_ProbeNavigation(int expected)
|
||||
{
|
||||
GetRootWidget().Update();
|
||||
if (m_Page != expected) return false;
|
||||
for (int i = 0; i < Pages().Count(); i++)
|
||||
{
|
||||
Widget panel = GetRootWidget().FindAnyWidget(Pages()[i]);
|
||||
Widget tab = GetRootWidget().FindAnyWidget(Navigation()[i]);
|
||||
Widget selected = GetRootWidget().FindAnyWidget("Selected_" + Navigation()[i]);
|
||||
if (!panel || !tab || !selected || panel.IsVisible() != (i == expected) || !tab.IsVisible() || selected.IsVisible() != (i == expected)) return false;
|
||||
float x, y, width, height, px, py;
|
||||
tab.GetScreenPos(x, y); tab.GetScreenSize(width, height); panel.GetScreenPos(px, py);
|
||||
if (width <= 1 || height <= 1 || (i == expected && y + height > py))
|
||||
{ PrintFormat("ERR ADMIN NAV BOUNDS: page=%1 tab=%2 bounds=%3,%4,%5,%6 panelY=%7", expected, i, x, y, width, height, py); return false; }
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ERR_ProbeFieldPage(string key)
|
||||
{
|
||||
Widget widget = GetRootWidget().FindAnyWidget(key);
|
||||
while (widget)
|
||||
{
|
||||
int page = Pages().Find(widget.GetName());
|
||||
if (page >= 0) { ERR_ProbePage(page); return; }
|
||||
widget = widget.GetParent();
|
||||
}
|
||||
}
|
||||
string ERR_ProbeDraft() { return SCR_EditBoxComponent.GetEditBoxComponent("radioDefenceSeconds", GetRootWidget()).GetValue(); }
|
||||
void ERR_ProbeEdit(string text) { SCR_EditBoxComponent.GetEditBoxComponent("radioDefenceSeconds", GetRootWidget()).SetValue(text); }
|
||||
void ERR_ProbeApply()
|
||||
@@ -11,8 +49,71 @@ modded class ERR_AdminMenu
|
||||
Widget widget = GetRootWidget().FindAnyWidget("Apply_radioDefenceSeconds");
|
||||
OnButton(SCR_ButtonBaseComponent.Cast(widget.FindHandler(SCR_ButtonBaseComponent)));
|
||||
}
|
||||
bool ERR_ProbeRandomChoices()
|
||||
{
|
||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||
{
|
||||
ERR_ProbeFieldPage(key);
|
||||
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("Random_" + key, GetRootWidget());
|
||||
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
|
||||
bool available = ERR_AdminMenuFields.Available(key, m_HQ, m_Intel);
|
||||
if (!button || !edit || button.IsEnabled() != available) return false;
|
||||
if (!available) continue;
|
||||
string previous = edit.GetValue();
|
||||
OnButton(button);
|
||||
if (edit.GetValue() != "Random") return false;
|
||||
edit.SetValue(previous);
|
||||
}
|
||||
ERR_ProbeFieldPage("startTimeMinutes");
|
||||
SCR_EditBoxComponent time = SCR_EditBoxComponent.GetEditBoxComponent("startTimeMinutes", GetRootWidget());
|
||||
string previousTime = time.GetValue();
|
||||
SCR_ButtonTextComponent day = SCR_ButtonTextComponent.GetButtonText("RandomDay_startTimeMinutes", GetRootWidget());
|
||||
if (!day || !day.IsEnabled()) return false;
|
||||
OnButton(day);
|
||||
bool selected = time.GetValue() == "Random day";
|
||||
time.SetValue(previousTime);
|
||||
ERR_ProbePage(0);
|
||||
return selected;
|
||||
}
|
||||
bool ERR_ProbeFactionRandomAll()
|
||||
{
|
||||
if (!m_FactionFieldsInitialized || m_FactionIds.Count() < 4) return false;
|
||||
ERR_ProbePage(4);
|
||||
if (m_Page != 4) return false;
|
||||
OnFactionButton(SCR_ButtonTextComponent.GetButtonText("FactionsRandomAll", GetRootWidget()));
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
|
||||
if (!combo || !combo.IsEnabled() || combo.GetCurrentIndex() != 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ERR_ProbeFactionApply() { OnFactionButton(SCR_ButtonTextComponent.GetButtonText("FactionsApply", GetRootWidget())); }
|
||||
bool ERR_ProbeHQ() { return m_HQ; }
|
||||
bool ERR_ProbeIntel() { return m_Intel; }
|
||||
bool ERR_ProbeMods()
|
||||
{
|
||||
GetRootWidget().Update();
|
||||
PrintFormat("ERR MOD MENU: page=%1 rows=%2 saved=%3 active=%4", m_Page, m_ModRows.Count(), m_ModSaved.Count(), m_ModActive.Count());
|
||||
if (m_Page != 3 || m_ModRows.Count() != ERR_ContentRegistry.Ids().Count()) return false;
|
||||
foreach (string pack, Widget row : m_ModRows)
|
||||
{
|
||||
TextWidget text = TextWidget.Cast(row.FindAnyWidget("PackState"));
|
||||
if (text) PrintFormat("ERR MOD ROW: id=%1 state=%2", pack, text.GetText());
|
||||
if (!text || !text.GetText().Contains("Saved preference:") || !m_ModSaved.Contains(pack) || !m_ModActive.Contains(pack)) return false;
|
||||
float width, height; row.GetScreenSize(width, height);
|
||||
PrintFormat("ERR MOD BOUNDS: id=%1 width=%2 height=%3", pack, width, height);
|
||||
if (width < 100 || height < 2) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ERR_ProbeMod(bool enabled)
|
||||
{
|
||||
Widget row = m_ModRows.Get("rhs");
|
||||
string name = "Disable";
|
||||
if (enabled) name = "Enable";
|
||||
OnModButton(SCR_ButtonTextComponent.GetButtonText(name, row));
|
||||
}
|
||||
int ERR_ProbeLogCursor() { return m_LogCursor; }
|
||||
bool ERR_ProbeJournalVisible()
|
||||
{
|
||||
@@ -39,10 +140,14 @@ class ERR_NativeAdminMenuProbe
|
||||
protected DamageManagerComponent m_Damage;
|
||||
protected bool m_PreviousDamage;
|
||||
protected int m_Stage;
|
||||
protected int m_CapturePage;
|
||||
protected float m_Deadline;
|
||||
protected float m_Wait;
|
||||
protected int m_RadioSeconds;
|
||||
protected bool m_Failed;
|
||||
protected bool m_RhsSaved;
|
||||
protected ref array<string> m_FactionSaved = {};
|
||||
protected ref array<string> m_FactionActive = {};
|
||||
void ERR_NativeAdminMenuProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected int Fail(string reason)
|
||||
{
|
||||
@@ -67,8 +172,12 @@ class ERR_NativeAdminMenuProbe
|
||||
m_Damage = DamageManagerComponent.Cast(m_Controller.GetControlledEntity().FindComponent(DamageManagerComponent));
|
||||
m_PreviousDamage = m_Damage.IsDamageHandlingEnabled();
|
||||
m_RadioSeconds = m_Mode.Settings().Get("radioDefenceSeconds");
|
||||
m_Deadline = now + 45000;
|
||||
m_Controller.ERR_AdminSubmit("menu");
|
||||
m_Deadline = now + 90000;
|
||||
m_RhsSaved = m_Mode.ContentSettings().Enabled("rhs");
|
||||
GetGame().OpenPauseMenu(false, true);
|
||||
if (!PauseMenuUI.ERR_ProbeModsEntry()) return Fail("Esc menu missing Escapists Reintroduced entry");
|
||||
PauseMenuUI.ERR_ProbeOpenMods();
|
||||
Pass("native Esc menu entry opens mission administration");
|
||||
m_Stage = 1; return 0;
|
||||
}
|
||||
if (m_Stage < 6 && !menu) return Fail("dialog unexpectedly closed");
|
||||
@@ -78,6 +187,32 @@ class ERR_NativeAdminMenuProbe
|
||||
array<string> capabilities = {}; m_Mode.Admin().Capabilities(capabilities);
|
||||
if (menu.ERR_ProbeHQ() != capabilities.Contains("supports=hq") || menu.ERR_ProbeIntel() != capabilities.Contains("supports=intel")) return Fail("capability snapshot mismatch");
|
||||
Pass("real owner RPC opens menu and returns current settings/capabilities");
|
||||
System.MakeScreenshot("$logs:err-admin-open");
|
||||
if (!menu.ERR_ProbeNavigation(0)) return Fail("Esc entry did not open Mission page with shared navigation");
|
||||
menu.ERR_ProbePage(3);
|
||||
if (!menu.ERR_ProbeMods()) return Fail("mod rows missing state or collapsed");
|
||||
Print("ERR MOD CAPTURE READY");
|
||||
menu.ERR_ProbePage(0);
|
||||
Print("ERR ADMIN CAPTURE PAGE: 0");
|
||||
m_Wait = now + 3000; m_Stage = 30; return 0;
|
||||
}
|
||||
if (m_Stage == 30)
|
||||
{
|
||||
if (!menu || !menu.ERR_ProbeNavigation(m_CapturePage)) return Fail("page or top navigation bounds invalid");
|
||||
System.MakeScreenshot("$logs:err-admin-page-" + m_CapturePage.ToString());
|
||||
m_CapturePage++;
|
||||
if (m_CapturePage < ERR_AdminMenu.Pages().Count())
|
||||
{ menu.ERR_ProbePage(m_CapturePage); Print("ERR ADMIN CAPTURE PAGE: " + m_CapturePage.ToString()); }
|
||||
else { Pass("all eight pages reached through shared top navigation with one visible page"); m_Stage = 13; }
|
||||
m_Wait = now + 3000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Stage == 13)
|
||||
{
|
||||
if (!menu) return Fail("mods dialog closed during visibility check");
|
||||
menu.ERR_ProbePage(0);
|
||||
if (!menu.ERR_ProbeRandomChoices()) return Fail("random choice controls missing, incorrectly gated or did not select draft");
|
||||
Pass("all Random buttons and Random day select editable drafts");
|
||||
menu.ERR_ProbeEdit("47");
|
||||
m_Controller.ERR_AdminRequestSnapshot();
|
||||
m_Wait = now + 3000; m_Stage = 2; return 0;
|
||||
@@ -103,6 +238,51 @@ class ERR_NativeAdminMenuProbe
|
||||
if (m_Mode.Settings().Get("radioDefenceSeconds") != m_RadioSeconds) return Fail("test setting restoration failed");
|
||||
Pass("test setting restored without resetting menu drafts");
|
||||
System.MakeScreenshot("$logs:err-admin-hq");
|
||||
menu.ERR_ProbePage(3);
|
||||
menu.ERR_ProbeMod(!m_RhsSaved);
|
||||
m_Wait = now + 2000; m_Stage = 11; return 0;
|
||||
}
|
||||
if (m_Stage == 11)
|
||||
{
|
||||
if (!menu || m_Mode.ContentSettings().Enabled("rhs") == m_RhsSaved || m_Mode.Content().Enabled("rhs") != m_RhsSaved || !menu.ERR_ProbeMods()) return Fail("mod button failed to save independently of active round");
|
||||
Pass("native mod toggle persists through authenticated owner RPC without changing active content");
|
||||
menu.ERR_ProbeMod(m_RhsSaved);
|
||||
m_Wait = now + 2000; m_Stage = 12; return 0;
|
||||
}
|
||||
if (m_Stage == 12)
|
||||
{
|
||||
if (!menu || m_Mode.ContentSettings().Enabled("rhs") != m_RhsSaved) return Fail("mod preference restoration failed");
|
||||
Pass("native mod toggle restores saved preference");
|
||||
foreach (string role : ERR_FactionChoices.Roles())
|
||||
{
|
||||
m_FactionSaved.Insert(m_Mode.FactionSelection().Saved(role));
|
||||
m_FactionActive.Insert(m_Mode.FactionSelection().Active(role));
|
||||
}
|
||||
if (!menu.ERR_ProbeFactionRandomAll()) return Fail("faction selectors or Random all failed");
|
||||
Pass("three faction selectors and Random all create distinct-role drafts");
|
||||
m_Wait = now + 3000; m_Stage = 20; return 0;
|
||||
}
|
||||
if (m_Stage == 20)
|
||||
{
|
||||
if (!menu || !menu.ERR_ProbeFactionRandomAll()) return Fail("snapshot lost faction drafts");
|
||||
menu.ERR_ProbeFactionApply();
|
||||
m_Wait = now + 1500; m_Stage = 21; return 0;
|
||||
}
|
||||
if (m_Stage == 21)
|
||||
{
|
||||
array<string> roles = ERR_FactionChoices.Roles();
|
||||
for (int i = 0; i < roles.Count(); i++)
|
||||
if (m_Mode.FactionSelection().Saved(roles[i]) != "random" || m_Mode.FactionSelection().Active(roles[i]) != m_FactionActive[i]) return Fail("faction Apply failed or changed current round");
|
||||
Pass("faction Apply persists all three through authenticated owner RPC without changing active round");
|
||||
m_Controller.ERR_AdminSubmit("factions set " + m_FactionSaved[0] + " " + m_FactionSaved[1] + " " + m_FactionSaved[2], true);
|
||||
m_Wait = now + 1500; m_Stage = 22; return 0;
|
||||
}
|
||||
if (m_Stage == 22)
|
||||
{
|
||||
array<string> restoredRoles = ERR_FactionChoices.Roles();
|
||||
for (int j = 0; j < restoredRoles.Count(); j++)
|
||||
if (m_Mode.FactionSelection().Saved(restoredRoles[j]) != m_FactionSaved[j]) return Fail("faction restoration failed");
|
||||
Pass("saved faction choices restored");
|
||||
if (menu.ERR_ProbeHQ()) menu.ERR_ProbePage(2);
|
||||
m_Wait = now + 3000; m_Stage = 5; return 0;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,57 @@ class ERR_NativeAdminSettingsProbe
|
||||
adapter.Probe(settings, "mission radio show", replies);
|
||||
return Check(replies.Count() == 1 && replies[0] == "radioDefenceSeconds=" + initial.Get("radioDefenceSeconds").ToString(), "existing radio alias and response remain intact");
|
||||
}
|
||||
protected static bool RandomChoices(string path, ERR_GameMode mode)
|
||||
{
|
||||
ERR_Settings settings = new ERR_Settings(path);
|
||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||
{
|
||||
int parsed;
|
||||
if (!Check(ERR_CommandArguments.Selection(key, "random", parsed) && parsed == -1 && settings.Set(key, parsed), "random selection saved: " + key)) return false;
|
||||
ERR_Settings reload = new ERR_Settings(path);
|
||||
if (!Check(reload.Get(key) == -1 && ERR_AdminMenuFields.Display(key, "-1") == "Random", "random mode survives reload and displays by name: " + key)) return false;
|
||||
if (key == "startDateCode") continue;
|
||||
int rolled = settings.Runtime(key);
|
||||
if (!Check(ERR_Settings.Valid(key, rolled), "random resolves within concrete bounds: " + key)) return false;
|
||||
for (int i = 0; i < 100; i++)
|
||||
if (settings.Runtime(key) != rolled) return Check(false, "repeated reads rerolled " + key);
|
||||
if (!Check(settings.Set(key, -1) && settings.Runtime(key) == rolled, "saving same mode preserves roll: " + key)) return false;
|
||||
}
|
||||
int radio = settings.Runtime("radioDefenceSeconds");
|
||||
if (!Check(settings.Set("intelRevealChancePercent", 15) && settings.Runtime("radioDefenceSeconds") == radio, "unrelated save does not reroll another option")) return false;
|
||||
if (!Check(settings.Set("radioDefenceSeconds", 45) && settings.Runtime("radioDefenceSeconds") == 45 && settings.Set("radioDefenceSeconds", -1) && ERR_Settings.Valid("radioDefenceSeconds", settings.Runtime("radioDefenceSeconds")), "fixed value replaces random and random can be selected again")) return false;
|
||||
if (!Check(!settings.Set("unknown", -1) && !settings.Set("weatherProfile", -2) && !settings.Set("startTimeMinutes", -3), "unknown keys and invalid modes rejected")) return false;
|
||||
int day;
|
||||
if (!Check(ERR_CommandArguments.Selection("startTimeMinutes", "random day", day) && day == -2 && settings.Set("startTimeMinutes", day), "Random day parses and saves")) return false;
|
||||
ERR_Settings reloadedDay = new ERR_Settings(path);
|
||||
if (!Check(reloadedDay.Get("startTimeMinutes") == -2 && ERR_AdminMenuFields.Display("startTimeMinutes", "-2") == "Random day" && !ERR_CommandArguments.Selection("weatherProfile", "random day", day), "Random day reloads and is restricted to time")) return false;
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
TimeAndWeatherManagerEntity manager = world.GetTimeAndWeatherManager();
|
||||
float sunrise, sunset;
|
||||
int first, end;
|
||||
if (!Check(manager.GetSunriseHour(sunrise) && manager.GetSunsetHour(sunset) && ERR_TimeService.DaylightBounds(sunrise, sunset, first, end), "native island sunrise and sunset define Random day")) return false;
|
||||
if (mode.Settings().Get("startTimeMinutes") == -2)
|
||||
{
|
||||
int started = mode.Time().StartMinutes();
|
||||
if (!Check(started / 60.0 > sunrise && started / 60.0 < sunset, "actual round starts within native daylight interval")) return false;
|
||||
}
|
||||
if (!Check(ERR_Settings.Valid("timeMultiplier", mode.Time().Multiplier()) && ERR_Settings.Valid("startDateCode", mode.Environment().CurrentDateCode()), "actual startup uses concrete speed and valid date")) return false;
|
||||
for (int sample = 0; sample < 100; sample++)
|
||||
{
|
||||
int minute = Math.RandomInt(first, end);
|
||||
int dateCode = ERR_EnvironmentService.RandomDate(manager.GetYear());
|
||||
if (minute / 60.0 <= sunrise || minute / 60.0 >= sunset || !ERR_Settings.Valid("startTimeMinutes", minute)) return Check(false, "daylight roll outside sun bounds");
|
||||
if (!ERR_Settings.Valid("startDateCode", dateCode) || 1900 + dateCode / 10000 != manager.GetYear()) return Check(false, "random date outside authored year");
|
||||
}
|
||||
array<string> replies = {};
|
||||
ERR_NativeAdminSettingsAdapter adapter = new ERR_NativeAdminSettingsAdapter();
|
||||
adapter.Probe(settings, "intel reveal set random", replies);
|
||||
if (!Check(settings.Get("intelRevealChancePercent") == -1 && replies[0].Contains("Random"), "numeric random passes real command handler")) return false;
|
||||
replies.Clear();
|
||||
ERR_EnvironmentCommands.Execute(settings, mode.Environment(), {"mission", "weather", "set", "random"}, replies);
|
||||
if (!Check(settings.Get("weatherProfile") == -1 && replies[0].Contains("Random"), "environment random passes real command handler")) return false;
|
||||
return Check(true, "100 valid daylight and calendar samples; all random modes retained");
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
if (!Check(Replication.IsServer() && mode && mode.Admin() && mode.Settings(), "native Core command service and settings ready")) return false;
|
||||
@@ -65,9 +116,20 @@ class ERR_NativeAdminSettingsProbe
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("0-100") && mode.Settings().Get("intelRevealChancePercent") == original, "actual dispatch rejects out-of-range value with guidance")) return false;
|
||||
mode.Admin().Execute(mode, true, "intel reveal help", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("second document"), "actual Core help describes physical document progression")) return false;
|
||||
int savedTime = mode.Settings().Get("startTimeMinutes");
|
||||
int activeTime = mode.Time().StartMinutes();
|
||||
mode.Admin().Execute(mode, true, "mission time set random day", replies);
|
||||
bool dayAccepted = mode.Settings().Get("startTimeMinutes") == -2 && mode.Time().StartMinutes() == activeTime && replies.Count() == 1 && replies[0].Contains("Random day");
|
||||
bool timeRestored = mode.Settings().Set("startTimeMinutes", savedTime);
|
||||
if (!Check(dayAccepted && timeRestored, "Random day command saves next-round mode without jumping active clock; restored")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission time set random", replies);
|
||||
bool randomAccepted = mode.Settings().Get("startTimeMinutes") == -1 && mode.Time().StartMinutes() == activeTime && replies.Count() == 1 && replies[0].Contains("Random");
|
||||
timeRestored = mode.Settings().Set("startTimeMinutes", savedTime);
|
||||
if (!Check(randomAccepted && timeRestored, "Random time command preserves active clock; restored")) return false;
|
||||
string path = "$profile:erri-native-admin-settings-probe.json";
|
||||
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "disposable fixture filenames are unused")) return false;
|
||||
bool result = Storage(path);
|
||||
if (result) result = RandomChoices(path, mode);
|
||||
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
|
||||
if (!result) return false;
|
||||
return Check(mode.Settings().Get("intelRevealChancePercent") == original && !FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "fixture removed its files and never changed actual mission settings");
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_ObjectiveDefence
|
||||
{
|
||||
bool ERR_ProbeMedical()
|
||||
{
|
||||
foreach (ERR_ObjectiveDefenceEntry entry : m_Entries)
|
||||
{
|
||||
if (entry.Site.Definition.Id != "everon-medical-1") continue;
|
||||
if (!entry.Spawned || !entry.Admitted || !entry.Group || !entry.Hold) return false;
|
||||
return entry.Group.IsExpandComplete() && m_Mode.ForceLiving(entry.Group) == entry.Group.m_aUnitPrefabSlots.Count()
|
||||
&& vector.Distance(entry.Hold.GetOrigin(), entry.Site.Location.Position()) < 0.1
|
||||
&& m_HQ.ERR_ProbeDedicated(entry.Group);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
modded class ERR_HunterService
|
||||
{
|
||||
ERR_HunterVehicle ERR_ProbeFirst()
|
||||
{
|
||||
if (m_Hunters.IsEmpty()) return null;
|
||||
ERR_HunterVehicle first = m_Hunters[0];
|
||||
return first;
|
||||
}
|
||||
}
|
||||
modded class ERR_HQ
|
||||
{
|
||||
void ERR_ProbeBody(int slot, float now)
|
||||
{
|
||||
ERR_HQReport report = new ERR_HQReport();
|
||||
report.TravelMode = "body"; report.MessageId = 100000 + slot;
|
||||
report.CasualtySourceId = "fixture-identified"; report.CasualtyStrength = 4; report.CasualtySlot = slot;
|
||||
report.ObservedAt = now; report.Position = "5000 50 5000";
|
||||
// Delivered intake boundary; policy tests independently cover route/replay rules.
|
||||
Accept(report, now);
|
||||
}
|
||||
bool ERR_ProbeDedicated(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) return unit.ReservedForSupport && !unit.Eligible(m_Mode);
|
||||
return false;
|
||||
}
|
||||
bool ERR_ProbeMedicalDefence() { return m_ObjectiveDefence.ERR_ProbeMedical(); }
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRAggressionStage;
|
||||
protected int m_ERRAggressionDeadline;
|
||||
protected vector m_ERRHunterStart;
|
||||
protected bool m_ERRHunterOrder;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "aggression") return;
|
||||
m_ERRAggressionDeadline = System.GetTickCount() + 270000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_AggressionTick, 1000, true);
|
||||
}
|
||||
protected bool ERR_AggressionCheck(bool ok, string text)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: aggression " + text); return true; }
|
||||
Print("ERR SMOKE FAIL: aggression " + text, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_AggressionTick); return false;
|
||||
}
|
||||
protected void ERR_AggressionTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRAggressionDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_AggressionCheck(false, "native startup/movement deadline"); return; }
|
||||
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent));
|
||||
if (damage) damage.EnableDamageHandling(false);
|
||||
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (m_ERRAggressionStage == 0)
|
||||
{
|
||||
if (!ERR_AggressionCheck(UnlockPrison(player), "native prison release")) return;
|
||||
player.SetOrigin(ERR_Actors.Ground(Sites().Find("everon-medical-1").Location.Position() + "300 0 0"));
|
||||
for (int i = 0; i < 3; i++) hq.ERR_ProbeBody(i, now);
|
||||
hq.ERR_ProbeBody(2, now);
|
||||
if (!ERR_AggressionCheck(hq.Watch().IdentifiedBodies() == 3 && hq.Watch().Aggression() == 1 && hq.Watch().HunterTarget() == 1, "three distinct delivered bodies unlock one UAZ; duplicate ignored")) return;
|
||||
if (!ERR_AggressionCheck(ERR_HQWatchRules.Aggression(90) == 30 && ERR_HQWatchRules.Hunters(30) == 15 && ERR_HQWatchRules.Hunters(1000) == 15, "maximum 30 aggression / 15 UAZ allowance")) return;
|
||||
for (int j = 0; j < Sites().Count(); j++)
|
||||
{
|
||||
ERR_SiteRuntime site = Sites().At(j);
|
||||
if (site.Group && site.Group.GetFaction() == EnemyFaction() && ERR_ObjectiveDefence.Objective(site.Definition.Kind))
|
||||
if (!ERR_AggressionCheck(hq.ERR_ProbeDedicated(site.Group), "objective guard permanently excluded from HQ reassignment: " + site.Definition.Id)) return;
|
||||
}
|
||||
m_ERRAggressionStage = 1;
|
||||
}
|
||||
ERR_HunterVehicle hunter = hq.Hunters().ERR_ProbeFirst();
|
||||
if (!hunter || !hunter.Registered) return;
|
||||
if (m_ERRAggressionStage == 1)
|
||||
{
|
||||
if (!ERR_AggressionCheck(hunter.CrewValid(this) && hunter.Members.Count() == 2, "physical UAZ with native seated driver and gunner")) return;
|
||||
m_ERRHunterStart = hunter.Entity.GetOrigin(); m_ERRAggressionStage = 2;
|
||||
}
|
||||
if (m_ERRAggressionStage == 2)
|
||||
{
|
||||
foreach (IEntity soldier : hunter.Members)
|
||||
{
|
||||
SCR_AICombatComponent combat = SCR_AICombatComponent.Cast(soldier.FindComponent(SCR_AICombatComponent));
|
||||
if (!combat || combat.GetAISkill() != EAISkill.CYLON || !combat.ERR_EnhancedEnemy()) return;
|
||||
}
|
||||
if (hunter.Waypoint) m_ERRHunterOrder = true;
|
||||
if (!m_ERRHunterOrder || vector.DistanceXZ(m_ERRHunterStart, hunter.Entity.GetOrigin()) < 30) return;
|
||||
if (!hq.ERR_ProbeMedicalDefence()) return;
|
||||
if (!ERR_AggressionCheck(true, "nearby medical stash gained complete dedicated squad and defend waypoint at terrain height")) return;
|
||||
if (!ERR_AggressionCheck(true, "HQ radio delivered road order; UAZ drove over 30m; maximum scoped crew skill active")) return;
|
||||
if (!ERR_AggressionCheck(hq.Hunters().Deployed() == 1, "bounded allowance does not spawn duplicate UAZs")) return;
|
||||
hq.Hunters().Stop();
|
||||
if (!ERR_AggressionCheck(hunter.Released && !hunter.Waypoint && hunter.Entity && hunter.Members.Count() == 2, "stop removes orders and preserves physical vehicle/crew")) return;
|
||||
foreach (IEntity survivor : hunter.Members)
|
||||
{
|
||||
SCR_AICombatComponent survivingCombat = SCR_AICombatComponent.Cast(survivor.FindComponent(SCR_AICombatComponent));
|
||||
if (!ERR_AggressionCheck(survivingCombat && survivingCombat.ERR_EnhancedEnemy(), "released driving task preserves surviving crew AI profile")) return;
|
||||
}
|
||||
GetGame().GetCallqueue().Remove(ERR_AggressionTick);
|
||||
Print("ERR SMOKE COMPLETE: aggression, physical UAZ, native movement, dedicated objectives and scoped skill");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,205 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_DiscoveryMarker
|
||||
{
|
||||
string ERR_ProbeIcon() { return m_Icon; }
|
||||
}
|
||||
modded class ERR_LocationComponent
|
||||
{
|
||||
bool ERR_ProbeLocationIcon()
|
||||
{
|
||||
ERR_UpdateMarker();
|
||||
if (Kind() == ERR_LocationKind.PRISON) return !m_ERRMarker;
|
||||
if (!m_ERRMarker || m_ERRMarker.GetMarkerOwnerID() != -1) return false;
|
||||
string expected = ERR_DiscoveryMarker.Icon(Kind(), Id());
|
||||
if (Knowledge() == 1) expected = "mark-question";
|
||||
int point[2]; m_ERRMarker.GetWorldPos(point);
|
||||
return m_ERRMarker.ERR_ProbeIcon() == expected && point[0] == ERR_DiscoveryMapRules.Coordinate(Position()[0], Knowledge()) && point[1] == ERR_DiscoveryMapRules.Coordinate(Position()[2], Knowledge());
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected bool ERR_RadioRoof(IEntity tent, vector position, IEntity exclude = null)
|
||||
{
|
||||
TraceParam roof = new TraceParam();
|
||||
roof.Start = position; roof.End = position + "0 5 0";
|
||||
roof.Flags = TraceFlags.ENTS; roof.Exclude = exclude;
|
||||
return GetWorld().TraceMove(roof, null) < 1 && roof.TraceEnt == tent;
|
||||
}
|
||||
protected bool ERR_TestRadioCamp()
|
||||
{
|
||||
ERR_SiteRuntime radio = Sites().Find("everon-radio-1");
|
||||
IEntity tent;
|
||||
foreach (IEntity prop : radio.Props)
|
||||
if (prop && prop.GetPrefabData().GetPrefabName().Contains("/TentUSSR_01_base.et")) tent = prop;
|
||||
if (!ERR_AirfieldCheck(tent && ERR_RadioRoof(tent, radio.Location.Position() + "0 0.2 0", radio.Location.GetOwner()), "radio physically underneath tent roof")) return false;
|
||||
ERR_SpawnPlacement placement = new ERR_SpawnPlacement();
|
||||
bool accessible;
|
||||
for (int step = 0; step < 32; step++)
|
||||
{
|
||||
float angle = step * Math.PI / 16;
|
||||
vector offset = Vector(Math.Cos(angle) * 1.4, 0, Math.Sin(angle) * 1.4);
|
||||
vector position = ERR_Actors.Ground(radio.Location.Position() + ERR_CampTerrain.Rotate(offset, radio.Definition.Heading));
|
||||
if (!placement.Clear(position, null) || !ERR_RadioRoof(tent, position + "0 1.7 0")) continue;
|
||||
TraceParam sight = new TraceParam();
|
||||
sight.Start = position + "0 1.3 0"; sight.End = radio.Location.Position() + "0 0.3 0";
|
||||
sight.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
float fraction = GetWorld().TraceMove(sight, null);
|
||||
if (fraction < 1 && sight.TraceEnt != radio.Location.GetOwner()) continue;
|
||||
accessible = true; break;
|
||||
}
|
||||
if (!ERR_AirfieldCheck(accessible, "standing room inside tent within radio interaction distance and line of sight")) return false;
|
||||
array<AIAgent> guards = {}; radio.Group.GetAgents(guards);
|
||||
if (!ERR_AirfieldCheck(guards.Count() == radio.InitialGuardCount && guards.Count() > 0, "radio always has its complete physical guard squad")) return false;
|
||||
foreach (AIAgent guard : guards)
|
||||
if (!ERR_AirfieldCheck(guard && ERR_Actors.CanFight(guard.GetControlledEntity()), "radio guard alive and conscious")) return false;
|
||||
if (!ERR_AirfieldCheck(ERR_HQ.Get().ERR_ProbeRadioReservation(radio.Group), "HQ permanently refuses radio squad reassignment and courier reservation")) return false;
|
||||
array<AIWaypoint> waypoints = {}; radio.Group.GetWaypoints(waypoints);
|
||||
bool defending;
|
||||
foreach (AIWaypoint waypoint : waypoints)
|
||||
if (SCR_DefendWaypoint.Cast(waypoint) && vector.DistanceXZ(waypoint.GetOrigin(), radio.Location.Position()) < 0.1 && waypoint.GetCompletionRadius() == 20) defending = true;
|
||||
return ERR_AirfieldCheck(defending, "permanent native defend order centred on radio with 20m radius");
|
||||
}
|
||||
|
||||
protected int m_ERRAirfieldStage;
|
||||
protected bool m_ERRAirfieldOpeningChecked;
|
||||
protected int m_ERRMortarFolders;
|
||||
protected IEntity m_ERRMortarFolder;
|
||||
protected ref ERR_AdminItemCallback m_ERRMortarPickup;
|
||||
protected bool ERR_MortarIntelReady()
|
||||
{
|
||||
array<ref ERR_MortarBattery> batteries = {}; ERR_MortarSystem.Get().Batteries(batteries);
|
||||
if (batteries.Count() != 4) { ERR_AirfieldCheck(false, "exactly four authored mortar batteries"); return false; }
|
||||
foreach (ERR_MortarBattery battery : batteries)
|
||||
{
|
||||
if (!battery.SpawnFailure.IsEmpty()) { ERR_AirfieldCheck(false, "mortar preparation: " + battery.SpawnFailure); return false; }
|
||||
if (!battery.StartupActivated) return false;
|
||||
}
|
||||
if (m_ERRMortarFolder)
|
||||
{
|
||||
if (!m_ERRMortarPickup.Finished) return false;
|
||||
if (!m_ERRMortarPickup.Succeeded) { ERR_AirfieldCheck(false, "native intel inventory insertion"); return false; }
|
||||
if (!Discovery().IsSpent(m_ERRMortarFolder)) return false;
|
||||
m_ERRMortarFolder = null;
|
||||
}
|
||||
int known;
|
||||
for (int i = 1; i <= 4; i++)
|
||||
if (Sites().Find("everon-mortar-" + i).Location.Knowledge() == 2) known++;
|
||||
if (known == 4) return true;
|
||||
if (m_ERRMortarFolders >= 8) { ERR_AirfieldCheck(false, "all four mortar locations discoverable within eight folders"); return false; }
|
||||
// Exclude already-known non-mortar destinations, then exercise actual normal
|
||||
// untargeted folder recovery. No direct mortar Discover call is made here.
|
||||
array<ERR_LocationComponent> locations = {}; Locations(locations);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
if (location.Kind() != ERR_LocationKind.MORTAR) location.Discover(0, 100);
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||
m_ERRMortarFolder = ERR_Actors.Spawn(ERR_Resources.IntelFolder(), player.GetOrigin()); Own(m_ERRMortarFolder);
|
||||
m_ERRMortarPickup = new ERR_AdminItemCallback();
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(player.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!m_ERRMortarFolder || !inventory.TryInsertItem(m_ERRMortarFolder, EStoragePurpose.PURPOSE_DEPOSIT, m_ERRMortarPickup))
|
||||
ERR_AirfieldCheck(false, "physical intel pickup accepted");
|
||||
m_ERRMortarFolders++;
|
||||
return false;
|
||||
}
|
||||
|
||||
protected int m_ERRAirfieldDeadline;
|
||||
protected float m_ERRAirfieldWait;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "airfield-markers") return;
|
||||
m_ERRAirfieldDeadline = System.GetTickCount() + 200000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_AirfieldTick, 500, true);
|
||||
}
|
||||
protected bool ERR_AirfieldCheck(bool value, string text)
|
||||
{
|
||||
if (value) { Print("ERR SMOKE PASS: airfield " + text); return true; }
|
||||
Print("ERR SMOKE FAIL: airfield " + text, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_AirfieldTick); return false;
|
||||
}
|
||||
protected void ERR_AirfieldTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERRAirfieldDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_AirfieldCheck(false, "preparation or deadline failure"); return; }
|
||||
if ((Phase() != ERR_Phase.PRISON && Phase() != ERR_Phase.RADIO_SEARCH) || !GetGame().GetPlayerController() || !GetGame().GetPlayerController().GetControlledEntity()) return;
|
||||
if (GetWorld().GetWorldTime() < m_ERRAirfieldWait) return;
|
||||
if (m_ERRAirfieldStage == 0)
|
||||
{
|
||||
if (!m_ERRAirfieldOpeningChecked)
|
||||
{
|
||||
if (!ERR_TestRadioCamp()) return;
|
||||
if (!ERR_AirfieldCheck(ERR_NativeScoutProbe.Run(), "ten hill scouts: complete finite loadouts, full ammo packs, physical radios and reserved observation posts")) return;
|
||||
ERR_SiteRuntime prison = Sites().Find("everon-prison-1");
|
||||
array<AIAgent> guards = {}; prison.Group.GetAgents(guards);
|
||||
int radios; array<vector> guardPositions = {};
|
||||
foreach (AIAgent guard : guards)
|
||||
{
|
||||
IEntity actor = guard.GetControlledEntity();
|
||||
float distance = vector.DistanceXZ(actor.GetOrigin(), prison.Location.Position());
|
||||
if (!ERR_AirfieldCheck(distance >= 11.5 && distance <= 24.5 && !guard.IsAIActivated() && ERR_Actors.CanFight(actor), "guard alive, frozen and outside prison at random perimeter position")) return;
|
||||
foreach (vector previous : guardPositions)
|
||||
if (!ERR_AirfieldCheck(vector.DistanceXZ(previous, actor.GetOrigin()) > 3, "guards occupy separate perimeter positions")) return;
|
||||
guardPositions.Insert(actor.GetOrigin());
|
||||
if (ERR_HQRadio.Capability(actor) == 2) radios++;
|
||||
}
|
||||
if (!ERR_AirfieldCheck(radios > 0, "perimeter patrol retains physical large radio")) return;
|
||||
for (int mortarId = 1; mortarId <= 4; mortarId++)
|
||||
{
|
||||
ERR_LocationComponent hidden = Sites().Find("everon-mortar-" + mortarId).Location;
|
||||
if (!ERR_AirfieldCheck(hidden.Knowledge() == 0, "mortar initially undiscovered")) return;
|
||||
}
|
||||
ERR_LocationComponent partial = Sites().Find("everon-mortar-1").Location;
|
||||
partial.Discover(99, 0);
|
||||
if (!ERR_AirfieldCheck(partial.Knowledge() == 1 && partial.ERR_ProbeLocationIcon(), "partial mortar intel displays question mark at coarse location")) return;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||
if (!ERR_AirfieldCheck(UnlockPrison(player), "normal prison unlock starts HQ and mortar activity")) return;
|
||||
// Park only the fixture actor beside the unguarded medical stash so nearby
|
||||
// prison combat does not interrupt the inventory/UI observation sequence.
|
||||
ERR_SpawnPlacement placement = new ERR_SpawnPlacement(); bool parked;
|
||||
for (int spot = 0; spot < 8; spot++)
|
||||
{
|
||||
vector position = ERR_Actors.Ground(Sites().Find("everon-medical-1").Location.Position() + Vector(5 + spot * 2, 0, 5));
|
||||
if (!placement.Clear(position, null, -1, player)) continue;
|
||||
vector transform[4]; player.GetTransform(transform); transform[3] = position;
|
||||
ChimeraCharacter.Cast(player).Teleport(transform); parked = true; break;
|
||||
}
|
||||
if (!ERR_AirfieldCheck(parked, "fixture actor parked on checked terrain for mortar intel observation")) return;
|
||||
m_ERRAirfieldOpeningChecked = true;
|
||||
}
|
||||
if (!ERR_MortarIntelReady()) return;
|
||||
if (!ERR_AirfieldCheck(!Sites().Find("everon-supply-2"), "guarded car-and-crate site absent")) return;
|
||||
ERR_SiteRuntime extraction = Sites().Find("everon-extraction-1");
|
||||
if (!ERR_AirfieldCheck(extraction.Definition.RequireAirfield && vector.DistanceXZ(ExtractionVehicle().GetOrigin(), "4800.953 0 11487.412") < 3 && ERR_CasualtyLoadingRules.AircraftReady(ExtractionVehicle()), "production aircraft settled intact on validated Everon airfield pad")) return;
|
||||
if (!ERR_AirfieldCheck(!ERR_AirfieldPlacement.Clear(ERR_Actors.Ground("7012 0 7500")), "old off-airfield helicopter position rejected")) return;
|
||||
if (!ERR_AirfieldCheck(!ERR_AirfieldPlacement.Clear(ExtractionVehicle().GetOrigin()), "occupied airfield pad rejected")) return;
|
||||
if (!ERR_AirfieldCheck(Sites().Find("everon-medical-1").Definition.RevealOnEscape && Sites().Find("everon-supply-1").Definition.RevealOnEscape, "two reachable intel sites retained")) return;
|
||||
if (!ERR_AirfieldCheck(true, "four stocked operational mortar batteries discovered through physical intel")) return;
|
||||
array<ERR_LocationComponent> locations = {}; Locations(locations);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
location.Discover(0, 100);
|
||||
if (!ERR_AirfieldCheck(location.Knowledge() == 2 && location.ERR_ProbeLocationIcon(), "identified " + location.Id() + " has correct map visibility and icon")) return;
|
||||
}
|
||||
GetGame().GetMenuManager().OpenMenu(ChimeraMenuPreset.MapMenu);
|
||||
m_ERRAirfieldStage = 1; m_ERRAirfieldWait = GetWorld().GetWorldTime() + 3000; return;
|
||||
}
|
||||
SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance();
|
||||
if (m_ERRAirfieldStage == 1)
|
||||
{
|
||||
if (!ERR_AirfieldCheck(mapEntity && mapEntity.IsOpen(), "native fullscreen map open")) return;
|
||||
mapEntity.ZoomPanSmooth(0.08, 5800, 9000, 0.1);
|
||||
m_ERRAirfieldStage = 2; m_ERRAirfieldWait = GetWorld().GetWorldTime() + 3000; return;
|
||||
}
|
||||
if (m_ERRAirfieldStage == 2)
|
||||
{
|
||||
System.MakeScreenshot("$logs:err-airfield-map-icons");
|
||||
Print("ERR AIRFIELD CAPTURE READY");
|
||||
m_ERRAirfieldStage = 3; m_ERRAirfieldWait = GetWorld().GetWorldTime() + 15000; return;
|
||||
}
|
||||
GetGame().GetMenuManager().CloseMenuByPreset(ChimeraMenuPreset.MapMenu);
|
||||
GetGame().GetCallqueue().Remove(ERR_AirfieldTick);
|
||||
// Runner tears down the test world. Avoid mixing native map close with a
|
||||
// same-frame task-list teardown, which has a vanilla delayed-refresh race.
|
||||
Print("ERR SMOKE COMPLETE: airfield, four mortar intel sites, hidden prison and random perimeter guards");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -54,7 +54,7 @@ modded class ERR_GameMode
|
||||
string enabled;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
string nativeEndCase;
|
||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing")) return;
|
||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "aggression" || nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing" || nativeEndCase == "generated-sites" || nativeEndCase == "mortar-station" || nativeEndCase == "opening-tools" || nativeEndCase == "airfield-markers" || nativeEndCase == "air-logistics" || nativeEndCase == "air-assault" || nativeEndCase == "air-recon")) return;
|
||||
ERR_NativeObjectiveProbe.BeginRound();
|
||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||
string outcome;
|
||||
@@ -565,6 +565,11 @@ modded class ERR_GameMode
|
||||
|
||||
if (m_ERRExtractionCase == "prisoner-kit")
|
||||
{
|
||||
int contentMenu = ERR_ProbeAdminMenu(now);
|
||||
if (contentMenu < 0) { ERR_BaseCheck(false, "content admin menu"); return; }
|
||||
if (contentMenu == 0) return;
|
||||
if (!ERR_NativeAdminSettingsProbe.Run(this)) { ERR_BaseCheck(false, "random settings storage and commands"); return; }
|
||||
if (!ERR_NativeContentProbe.Run(this)) { ERR_BaseCheck(false, "content registry and backpack"); return; }
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPrisonerProbe = new ERR_NativePrisonerProbe(this, m_ERRBasePlayer);
|
||||
m_ERRBaseStep = 700;
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeContentProbe
|
||||
{
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: content " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: content " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
if (!ERR_NativeFactionSelectionProbe.Run(mode)) return false;
|
||||
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||
if (!Check(definition && definition.Valid() && definition.Options.Count() == 4, "native pack and three faction definitions load")) return false;
|
||||
ERR_SiteRuntime prison = mode.Sites().Find("everon-prison-1");
|
||||
if (!Check(prison && prison.Container && prison.Container.FindComponent(BaseLoadoutClothComponent), "prison supplies use a wearable ground backpack")) return false;
|
||||
BaseInventoryStorageComponent storage = BaseInventoryStorageComponent.Cast(prison.Container.FindComponent(BaseInventoryStorageComponent));
|
||||
array<IEntity> items = {};
|
||||
if (storage) storage.GetAll(items);
|
||||
array<IEntity> unique = {};
|
||||
int pistols, magazines, maps;
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || unique.Contains(item)) continue;
|
||||
unique.Insert(item);
|
||||
if (!Check(ERR_SpawnSceneInventory.InStorage(item), "bag item has native visible storage ownership")) return false;
|
||||
ResourceName prefab = item.GetPrefabData().GetPrefabName();
|
||||
PrintFormat("ERR CONTENT BAG: item=%1 prefab=%2", item, prefab);
|
||||
if (prefab.Contains("Handgun_PM.et")) pistols++;
|
||||
else if (prefab.Contains("Magazine_9x18_PM_8rnd_Ball.et")) magazines++;
|
||||
else maps++;
|
||||
}
|
||||
if (!Check(unique.Count() == 14 && pistols == 5 && magazines == 8 && maps == 1, "backpack physically contains five pistols, eight magazines and map before admission")) return false;
|
||||
array<string> siteIds = {"everon-supply-1", "everon-radio-1", "everon-supply-2"};
|
||||
array<Faction> factions = {mode.PlayerFaction(), mode.EnemyFaction()};
|
||||
int poolIndex;
|
||||
for (int i = 0; i < siteIds.Count(); i++)
|
||||
{
|
||||
ERR_SiteRuntime site = mode.Sites().Find(siteIds[i]);
|
||||
ResourceName expected;
|
||||
if (site && site.Definition.AllowFactionPool) expected = mode.Content().GuardGroup(poolIndex++);
|
||||
if (expected.IsEmpty()) expected = mode.Factions().GuardGroupPrefab;
|
||||
if (!Check(site && site.Group && site.Group.GetPrefabData().GetPrefabName() == expected && mode.IsMissionEnemy(site.Group.GetFaction()), "selected physical faction group at " + siteIds[i])) return false;
|
||||
if (!factions.Contains(site.Group.GetFaction())) factions.Insert(site.Group.GetFaction());
|
||||
}
|
||||
foreach (Faction a : factions)
|
||||
foreach (Faction b : factions)
|
||||
if (a != b && !Check(a.IsFactionEnemy(b) && b.IsFactionEnemy(a), "mutual hostility " + a.GetFactionKey() + " / " + b.GetFactionKey())) return false;
|
||||
array<string> replies = {};
|
||||
bool original = mode.ContentSettings().Enabled("rhs");
|
||||
mode.Admin().Execute(mode, false, "mods rhs disable", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0] == "Administrator access required." && mode.ContentSettings().Enabled("rhs") == original, "unauthorized mod command rejected")) return false;
|
||||
string path = "$profile:erri-content-probe.json";
|
||||
if (!Check(!FileIO.FileExists(path), "disposable preference path unused")) return false;
|
||||
ERR_ContentSettings settings = new ERR_ContentSettings(path);
|
||||
ERR_ContentPolicy active = new ERR_ContentPolicy(settings);
|
||||
if (!Check(active.Enabled("rhs") && active.Enabled("rhs_usmc") && !active.Enabled("unknown"), "new options default on; unknown IDs denied")) return false;
|
||||
if (!Check(settings.Set("rhs", 0) && active.Enabled("rhs_usmc"), "saving pack disable leaves active round unchanged")) return false;
|
||||
ERR_ContentSettings reloaded = new ERR_ContentSettings(path);
|
||||
ERR_ContentPolicy disabled = new ERR_ContentPolicy(reloaded);
|
||||
if (!Check(!disabled.Enabled("rhs") && !disabled.Enabled("rhs_usmc") && !disabled.Enabled("rhs_afrf") && !disabled.Enabled("rhs_ion") && disabled.GuardGroup(0).IsEmpty(), "reload excludes disabled pack and every child faction")) return false;
|
||||
if (!Check(!disabled.Allows("{A1D5833FF10C6503}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1M2.et") && disabled.Allows("{C850A33226B8F9C1}Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et"), "disabled RHS optics keep vanilla alternatives")) return false;
|
||||
if (!Check(reloaded.Set("rhs", 1) && reloaded.Set("rhs_usmc", 0), "pack and individual faction preferences save independently")) return false;
|
||||
ERR_ContentPolicy selected = new ERR_ContentPolicy(reloaded);
|
||||
if (!Check(selected.Enabled("rhs") && !selected.Enabled("rhs_usmc") && selected.Enabled("rhs_afrf") && selected.Enabled("rhs_ion"), "individual faction disable preserves sibling factions")) return false;
|
||||
if (!Check(!reloaded.Set("unknown", 1) && !reloaded.Set("rhs", 2), "unknown IDs and malformed flags rejected")) return false;
|
||||
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
|
||||
return Check(!FileIO.FileExists(path) && mode.ContentSettings().Enabled("rhs") == original, "disposable preferences cleaned; live preferences unchanged");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -13,7 +13,9 @@ class ERR_NativeContractsProbe
|
||||
if (!Check(mode.Factions() && mode.Factions().WorldReady() && mode.PreparationError().IsEmpty(), "catalog resolves hostile vanilla factions and successful preparation")) return false;
|
||||
if (!Check(SCR_FactionManager.SGetFaction(player) == mode.PlayerFaction() && player.GetPrefabData().GetPrefabName() == mode.Factions().PrisonerPrefab, "physical prisoner matches injected faction and resource")) return false;
|
||||
ERR_SiteRuntime site = mode.Sites().Find("everon-supply-1");
|
||||
if (!Check(site && site.Group && site.Group.GetFaction() == mode.EnemyFaction() && site.Group.GetPrefabData().GetPrefabName() == mode.Factions().GuardGroupPrefab, "physical guard squad comes from injected catalog")) return false;
|
||||
ResourceName expectedGroup = mode.Content().GuardGroup(0);
|
||||
if (expectedGroup.IsEmpty()) expectedGroup = mode.Factions().GuardGroupPrefab;
|
||||
if (!Check(site && site.Group && mode.IsMissionEnemy(site.Group.GetFaction()) && site.Group.GetPrefabData().GetPrefabName() == expectedGroup, "physical guard squad comes from enabled catalog")) return false;
|
||||
ERR_FactionCatalog isolated = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"));
|
||||
if (!isolated) return false;
|
||||
string enemyKey = isolated.EnemyFactionKey;
|
||||
|
||||
@@ -9,7 +9,7 @@ class ERR_NativeDefinitionProbe
|
||||
}
|
||||
static bool Run()
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
if (!Check(definition && definition.Valid(), "authored configuration resolves")) return false;
|
||||
ERR_SiteDefinition cache = definition.Find("everon-supply-1");
|
||||
if (!cache) return false;
|
||||
|
||||
@@ -53,7 +53,7 @@ class ERR_NativeFactionCatalogProbe
|
||||
if (!Check(manager != null, "native faction manager exists")) return false;
|
||||
SCR_Faction us = SCR_Faction.Cast(manager.GetFactionByKey("US"));
|
||||
SCR_Faction ussr = SCR_Faction.Cast(manager.GetFactionByKey("USSR"));
|
||||
if (!Check(us && ussr && mode.PlayerFaction() == us && mode.EnemyFaction() == ussr, "mission retains US and USSR faction identities")) return false;
|
||||
if (!Check(us && ussr && mode.PlayerFaction().GetFactionKey() == mode.FactionSelection().Choice("escapists").Key && mode.EnemyFaction().GetFactionKey() == mode.FactionSelection().Choice("enemy").Key, "mission uses selected faction identities")) return false;
|
||||
if (!Check(us.IsFactionEnemy(ussr) && ussr.IsFactionEnemy(us) && !us.IsFactionEnemy(us) && !ussr.IsFactionEnemy(ussr), "mutual enemy and same-faction relations preserved")) return false;
|
||||
if (!Check(us.GetFactionRadioEncryptionKey() == "chickenNuggets" && ussr.GetFactionRadioEncryptionKey() == "coldBorscht" && us.GetFactionRadioFrequency() == 48000 && ussr.GetFactionRadioFrequency() == 42000, "native radio encryption and frequencies preserved")) return false;
|
||||
if (!Check(us.GetCallsignInfo() && ussr.GetCallsignInfo(), "native faction callsign configurations resolve")) return false;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeFactionSelectionProbe
|
||||
{
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: factions " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: factions " + detail, LogLevel.ERROR); return false;
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
ERR_FactionSelection live = mode.FactionSelection();
|
||||
if (!Check(live && live.Ready() && mode.PlayerFaction() != mode.EnemyFaction(), "server resolves distinct roles before preparation")) return false;
|
||||
Faction independent = GetGame().GetFactionManager().GetFactionByKey(live.Choice("independent").Key);
|
||||
if (!Check(independent && independent != mode.PlayerFaction() && independent != mode.EnemyFaction() && mode.IsMissionEnemy(independent) && !mode.IsMissionEnemy(mode.PlayerFaction()), "independent is hostile while player faction is excluded")) return false;
|
||||
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(player.FindComponent(FactionAffiliationComponent));
|
||||
if (!Check(affiliation && affiliation.GetAffiliatedFaction() == mode.PlayerFaction(), "admitted prisoner uses selected player faction")) return false;
|
||||
map<string, string> saved = new map<string, string>();
|
||||
map<string, string> resolved = new map<string, string>();
|
||||
saved.Set("escapists", "random"); saved.Set("enemy", "us"); saved.Set("independent", "fia");
|
||||
for (int i = 0; i < 30; i++)
|
||||
{
|
||||
array<string> pool = {"us", "ussr", "fia"};
|
||||
if (!ERR_FactionChoices.Resolve(pool, saved, resolved) || resolved.Get("escapists") != "ussr" || resolved.Get("enemy") != "us" || resolved.Get("independent") != "fia") return Check(false, "Random stole a later explicit selection");
|
||||
}
|
||||
if (!Check(true, "Random reserves later explicit roles first")) return false;
|
||||
saved.Set("enemy", "random"); saved.Set("independent", "random");
|
||||
for (int j = 0; j < 60; j++)
|
||||
{
|
||||
array<string> all = {"us", "ussr", "fia", "rhs_afrf", "rhs_usmc", "rhs_ion"};
|
||||
if (!ERR_FactionChoices.Resolve(all, saved, resolved) || !ERR_FactionChoices.Distinct(resolved)) return Check(false, "Random all duplicated a faction");
|
||||
}
|
||||
if (!Check(true, "Random all chooses three different factions")) return false;
|
||||
saved.Set("enemy", "rhs_afrf");
|
||||
array<string> basePool = {"us", "ussr", "fia"};
|
||||
if (!Check(ERR_FactionChoices.Resolve(basePool, saved, resolved) && resolved.Get("enemy") != "rhs_afrf", "disabled choice falls back to eligible random faction")) return false;
|
||||
array<string> replies = {};
|
||||
string previous = live.Saved("escapists");
|
||||
mode.Admin().Execute(mode, false, "factions set random random random", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0] == "Administrator access required." && live.Saved("escapists") == previous, "unauthorized role changes rejected")) return false;
|
||||
string path = "$profile:erri-faction-probe.json";
|
||||
ERR_FactionSelection settings = new ERR_FactionSelection(path);
|
||||
array<string> valid = {"random", "ussr", "fia"};
|
||||
array<string> duplicate = {"us", "us", "fia"};
|
||||
array<string> unknown = {"unknown", "ussr", "fia"};
|
||||
if (!Check(settings.Save(valid, mode.ContentSettings()) && !settings.Save(duplicate, mode.ContentSettings()) && !settings.Save(unknown, mode.ContentSettings()), "atomic persistence rejects duplicate and unknown selections")) return false;
|
||||
ERR_FactionSelection reloaded = new ERR_FactionSelection(path);
|
||||
if (!Check(reloaded.Saved("escapists") == "random" && reloaded.Saved("enemy") == "ussr" && reloaded.Saved("independent") == "fia", "saved roles survive reload and rejected changes")) return false;
|
||||
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,138 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_LocationComponent
|
||||
{
|
||||
bool ERR_ProbeGeneratedMarker(bool expected)
|
||||
{
|
||||
ERR_UpdateMarker();
|
||||
if (!expected) return m_ERRMarker == null;
|
||||
if (!m_ERRMarker) return false;
|
||||
int point[2]; m_ERRMarker.GetWorldPos(point);
|
||||
return point[0] == ERR_DiscoveryMapRules.Coordinate(Position()[0], Knowledge()) && point[1] == ERR_DiscoveryMapRules.Coordinate(Position()[2], Knowledge());
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERRGeneratedStage;
|
||||
protected float m_ERRGeneratedWait;
|
||||
protected int m_ERRGeneratedDeadline;
|
||||
protected IEntity m_ERRGeneratedPlayer, m_ERRGeneratedNear, m_ERRGeneratedFar, m_ERRGeneratedSeated;
|
||||
protected Vehicle m_ERRGeneratedVehicle;
|
||||
protected ERR_LocationComponent m_ERRGeneratedRadio;
|
||||
protected ref array<IEntity> m_ERRGeneratedTeam = {};
|
||||
protected ref map<int, vector> m_ERRGeneratedReservations = new map<int, vector>();
|
||||
protected bool m_ERRGeneratedTeamChecked;
|
||||
protected int ERR_GeneratedTeamSlots(IEntity player)
|
||||
{
|
||||
if (m_ERRGeneratedTeamChecked) return 1;
|
||||
ERR_SpawnPlacement placement = new ERR_SpawnPlacement();
|
||||
if (m_ERRGeneratedTeam.IsEmpty() && !ERR_GeneratedCheck(!placement.Clear(player.GetOrigin(), null), "physical player blocks occupied slot without a reservation")) return -1;
|
||||
if (m_ERRGeneratedTeam.IsEmpty() && !ERR_GeneratedCheck(!placement.Clear(ERR_Actors.Ground(PrisonPosition() + ERR_CampTerrain.Rotate("0 0 6", PrisonHeading())), null), "rotated physical wall blocks spawn volume")) return -1;
|
||||
m_ERRGeneratedReservations.Set(0, player.GetOrigin());
|
||||
if (m_ERRGeneratedTeam.Count() < 11)
|
||||
{
|
||||
vector position;
|
||||
if (!ERR_GeneratedCheck(placement.Prison(PrisonPosition(), m_ERRGeneratedReservations, position, PrisonHeading()), "rotated prison clear slot for member " + (m_ERRGeneratedTeam.Count() + 2))) return -1;
|
||||
IEntity actor = ERR_Actors.Spawn(Factions().PrisonerPrefab, position);
|
||||
if (!ERR_GeneratedCheck(actor != null, "additional physical prisoner created")) return -1;
|
||||
Own(actor); m_ERRGeneratedTeam.Insert(actor); m_ERRGeneratedReservations.Set(m_ERRGeneratedTeam.Count(), position);
|
||||
DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
return 0;
|
||||
}
|
||||
foreach (IEntity prisoner : m_ERRGeneratedTeam) ERR_Actors.DeleteSpawned(prisoner);
|
||||
m_ERRGeneratedTeam.Clear(); m_ERRGeneratedReservations.Clear(); m_ERRGeneratedTeamChecked = true;
|
||||
return 1;
|
||||
}
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "generated-sites") return;
|
||||
m_ERRGeneratedDeadline = System.GetTickCount() + 120000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_GeneratedProbeTick, 500, true);
|
||||
}
|
||||
protected bool ERR_GeneratedCheck(bool value, string detail)
|
||||
{
|
||||
if (value) { Print("ERR SMOKE PASS: generated " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: generated " + detail, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_GeneratedProbeTick); return false;
|
||||
}
|
||||
protected void ERR_GeneratedProbeTick()
|
||||
{
|
||||
if (System.GetTickCount() > m_ERRGeneratedDeadline || !PreparationError().IsEmpty()) { ERR_GeneratedCheck(false, "deadline or preparation failure: " + PreparationError()); return; }
|
||||
if (!Active() || GetGame().GetWorld().GetWorldTime() < m_ERRGeneratedWait) return;
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller || !controller.GetControlledEntity()) return;
|
||||
if (m_ERRGeneratedStage == 0)
|
||||
{
|
||||
m_ERRGeneratedPlayer = controller.GetControlledEntity();
|
||||
if (ERR_GeneratedTeamSlots(m_ERRGeneratedPlayer) != 1) return;
|
||||
ERR_SiteRuntime prison = Sites().Find("everon-prison-1"); ERR_SiteRuntime radio = Sites().Find("everon-radio-1");
|
||||
if (!ERR_GeneratedCheck(prison && radio && prison.Definition.PlacementResolved && radio.Definition.PlacementResolved, "both generated locations committed before admission")) return;
|
||||
if (!ERR_GeneratedCheck(vector.DistanceXZ(prison.Definition.Position, radio.Definition.Position) >= 1000 && vector.DistanceXZ(prison.Definition.Position, "6090 0 7160") > 1, "fresh island prison and separated radio")) return;
|
||||
vector local = ERR_CampTerrain.Local(m_ERRGeneratedPlayer.GetOrigin() - PrisonPosition(), PrisonHeading());
|
||||
if (!ERR_GeneratedCheck(Math.AbsFloat(local[0]) <= 5.5 && Math.AbsFloat(local[2]) <= 5.5 && PrisonEscape().Registered(), "player inside rotated prison with intact registered gate")) return;
|
||||
BaseInventoryStorageComponent bag = BaseInventoryStorageComponent.Cast(prison.Container.FindComponent(BaseInventoryStorageComponent));
|
||||
array<IEntity> contents = {}; if (bag) bag.GetAll(contents);
|
||||
if (!ERR_GeneratedCheck(contents.Count() == 14 && radio.Group.GetFaction() == EnemyFaction(), "starter backpack ready and camp guards use enemy faction")) return;
|
||||
m_ERRGeneratedRadio = radio.Location;
|
||||
if (!ERR_GeneratedCheck(radio.Props.Count() == 5, "radio composition has five independently owned scenery objects")) return;
|
||||
if (!ERR_GeneratedCheck(m_ERRGeneratedRadio.ERR_ProbeGeneratedMarker(false), "undiscovered radio location hidden")) return;
|
||||
m_ERRGeneratedRadio.Discover(99, 0);
|
||||
if (!ERR_GeneratedCheck(m_ERRGeneratedRadio.Knowledge() == 1 && m_ERRGeneratedRadio.ERR_ProbeGeneratedMarker(true), "partial intel creates approximate question marker")) return;
|
||||
m_ERRGeneratedRadio.Discover(99, 0);
|
||||
if (!ERR_GeneratedCheck(m_ERRGeneratedRadio.Knowledge() == 2 && m_ERRGeneratedRadio.ERR_ProbeGeneratedMarker(true), "further intel reveals actual generated coordinates")) return;
|
||||
m_ERRGeneratedNear = ERR_Actors.Spawn(Factions().PrisonerPrefab, ERR_Actors.Ground(PrisonPosition() + "0 0 15")); Own(m_ERRGeneratedNear);
|
||||
vector far;
|
||||
for (int radius = 1200; radius <= 6000 && far == vector.Zero; radius += 300)
|
||||
for (int i = 0; i < 36; i++)
|
||||
{
|
||||
vector candidate = ERR_Actors.Ground(PrisonPosition() + ERR_CampTerrain.Rotate(Vector(radius, 0, 0), i * 10));
|
||||
if (candidate[1] > 1 && !ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetWorld(), candidate)) { far = candidate; break; }
|
||||
}
|
||||
if (!ERR_GeneratedCheck(far != vector.Zero, "dry outside-radius test location available")) return;
|
||||
m_ERRGeneratedFar = ERR_Actors.Spawn(Factions().PrisonerPrefab, far); Own(m_ERRGeneratedFar);
|
||||
m_ERRGeneratedVehicle = Vehicle.Cast(ERR_Actors.Spawn("{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et", ERR_Actors.Ground(PrisonPosition() + "0 0 20"))); Own(m_ERRGeneratedVehicle);
|
||||
BaseCompartmentManagerComponent seats = BaseCompartmentManagerComponent.Cast(m_ERRGeneratedVehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
array<BaseCompartmentSlot> slots = {}; seats.GetCompartments(slots);
|
||||
foreach (BaseCompartmentSlot slot : slots)
|
||||
{
|
||||
if (slot.GetType() != ECompartmentType.CARGO) continue;
|
||||
AIGroup group;
|
||||
m_ERRGeneratedSeated = slot.SpawnCharacterInCompartment(Factions().PrisonerPrefab, group, "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et");
|
||||
Own(group); Own(m_ERRGeneratedSeated); break;
|
||||
}
|
||||
if (!ERR_GeneratedCheck(m_ERRGeneratedNear && m_ERRGeneratedFar && m_ERRGeneratedVehicle && m_ERRGeneratedSeated, "friendly foot AI, distant AI and seated AI fixtures created")) return;
|
||||
m_ERRGeneratedWait = GetWorld().GetWorldTime() + 2000; m_ERRGeneratedStage = 1; return;
|
||||
}
|
||||
if (m_ERRGeneratedStage == 1)
|
||||
{
|
||||
if (!ERR_GeneratedCheck(ERR_Actors.Alive(m_ERRGeneratedNear) && ERR_Actors.Alive(m_ERRGeneratedFar) && ERR_Actors.Alive(m_ERRGeneratedSeated) && CompartmentAccessComponent.GetVehicleIn(m_ERRGeneratedSeated) == m_ERRGeneratedVehicle, "targets alive and passenger physically seated")) return;
|
||||
array<string> denied = {}; Admin().Execute(this, false, "killais", denied);
|
||||
if (!ERR_GeneratedCheck(denied.Count() == 1 && denied[0] == "Administrator access required." && ERR_Actors.Alive(m_ERRGeneratedNear), "unauthorized kill request refused")) return;
|
||||
controller.ERR_AdminSubmit("killais");
|
||||
m_ERRGeneratedWait = GetWorld().GetWorldTime() + 2000; m_ERRGeneratedStage = 2; return;
|
||||
}
|
||||
if (m_ERRGeneratedStage == 2)
|
||||
{
|
||||
if (!ERR_GeneratedCheck(!ERR_Actors.Alive(m_ERRGeneratedNear) && !ERR_Actors.Alive(m_ERRGeneratedSeated) && ERR_Actors.Alive(m_ERRGeneratedFar) && ERR_Actors.Alive(m_ERRGeneratedVehicle) && ERR_Actors.Alive(m_ERRGeneratedPlayer), "authenticated killais kills nearby foot and seated AI, preserves player, vehicle and distant AI")) return;
|
||||
if (!ERR_GeneratedCheck(UnlockPrison(m_ERRGeneratedPlayer), "generated prison unlock enters normal progression")) return;
|
||||
vector approach = ERR_Actors.Ground(m_ERRGeneratedRadio.Position() + ERR_CampTerrain.Rotate("0 0 -1.5", Sites().Find("everon-radio-1").Definition.Heading));
|
||||
m_ERRGeneratedPlayer.SetOrigin(approach);
|
||||
m_ERRGeneratedWait = GetWorld().GetWorldTime() + 1000; m_ERRGeneratedStage = 3; return;
|
||||
}
|
||||
if (m_ERRGeneratedStage == 3)
|
||||
{
|
||||
if (!ERR_GeneratedCheck(TuneRadio(m_ERRGeneratedPlayer, m_ERRGeneratedRadio) && Phase() == ERR_Phase.RADIO_DEFENCE, "generated radio starts existing transmission objective")) return;
|
||||
CharacterControllerComponent.Cast(m_ERRGeneratedPlayer.FindComponent(CharacterControllerComponent)).SetHeadingAngle(Sites().Find("everon-radio-1").Definition.Heading * Math.DEG2RAD, true);
|
||||
Print("ERR GENERATED CAPTURE READY");
|
||||
m_ERRGeneratedWait = GetWorld().GetWorldTime() + 3000; m_ERRGeneratedStage = 4; return;
|
||||
}
|
||||
if (m_ERRGeneratedStage == 4)
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(ERR_GeneratedProbeTick);
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: generated sites, actual intel markers, admin killais and radio progression; scripted local fixture");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,109 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected int m_ERROpeningStage;
|
||||
protected int m_ERROpeningDeadline;
|
||||
protected ref array<IEntity> m_ERROpeningDocuments = {};
|
||||
protected SCR_AIGroup m_ERROpeningFallbackGroup;
|
||||
protected ref ERR_PrisonPatrol m_ERROpeningFallback;
|
||||
protected ref ERR_AdminItemCallback m_ERROpeningPickup;
|
||||
protected IEntity m_ERROpeningPicked;
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string outcome;
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "opening-tools") return;
|
||||
m_ERROpeningDeadline = System.GetTickCount() + 150000;
|
||||
GetGame().GetCallqueue().CallLater(ERR_OpeningTick, 500, true);
|
||||
}
|
||||
protected bool ERR_OpeningCheck(bool value, string text)
|
||||
{
|
||||
if (value) { Print("ERR SMOKE PASS: opening " + text); return true; }
|
||||
Print("ERR SMOKE FAIL: opening " + text, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_OpeningTick); return false;
|
||||
}
|
||||
protected bool ERR_OpeningDocument(IEntity entity)
|
||||
{
|
||||
if (entity && entity.GetPrefabData() && entity.GetPrefabData().GetPrefabName() == ERR_Resources.IntelFolder()) m_ERROpeningDocuments.Insert(entity);
|
||||
return true;
|
||||
}
|
||||
protected bool ERR_OpeningAircraftAudit(IEntity entity)
|
||||
{
|
||||
if (!entity || !entity.GetPrefabData()) return true;
|
||||
string prefab = entity.GetPrefabData().GetPrefabName(); string name = prefab; name.ToLower();
|
||||
if (name.Contains("helicopter") || name.Contains("mi8") || name.Contains("uh1"))
|
||||
PrintFormat("ERR OPENING AIRFRAME: prefab=%1 position=%2 extraction=%3", prefab, entity.GetOrigin(), entity == ExtractionVehicle());
|
||||
return true;
|
||||
}
|
||||
protected void ERR_OpeningTick()
|
||||
{
|
||||
if (System.GetTickCount() >= m_ERROpeningDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_OpeningCheck(false, "fixture exceeded deadline or mission failed"); return; }
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
IEntity player; if (controller) player = controller.GetControlledEntity();
|
||||
if (Phase() != ERR_Phase.PRISON || !player || !Players().CurrentActor(player)) return;
|
||||
if (m_ERROpeningStage == 0)
|
||||
{
|
||||
array<ResourceName> roster = {"rifleman", "squadleader", "Character_RTO.et", "Character_RTO_second.et"};
|
||||
for (int size = 3; size <= 5; size++)
|
||||
{
|
||||
array<ResourceName> selected = ERR_PrisonPatrol.Members(roster, size);
|
||||
int operators; foreach (ResourceName prefab : selected) if (prefab.Contains("RTO")) operators++;
|
||||
if (!ERR_OpeningCheck(selected.Count() == size && operators == 1, "roster selection retains exactly one RTO at size " + size)) return;
|
||||
}
|
||||
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
|
||||
GetWorld().QueryEntitiesByAABB(low, high, ERR_OpeningAircraftAudit, null, EQueryEntitiesFlags.ALL);
|
||||
ERR_SiteRuntime prison = Sites().Find("everon-prison-1");
|
||||
array<AIAgent> guards = {}; prison.Group.GetAgents(guards);
|
||||
if (!ERR_OpeningCheck(guards.Count() >= 3 && guards.Count() <= 5 && guards.Count() == prison.InitialGuardCount, "3-5 physical prison guards")) return;
|
||||
int radios;
|
||||
foreach (AIAgent guard : guards)
|
||||
{
|
||||
if (!ERR_OpeningCheck(!guard.IsAIActivated() && ERR_Actors.CanFight(guard.GetControlledEntity()), "guard held alive until lockpick")) return;
|
||||
if (ERR_HQRadio.Capability(guard.GetControlledEntity()) == 2) radios++;
|
||||
}
|
||||
if (!ERR_OpeningCheck(radios >= 1, "large manpack radio physically equipped")) return;
|
||||
if (!ERR_OpeningCheck(ERR_CasualtyLoadingRules.AircraftReady(ExtractionVehicle()), "initial extraction helicopter intact and grounded")) return;
|
||||
// Test the fallback on a faction roster containing no RTO.
|
||||
m_ERROpeningFallbackGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Actors.Ground(prison.Group.GetOrigin() + "10 0 10")));
|
||||
Own(m_ERROpeningFallbackGroup);
|
||||
m_ERROpeningFallbackGroup.SetFaction(EnemyFaction());
|
||||
m_ERROpeningFallbackGroup.m_aUnitPrefabSlots = {"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"};
|
||||
m_ERROpeningFallbackGroup.RequestSpawn(1);
|
||||
m_ERROpeningFallback = new ERR_PrisonPatrol();
|
||||
array<string> denied = {}; Admin().Execute(this, false, "cheat intel", denied);
|
||||
if (!ERR_OpeningCheck(denied.Count() == 1 && denied[0] == "Administrator access required.", "unauthorized intel command denied")) return;
|
||||
controller.ERR_AdminSubmit("cheat intel");
|
||||
m_ERROpeningStage = 1; return;
|
||||
}
|
||||
if (m_ERROpeningStage == 1)
|
||||
{
|
||||
m_ERROpeningDocuments.Clear();
|
||||
GetWorld().QueryEntitiesBySphere(player.GetOrigin(), 6, ERR_OpeningDocument, null, EQueryEntitiesFlags.ALL);
|
||||
if (m_ERROpeningDocuments.IsEmpty()) return;
|
||||
if (!ERR_OpeningCheck(m_ERROpeningDocuments.Count() == 6, "authenticated command spawned exactly six physical intel documents")) return;
|
||||
foreach (IEntity document : m_ERROpeningDocuments)
|
||||
{
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(document.FindComponent(InventoryItemComponent));
|
||||
if (!ERR_OpeningCheck(item && !item.GetParentSlot() && Math.AbsFloat(document.GetOrigin()[1] - ERR_Actors.Ground(document.GetOrigin())[1]) < 0.4 && !Discovery().IsSpent(document), "fresh document lies on nearby ground outside inventory")) return;
|
||||
}
|
||||
m_ERROpeningPicked = m_ERROpeningDocuments[0];
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(player.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
m_ERROpeningPickup = new ERR_AdminItemCallback();
|
||||
if (!ERR_OpeningCheck(inventory.TryInsertItem(m_ERROpeningPicked, EStoragePurpose.PURPOSE_DEPOSIT, m_ERROpeningPickup), "ground document pickup accepted")) return;
|
||||
m_ERROpeningStage = 2; return;
|
||||
}
|
||||
if (m_ERROpeningStage == 2)
|
||||
{
|
||||
ERR_PrepareResult radio = m_ERROpeningFallback.Radio(this, m_ERROpeningFallbackGroup, GetWorld().GetWorldTime() + 10000);
|
||||
if (radio == ERR_PrepareResult.FAILED) { ERR_OpeningCheck(false, "fallback radio insertion failed"); return; }
|
||||
if (radio != ERR_PrepareResult.READY || !m_ERROpeningPickup.Finished || !Discovery().IsSpent(m_ERROpeningPicked)) return;
|
||||
if (!ERR_OpeningCheck(m_ERROpeningPickup.Succeeded, "picked-up cheat document consumed by normal discovery")) return;
|
||||
if (!ERR_OpeningCheck(ERR_HQRadio.Capability(m_ERROpeningFallbackGroup.GetLeaderEntity()) == 2, "non-RTO loadout receives actual large radio")) return;
|
||||
if (!ERR_OpeningCheck(UnlockPrison(player), "normal lockpick progression remains available")) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_OpeningTick);
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
Print("ERR SMOKE COMPLETE: prison patrol, large radio, ground intel admin command and discovery");
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -38,7 +38,7 @@ class ERR_NativePatrolProbe
|
||||
}
|
||||
protected bool DefinitionChecks()
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
if (!Check(definition && definition.Valid(), "authored definition resolves")) return false;
|
||||
ERR_SiteDefinition site = definition.Find("everon-outpost-1");
|
||||
if (!Check(site && site.Patrol && site.Patrol.Count() == 4, "four ordered outpost points authored")) return false;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_HQ
|
||||
{
|
||||
bool ERR_ProbeRadioReservation(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
if (!unit.ReservedForSupport || !unit.HomeGuard || unit.Eligible(m_Mode)) return false;
|
||||
if (ReserveLogistics(group, 999)) return false;
|
||||
ReleaseMortarCrew(group); // Only actual destroyed mortar crews may be released.
|
||||
return unit.ReservedForSupport && !unit.Eligible(m_Mode) && !unit.Waypoint && !unit.PendingOrder && !unit.Assignment;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -27,7 +27,7 @@ class ERR_NativeTransportProbe
|
||||
}
|
||||
protected bool DefinitionChecks()
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
ERR_SiteDefinition site;
|
||||
if (definition) site = definition.Find("everon-supply-2");
|
||||
if (!Check(site && site.Transport && definition.Valid(), "authored transport definition resolves")) return false;
|
||||
@@ -189,7 +189,7 @@ class ERR_NativeTransportProbe
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(!m_Site.Transport && m_Mode.Sites().TransportCount() == 0 && m_Mode.Sites().Prepare() == ERR_PrepareResult.READY, "lost transport remains absent after service ticks and preparation queries")) return -1;
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
if (!Check(!m_Mode.Sites().Begin(definition, ERR_Supplies.Map()) && m_Mode.Sites().TransportCount() == 0, "duplicate initialization cannot replace lost transport")) return -1;
|
||||
m_Mode.Sites().Stop();
|
||||
if (!Check(m_Mode.Sites().Prepare() == ERR_PrepareResult.FAILED && !m_Mode.Sites().Begin(definition, ERR_Supplies.Map()), "stopped finite owner cannot reinitialize transport")) return -1;
|
||||
|
||||
@@ -15,7 +15,7 @@ class ERR_NativeTransportSurvey
|
||||
{
|
||||
static void Run(ERR_GameMode mode)
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf"));
|
||||
ERR_SiteDefinition site = definition.Find("everon-supply-2");
|
||||
int clear;
|
||||
for (int x = -40; x <= 40; x += 8)
|
||||
|
||||
@@ -54,8 +54,8 @@ class ERR_ObjectiveText
|
||||
}
|
||||
// Scan all sites before acknowledging rough intel: an identified radio wins.
|
||||
if (roughRadio)
|
||||
return "Intel recovered: a 500 m search square is now marked on the map. Use a carried field map to view it. Take a different intelligence folder into your inventory to pinpoint the radio; a folder already recovered cannot provide another clue. Try the other marked supply cache if needed. A close approach with a clear view can also identify the station.";
|
||||
return "Choose either marked supply cache. Take its intelligence folders into your own inventory to reveal the station on your map. If only a search area appears, take another folder. Each cache has enough folders; use the other if one is lost. If every folder is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can also be discovered by approaching with a clear view.";
|
||||
return "Intel recovered: a question mark shows an approximate location, but its type is still unknown. Take a different intelligence folder into your inventory to pinpoint the radio; a folder already recovered cannot provide another clue. Try the other marked supply cache if needed. A close approach with a clear view can also identify the station.";
|
||||
return "Choose either marked supply cache. Take its intelligence folders into your own inventory to reveal the station on your map. If the location is not yet identified, take another folder. Each cache has enough folders; use the other if one is lost. If every folder is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can also be discovered by approaching with a clear view.";
|
||||
}
|
||||
if (phase == ERR_Phase.RADIO_DEFENCE)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ modded class SCR_ChimeraAIAgent
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
IEntity actor = GetControlledEntity();
|
||||
if (!mode || mode.Phase() > ERR_Phase.PRISON || !actor || ERR_Actors.Player(actor) || IsPlayerPending_S()) return;
|
||||
if (!mode.EnemyFaction() || SCR_FactionManager.SGetFaction(actor) != mode.EnemyFaction()) return;
|
||||
if (!mode.IsMissionEnemy(SCR_FactionManager.SGetFaction(actor))) return;
|
||||
DeactivateAI();
|
||||
if (m_ERRPrisonMode) return;
|
||||
m_ERRPrisonMode = mode;
|
||||
|
||||
@@ -161,7 +161,7 @@ class ERR_PrisonEscapeService
|
||||
vector position = actor.GetOrigin();
|
||||
float ground = GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
|
||||
if (!ERR_PrisonEscapeRules.Grounded(position[1] - ground)) continue;
|
||||
vector offset = position - m_Center;
|
||||
vector offset = ERR_CampTerrain.Local(position - m_Center, m_Mode.PrisonHeading());
|
||||
ERR_PrisonEscapeSample sample = m_Samples.Get(life.AccountId);
|
||||
bool armed;
|
||||
if (sample && sample.Actor == actor && sample.Controller == controller)
|
||||
@@ -169,7 +169,7 @@ class ERR_PrisonEscapeService
|
||||
float elapsed = now - sample.ObservedAt;
|
||||
float displacement = vector.Distance(position, sample.Position);
|
||||
bool continuous = ERR_PrisonEscapeRules.Continuous(elapsed, displacement);
|
||||
vector previousOffset = sample.Position - m_Center;
|
||||
vector previousOffset = ERR_CampTerrain.Local(sample.Position - m_Center, m_Mode.PrisonHeading());
|
||||
if (ERR_PrisonEscapeRules.Crossing(sample.Armed, previousOffset[0], previousOffset[2], offset[0], offset[2], elapsed, displacement))
|
||||
{
|
||||
// An actual qualifying exit is sufficient even if the gate remains intact.
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// One finite opening patrol. Build the selected faction's roster before asking
|
||||
// the native spawn queue for any members; never spawn a full squad and trim it.
|
||||
class ERR_PrisonPatrol
|
||||
{
|
||||
protected IEntity m_Radio;
|
||||
protected bool m_RequestedRadio;
|
||||
|
||||
protected bool m_Placed;
|
||||
bool Place(SCR_AIGroup group, vector center)
|
||||
{
|
||||
if (m_Placed) return true;
|
||||
if (!Replication.IsServer() || !group) return false;
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
if (agents.Count() < 3 || agents.Count() > 5) return false;
|
||||
map<int, vector> positions = new map<int, vector>();
|
||||
ERR_SpawnPlacement placement = new ERR_SpawnPlacement();
|
||||
float start = Math.RandomFloat(0, 360);
|
||||
float sector = 360.0 / agents.Count();
|
||||
for (int index = 0; index < agents.Count(); index++)
|
||||
{
|
||||
if (!agents[index] || !ChimeraCharacter.Cast(agents[index].GetControlledEntity())) return false;
|
||||
bool found;
|
||||
for (int attempt = 0; attempt < 80; attempt++)
|
||||
{
|
||||
float angle = (start + sector * index + Math.RandomFloat(-sector * 0.3, sector * 0.3)) * Math.DEG2RAD;
|
||||
float radius = Math.RandomFloat(12, 24);
|
||||
vector position = ERR_Actors.Ground(center + Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius));
|
||||
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) continue;
|
||||
if (!placement.Clear(position, positions, index, agents[index].GetControlledEntity())) continue;
|
||||
positions.Insert(index, position); found = true; break;
|
||||
}
|
||||
if (!found) return false;
|
||||
}
|
||||
for (int member = 0; member < agents.Count(); member++)
|
||||
{
|
||||
ChimeraCharacter actor = ChimeraCharacter.Cast(agents[member].GetControlledEntity());
|
||||
vector transform[4];
|
||||
Math3D.AnglesToMatrix(Vector(Math.RandomFloat(0, 360), 0, 0), transform);
|
||||
transform[3] = positions.Get(member);
|
||||
actor.Teleport(transform);
|
||||
}
|
||||
m_Placed = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
static array<ResourceName> Members(array<ResourceName> source, int count)
|
||||
{
|
||||
array<ResourceName> result = {};
|
||||
if (!source || source.IsEmpty() || count < 3 || count > 5) return result;
|
||||
array<ResourceName> soldiers = {};
|
||||
ResourceName radioman;
|
||||
foreach (ResourceName prefab : source)
|
||||
{
|
||||
string name = prefab; name.ToLower();
|
||||
if (name.Contains("_rto") || name.Contains("radioman"))
|
||||
{ if (radioman.IsEmpty()) radioman = prefab; }
|
||||
else if (!prefab.IsEmpty()) soldiers.Insert(prefab);
|
||||
}
|
||||
if (!radioman.IsEmpty()) result.Insert(radioman);
|
||||
if (soldiers.IsEmpty()) soldiers.Insert(source[0]);
|
||||
int index;
|
||||
while (result.Count() < count) { result.Insert(soldiers[index % soldiers.Count()]); index++; }
|
||||
return result;
|
||||
}
|
||||
|
||||
static bool Configure(SCR_AIGroup group, ResourceName profile, Faction faction)
|
||||
{
|
||||
if (!Replication.IsServer() || !group || !faction || group.GetAgentsCount() != 0) return false;
|
||||
Resource resource = Resource.Load(profile);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
IEntitySource source = resource.GetResource().ToEntitySource();
|
||||
array<ResourceName> roster = {};
|
||||
if (!source || !source.Get("m_aUnitPrefabSlots", roster)) return false;
|
||||
group.m_aUnitPrefabSlots = Members(roster, Math.RandomIntInclusive(3, 5));
|
||||
if (group.m_aUnitPrefabSlots.IsEmpty()) return false;
|
||||
group.SetFaction(faction);
|
||||
group.SetSpawnImmediately(true);
|
||||
group.RequestSpawn(group.m_aUnitPrefabSlots.Count());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Existing faction RTO loadouts are preferred. If a mod's fireteam has none,
|
||||
// equip one soldier with a real large radio, replacing its backpack if needed.
|
||||
ERR_PrepareResult Radio(ERR_GameMode mode, SCR_AIGroup group, float deadline)
|
||||
{
|
||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||
foreach (AIAgent member : agents)
|
||||
if (member && ERR_HQRadio.Capability(member.GetControlledEntity()) == 2) return ERR_PrepareResult.READY;
|
||||
if (GetGame().GetWorld().GetWorldTime() >= deadline) return ERR_PrepareResult.FAILED;
|
||||
if (m_RequestedRadio) return ERR_PrepareResult.PENDING;
|
||||
if (agents.IsEmpty() || !agents[0]) return ERR_PrepareResult.PENDING;
|
||||
IEntity actor = agents[0].GetControlledEntity();
|
||||
if (!actor) return ERR_PrepareResult.PENDING;
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(actor.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!inventory) return ERR_PrepareResult.PENDING;
|
||||
m_RequestedRadio = true;
|
||||
ResourceName radio = "{54C68E438DD34265}Prefabs/Items/Equipment/Radios/Radio_R107M.et";
|
||||
if (group.GetFaction().GetFactionKey() == "US" || group.GetFaction().GetFactionKey() == "RHS_USAF")
|
||||
radio = "{9B6B61BB3FE3DFB0}Prefabs/Items/Equipment/Radios/Radio_ANPRC77.et";
|
||||
m_Radio = ERR_Actors.Spawn(radio, actor.GetOrigin());
|
||||
if (!m_Radio) return ERR_PrepareResult.FAILED;
|
||||
mode.Own(m_Radio);
|
||||
inventory.EquipCloth(m_Radio);
|
||||
return ERR_PrepareResult.PENDING;
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class ERR_RadioDefenceService
|
||||
int DurationSeconds()
|
||||
{
|
||||
if (!m_Mode || !m_Mode.Settings()) return 0;
|
||||
return m_Mode.Settings().Get("radioDefenceSeconds");
|
||||
return m_Mode.Settings().Runtime("radioDefenceSeconds");
|
||||
}
|
||||
int SecondsRemaining(float now)
|
||||
{
|
||||
|
||||
@@ -6,12 +6,17 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
protected ResourceName m_Definition;
|
||||
protected ref ERR_SiteService m_Sites;
|
||||
protected bool m_Started;
|
||||
protected ref ERR_ScenarioDefinition m_RuntimeDefinition;
|
||||
protected ref ERR_GeneratedSites m_Generated;
|
||||
protected bool m_Finished;
|
||||
protected bool m_Failed;
|
||||
|
||||
ERR_SiteService Sites() { return m_Sites; }
|
||||
string FailureReason()
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq && hq.Scouts() && !hq.Scouts().Error().IsEmpty()) return hq.Scouts().Error();
|
||||
if (m_Generated && !m_Generated.Error().IsEmpty()) return m_Generated.Error();
|
||||
if (m_Sites && !m_Sites.FailureReason().IsEmpty()) return m_Sites.FailureReason();
|
||||
return "Scenario or additional force preparation failed.";
|
||||
}
|
||||
@@ -23,12 +28,35 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
if (!m_Started)
|
||||
{
|
||||
m_Started = true;
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||
#ifdef WORKBENCH
|
||||
string historicalFixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||
#endif
|
||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||
if (!m_RuntimeDefinition || !m_RuntimeDefinition.Valid()) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||
#ifdef WORKBENCH
|
||||
// Existing deterministic probes intentionally use authored coordinates.
|
||||
string fixture;
|
||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||
#endif
|
||||
m_Generated = new ERR_GeneratedSites();
|
||||
if (!m_Generated.Begin(m_RuntimeDefinition)) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||
}
|
||||
ERR_PrepareResult placement = m_Generated.Tick();
|
||||
if (placement == ERR_PrepareResult.FAILED) m_Failed = true;
|
||||
if (placement != ERR_PrepareResult.READY) return placement;
|
||||
if (!m_Sites)
|
||||
{
|
||||
m_Sites = new ERR_SiteService(mode);
|
||||
if (!m_Sites.Begin(definition, FieldMap())) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||
if (!m_Sites.Begin(m_RuntimeDefinition, FieldMap())) { m_Failed = true; return ERR_PrepareResult.FAILED; }
|
||||
}
|
||||
ERR_PrepareResult result = m_Sites.Prepare();
|
||||
if (result != ERR_PrepareResult.READY) return result;
|
||||
ERR_PrepareResult population = PrepareAdditionalPopulation();
|
||||
if (population == ERR_PrepareResult.FAILED) { m_Failed = true; m_Sites.Rollback(); }
|
||||
if (population != ERR_PrepareResult.READY) return population;
|
||||
if (!PrepareAdditionalForces())
|
||||
{
|
||||
m_Failed = true;
|
||||
@@ -47,6 +75,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.Active() && m_Sites) m_Sites.Activate();
|
||||
}
|
||||
protected ERR_PrepareResult PrepareAdditionalPopulation() { return ERR_PrepareResult.READY; }
|
||||
protected bool PrepareAdditionalForces() { return true; }
|
||||
protected ResourceName FieldMap() { return ERR_Supplies.Map(); }
|
||||
protected override void OnDelete(IEntity owner)
|
||||
|
||||
@@ -3,16 +3,19 @@ class ERR_Settings
|
||||
{
|
||||
static const string PATH = "$profile:rereintroduced.json";
|
||||
protected ref map<string, int> m_Values = new map<string, int>();
|
||||
protected ref map<string, int> m_Resolved = new map<string, int>();
|
||||
protected bool m_ReadOnly;
|
||||
protected string m_Path;
|
||||
|
||||
static array<string> Keys()
|
||||
{
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "stingerChanceOneIn", "automaticConvoyEnabled"};
|
||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "stingerChanceOneIn", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||
}
|
||||
|
||||
static bool Valid(string key, int value)
|
||||
{
|
||||
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
||||
if (key == "convoyCount") return value >= 0 && value <= 20;
|
||||
if (key == "automaticConvoyEnabled") return value == 0 || value == 1;
|
||||
if (key == "stingerChanceOneIn") return value >= 0 && value <= 10000;
|
||||
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
|
||||
@@ -44,8 +47,57 @@ class ERR_Settings
|
||||
return false;
|
||||
}
|
||||
|
||||
// Saved modes are distinct from validated concrete gameplay values.
|
||||
static bool Randomizable(string key)
|
||||
{
|
||||
return key == "heliTimerMinutes" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent" || key == "radioDefenceSeconds" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile";
|
||||
}
|
||||
static bool ValidSelection(string key, int value)
|
||||
{
|
||||
if (value == -1) return Randomizable(key);
|
||||
if (value == -2) return key == "startTimeMinutes";
|
||||
return Valid(key, value);
|
||||
}
|
||||
static string SelectionText(int value)
|
||||
{
|
||||
if (value == -1) return "Random";
|
||||
if (value == -2) return "Random day";
|
||||
return value.ToString();
|
||||
}
|
||||
static bool RandomBounds(string key, out int minimum, out int maximum)
|
||||
{
|
||||
minimum = 0;
|
||||
if (key == "heliTimerMinutes") { minimum = 1; maximum = 1440; }
|
||||
else if (key == "convoyCount") maximum = 20;
|
||||
else if (key == "reportIntervalSeconds") { minimum = 5; maximum = 60; }
|
||||
else if (key == "supportSquadLimit") maximum = 8;
|
||||
else if (key == "responseRadiusMetres") { minimum = 500; maximum = 5000; }
|
||||
else if (key == "aiIntelChancePercent" || key == "intelRevealChancePercent") maximum = 100;
|
||||
else if (key == "radioDefenceSeconds") { minimum = 30; maximum = 1800; }
|
||||
else if (key == "startTimeMinutes") maximum = 1439;
|
||||
else if (key == "timeMultiplier") { minimum = 1; maximum = 24; }
|
||||
else if (key == "weatherProfile") { minimum = 1; maximum = 3; }
|
||||
else return false;
|
||||
return true;
|
||||
}
|
||||
// Server-only, once per setting per round. Polls and gameplay reads never reroll.
|
||||
// Date and daylight depend on the world and are resolved by their services.
|
||||
int Runtime(string key)
|
||||
{
|
||||
int value = Get(key);
|
||||
if (value != -1 || !Replication.IsServer()) return value;
|
||||
if (m_Resolved.Contains(key)) return m_Resolved.Get(key);
|
||||
int minimum, maximum;
|
||||
if (!RandomBounds(key, minimum, maximum)) return value;
|
||||
value = Math.RandomInt(minimum, maximum + 1);
|
||||
m_Resolved.Set(key, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
static int AddedDefault(string key)
|
||||
{
|
||||
if (key == "heliTimerMinutes") return 15;
|
||||
if (key == "convoyCount") return 3;
|
||||
if (key == "automaticConvoyEnabled") return 1;
|
||||
if (key == "stingerChanceOneIn") return 15;
|
||||
if (key == "startTimeMinutes") return 840;
|
||||
@@ -69,6 +121,8 @@ class ERR_Settings
|
||||
m_Values.Set("weatherProfile", AddedDefault("weatherProfile"));
|
||||
m_Values.Set("stingerChanceOneIn", AddedDefault("stingerChanceOneIn"));
|
||||
m_Values.Set("automaticConvoyEnabled", AddedDefault("automaticConvoyEnabled"));
|
||||
m_Values.Set("convoyCount", AddedDefault("convoyCount"));
|
||||
m_Values.Set("heliTimerMinutes", AddedDefault("heliTimerMinutes"));
|
||||
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
|
||||
map<string, int> loaded;
|
||||
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
|
||||
@@ -97,7 +151,7 @@ class ERR_Settings
|
||||
float number;
|
||||
if (!json.ReadValue(key, number)) return false;
|
||||
int value = Math.Round(number);
|
||||
if (number != value || !Valid(key, value)) return false;
|
||||
if (number != value || !ValidSelection(key, value)) return false;
|
||||
values.Set(key, value);
|
||||
}
|
||||
return true;
|
||||
@@ -105,7 +159,7 @@ class ERR_Settings
|
||||
|
||||
bool Set(string key, int value)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ReadOnly || !Valid(key, value)) return false;
|
||||
if (!Replication.IsServer() || m_ReadOnly || !ValidSelection(key, value)) return false;
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
foreach (string option : Keys())
|
||||
@@ -139,6 +193,7 @@ class ERR_Settings
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (Get(key) != value) m_Resolved.Remove(key);
|
||||
m_Values = checked;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -57,8 +57,21 @@ class ERR_SiteDefinition
|
||||
ResourceName Prefab;
|
||||
[Attribute("0 0 0", UIWidgets.Coords)]
|
||||
vector Position;
|
||||
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, radio or mortar)")]
|
||||
bool RandomClear;
|
||||
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
||||
float Heading;
|
||||
[Attribute("0", UIWidgets.EditBox, "Height of composition root above its surveyed ground")]
|
||||
float RootHeight;
|
||||
bool PlacementResolved;
|
||||
[Attribute("12 0 0", UIWidgets.Coords, "Extraction aircraft offset from its site")]
|
||||
vector AircraftOffset;
|
||||
[Attribute("0", UIWidgets.CheckBox, "Require a clear pad on airfield grounds before aircraft creation")]
|
||||
bool RequireAirfield;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Optional physical guard group", "et")]
|
||||
ResourceName Guards;
|
||||
[Attribute("0", UIWidgets.CheckBox, "Choose a guard group from enabled content factions; retain catalog fallback when none enabled")]
|
||||
bool AllowFactionPool;
|
||||
[Attribute()] ref array<ref ERR_SitePatrolPoint> Patrol;
|
||||
[Attribute("0", UIWidgets.CheckBox, "Populate a finite squad from the faction catalog") ]
|
||||
bool Guarded;
|
||||
@@ -81,6 +94,8 @@ class ERR_ScenarioDefinition
|
||||
ref array<ref ERR_SiteDefinition> Sites;
|
||||
[Attribute("2", UIWidgets.EditBox, "Independent accessible caches containing enough radio documents") ]
|
||||
int MinimumRadioSources;
|
||||
[Attribute("0", UIWidgets.EditBox, "Finite hill scouts per round (0-32); each has a radio, binoculars and a sniper rifle")]
|
||||
int ScoutCount;
|
||||
|
||||
ERR_SiteDefinition Find(string id)
|
||||
{
|
||||
@@ -91,6 +106,7 @@ class ERR_ScenarioDefinition
|
||||
|
||||
bool Valid()
|
||||
{
|
||||
if (ScoutCount < 0 || ScoutCount > 32) return false;
|
||||
if (!Sites || Sites.IsEmpty() || Sites.Count() > 32) return false;
|
||||
set<string> ids = new set<string>();
|
||||
int prisons, radios, extractions, groups, items, sources, maps, props, transports;
|
||||
@@ -99,6 +115,9 @@ class ERR_ScenarioDefinition
|
||||
foreach (ERR_SiteDefinition site : Sites)
|
||||
{
|
||||
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
|
||||
if (site.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) return false;
|
||||
if (site.Kind == ERR_LocationKind.RADIO && !site.Guarded && site.Guards.IsEmpty()) return false;
|
||||
if (site.RandomClear && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR) return false;
|
||||
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
|
||||
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
|
||||
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST;
|
||||
@@ -118,7 +137,7 @@ class ERR_ScenarioDefinition
|
||||
foreach (ERR_SiteProp prop : site.Props)
|
||||
if (!prop || prop.Prefab.IsEmpty() || prop.Offset.Length() > 40) return false;
|
||||
}
|
||||
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.DEPOT) return false;
|
||||
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.MORTAR) return false;
|
||||
ids.Insert(site.Id);
|
||||
if (site.Kind == ERR_LocationKind.PRISON) prisons++;
|
||||
if (site.Kind == ERR_LocationKind.RADIO) radios++;
|
||||
|
||||
@@ -7,7 +7,7 @@ class ERR_SiteDiscoveryRules
|
||||
{
|
||||
if (radius < 0 || radius > 60) return false;
|
||||
if (kind == ERR_LocationKind.RADIO) return radius <= RadioRadiusLimit();
|
||||
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST) return true;
|
||||
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.MORTAR) return true;
|
||||
return (kind == ERR_LocationKind.PRISON || kind == ERR_LocationKind.EXTRACTION) && radius == 0;
|
||||
}
|
||||
static bool InRange(float radius, float horizontalDistance, float verticalDistance)
|
||||
|
||||
@@ -13,18 +13,22 @@ class ERR_SiteRuntime
|
||||
ERR_LocationComponent Location;
|
||||
IEntity Container;
|
||||
SCR_AIGroup Group;
|
||||
string ExpectedGuardFaction;
|
||||
int InitialGuardCount;
|
||||
ref ERR_PrisonPatrol PrisonPatrol;
|
||||
int LastInitialPhysical = -1;
|
||||
int LastInitialConscious = -1;
|
||||
Vehicle Transport;
|
||||
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 = {};
|
||||
@@ -43,6 +47,11 @@ class ERR_SiteService
|
||||
|
||||
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];
|
||||
}
|
||||
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.
|
||||
@@ -100,12 +109,23 @@ class ERR_SiteService
|
||||
PrintFormat("Reintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
|
||||
return !m_Obstructed;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -247,25 +267,35 @@ class ERR_SiteService
|
||||
{
|
||||
vector position = ERR_Actors.Ground(spec.Position);
|
||||
if (position[1] < 1) return Fail("site below sea level: " + spec.Id);
|
||||
IEntity root = Spawn(spec.Prefab, position);
|
||||
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);
|
||||
if (spec.Kind == ERR_LocationKind.PRISON) m_Mode.ConfigureStart(position, spec.Heading);
|
||||
if (spec.Kind == ERR_LocationKind.EXTRACTION)
|
||||
{
|
||||
Vehicle helicopter = Vehicle.Cast(Spawn(m_Mode.Factions().ExtractionPrefab, position + "12 0 0"));
|
||||
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");
|
||||
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())
|
||||
{
|
||||
if (!CacheClearance(position + spec.ContainerOffset)) return Fail("obstructed cache position: " + spec.Id);
|
||||
site.Container = Spawn(spec.Container, position + spec.ContainerOffset);
|
||||
vector containerPosition = position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading);
|
||||
bool clearContainer;
|
||||
if (spec.Kind == ERR_LocationKind.PRISON) clearContainer = BackpackClearance(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);
|
||||
@@ -301,21 +331,39 @@ class ERR_SiteService
|
||||
{
|
||||
foreach (ERR_SiteProp prop : spec.Props)
|
||||
{
|
||||
vector propPosition = ERR_Actors.Ground(position + prop.Offset);
|
||||
if (!CacheClearance(propPosition)) return Fail("obstructed scenery position: " + spec.Id);
|
||||
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.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);
|
||||
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);
|
||||
m_Entities.Insert(scenery); site.Props.Insert(scenery);
|
||||
}
|
||||
}
|
||||
if (!CreateTransport(site, position)) return false;
|
||||
if (spec.Guarded || !spec.Guards.IsEmpty())
|
||||
{
|
||||
vector guardPosition = position + spec.GuardOffset;
|
||||
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;
|
||||
site.Group = SCR_AIGroup.Cast(Spawn(groupPrefab, guardPosition));
|
||||
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 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();
|
||||
@@ -337,11 +385,16 @@ class ERR_SiteService
|
||||
Fail("initial guard group disappeared or has no configured members: " + site.Definition.Id);
|
||||
return m_State;
|
||||
}
|
||||
if (site.Group.GetFaction() != m_Mode.EnemyFaction())
|
||||
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);
|
||||
@@ -352,7 +405,7 @@ class ERR_SiteService
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (!actor || physical.Contains(actor)) continue;
|
||||
physical.Insert(actor);
|
||||
if (SCR_FactionManager.SGetFaction(actor) != m_Mode.EnemyFaction())
|
||||
if (SCR_FactionManager.SGetFaction(actor) != site.Group.GetFaction())
|
||||
{
|
||||
Fail("initial guard actor faction disagrees with catalog: " + site.Definition.Id);
|
||||
return m_State;
|
||||
@@ -381,6 +434,15 @@ class ERR_SiteService
|
||||
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()
|
||||
|
||||
@@ -146,7 +146,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
|
||||
protected bool SpawnPosition(ERR_GameMode mode, out vector position)
|
||||
{
|
||||
if (mode.Phase() == ERR_Phase.PRISON) return m_Placement.Prison(mode.PrisonPosition(), m_AuthorizedPositions, position);
|
||||
if (mode.Phase() == ERR_Phase.PRISON) return m_Placement.Prison(mode.PrisonPosition(), m_AuthorizedPositions, position, mode.PrisonHeading());
|
||||
array<IEntity> survivors = {};
|
||||
ERR_Actors.Players(survivors, true);
|
||||
foreach (IEntity survivor : survivors)
|
||||
@@ -155,7 +155,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
if (!mode.Players() || !mode.Players().CurrentActor(survivor) || !mode.PlayerFaction() || SCR_FactionManager.SGetFaction(survivor) != mode.PlayerFaction()) continue;
|
||||
if (m_Placement.Near(survivor, m_AuthorizedPositions, position)) return true;
|
||||
}
|
||||
return m_Placement.Prison(mode.PrisonPosition(), m_AuthorizedPositions, position);
|
||||
return m_Placement.Prison(mode.PrisonPosition(), m_AuthorizedPositions, position, mode.PrisonHeading());
|
||||
}
|
||||
|
||||
ERR_SpawnTicket CreateSpawnTicket(SCR_SpawnRequestComponent request, SCR_SpawnData data)
|
||||
@@ -328,7 +328,7 @@ class ERR_SpawnLogic : SCR_SpawnLogic
|
||||
m_Attempts.Remove(playerId);
|
||||
m_Retries.Remove(playerId);
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.PlayerSpawned(playerId, entity);
|
||||
if (mode) { ERR_Actors.SetFaction(entity, mode.PlayerFaction()); mode.PlayerSpawned(playerId, entity); }
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(playerId));
|
||||
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
PrintFormat("Reintroduced [spawn]: player %1 at %2", playerId, entity.GetOrigin());
|
||||
|
||||
@@ -2,18 +2,25 @@
|
||||
class ERR_SpawnPlacement
|
||||
{
|
||||
protected IEntity m_Exclude;
|
||||
protected vector m_Candidate;
|
||||
protected bool m_Blocked;
|
||||
protected bool Obstacle(IEntity entity)
|
||||
protected bool Occupied(IEntity entity)
|
||||
{
|
||||
if (!entity || (!entity.GetVObject() && !ChimeraCharacter.Cast(entity))) return true;
|
||||
// Character controller geometry is not included in every native shape trace.
|
||||
// Narrow the broad-phase result by actual body position, not its query bounds.
|
||||
if (entity != m_Exclude && ChimeraCharacter.Cast(entity) && vector.DistanceXZ(m_Candidate, entity.GetOrigin()) < 1.2 && Math.AbsFloat(m_Candidate[1] - entity.GetOrigin()[1]) < 2) m_Blocked = true;
|
||||
return !m_Blocked;
|
||||
}
|
||||
protected bool Obstacle(IEntity entity, vector start = "0 0 0", vector direction = "0 0 0")
|
||||
{
|
||||
if (!entity) return false;
|
||||
IEntity parent = entity;
|
||||
for (int depth = 0; parent && depth < 32; depth++)
|
||||
{
|
||||
if (parent == m_Exclude) return true;
|
||||
if (parent == m_Exclude) return false;
|
||||
parent = parent.GetParent();
|
||||
}
|
||||
m_Blocked = true;
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
bool Clear(vector position, map<int, vector> reservations, int ownReservation = -1, IEntity exclude = null)
|
||||
{
|
||||
@@ -30,16 +37,24 @@ class ERR_SpawnPlacement
|
||||
vector sample = ERR_Actors.Ground(position + Vector(x * 0.45, 0, z * 0.45));
|
||||
if (Math.AbsFloat(sample[1] - ground[1]) > 0.3) return false;
|
||||
}
|
||||
m_Blocked = false;
|
||||
m_Exclude = exclude;
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(ground + "-0.45 0.15 -0.45", ground + "0.45 1.9 0.45", Obstacle);
|
||||
m_Candidate = ground; m_Blocked = false;
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(ground + "-1.2 -2 -1.2", ground + "1.2 2 1.2", Occupied);
|
||||
if (m_Blocked) { m_Exclude = null; return false; }
|
||||
// Broad entity bounds overlap clear slots when the prison is rotated.
|
||||
// Test the filled standing volume against physical geometry instead.
|
||||
TraceBox volume = new TraceBox();
|
||||
volume.Start = ground + "0 0.15 0"; volume.End = volume.Start + "0 0.01 0";
|
||||
volume.Mins = "-0.45 0 -0.45"; volume.Maxs = "0.45 1.75 0.45";
|
||||
volume.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
volume.Exclude = exclude;
|
||||
float fraction = GetGame().GetWorld().TraceMove(volume, Obstacle);
|
||||
m_Exclude = null;
|
||||
return !m_Blocked;
|
||||
return ERR_ForceRules.Finite(fraction) && fraction >= 1;
|
||||
}
|
||||
bool Prison(vector center, map<int, vector> reservations, out vector position)
|
||||
bool Prison(vector center, map<int, vector> reservations, out vector position, float heading = 0)
|
||||
{
|
||||
// Native character query bounds are about 4m wide. A 3m lattice leaves
|
||||
// clearance between that conservative bound and the 0.9m query column.
|
||||
// A 3m lattice leaves clearance around the 0.9m standing volume.
|
||||
// Search the four inner positions, then eight edge positions, then corners.
|
||||
// All sixteen centers remain at least 1.5m inside the authored +/-6m walls.
|
||||
for (int band = 0; band <= 2; band++)
|
||||
@@ -50,7 +65,7 @@ class ERR_SpawnPlacement
|
||||
if (row == 0 || row == 3) outerAxes++;
|
||||
if (column == 0 || column == 3) outerAxes++;
|
||||
if (outerAxes != band) continue;
|
||||
vector candidate = ERR_Actors.Ground(center + Vector(-4.5 + column * 3, 0, -4.5 + row * 3));
|
||||
vector candidate = ERR_Actors.Ground(center + ERR_CampTerrain.Rotate(Vector(-4.5 + column * 3, 0, -4.5 + row * 3), heading));
|
||||
if (!Clear(candidate, reservations)) continue;
|
||||
position = candidate;
|
||||
return true;
|
||||
|
||||
@@ -33,6 +33,14 @@ class ERR_TimeService
|
||||
minutes = hours * 60 + remainder;
|
||||
return true;
|
||||
}
|
||||
// Whole minutes strictly after sunrise and before sunset; exclusive end.
|
||||
static bool DaylightBounds(float sunrise, float sunset, out int first, out int end)
|
||||
{
|
||||
if (sunrise < 0 || sunset > 24 || sunrise >= sunset) return false;
|
||||
first = Math.Floor(sunrise * 60) + 1;
|
||||
end = Math.Ceil(sunset * 60);
|
||||
return first >= 0 && first < end && end <= 1440;
|
||||
}
|
||||
bool Start(ERR_GameMode mode)
|
||||
{
|
||||
if (!Replication.IsServer() || !mode || !mode.Settings()) return false;
|
||||
@@ -41,8 +49,15 @@ class ERR_TimeService
|
||||
if (!world) return false;
|
||||
m_Manager = world.GetTimeAndWeatherManager();
|
||||
if (!m_Manager) return false;
|
||||
int startMinutes = mode.Settings().Get("startTimeMinutes");
|
||||
int multiplier = mode.Settings().Get("timeMultiplier");
|
||||
int startMinutes = mode.Settings().Runtime("startTimeMinutes");
|
||||
if (startMinutes == -2)
|
||||
{
|
||||
float sunrise, sunset;
|
||||
int first, end;
|
||||
if (!m_Manager.GetSunriseHour(sunrise) || !m_Manager.GetSunsetHour(sunset) || !DaylightBounds(sunrise, sunset, first, end)) return false;
|
||||
startMinutes = Math.RandomInt(first, end);
|
||||
}
|
||||
int multiplier = mode.Settings().Runtime("timeMultiplier");
|
||||
if (!ERR_Settings.Valid("startTimeMinutes", startMinutes) || !ERR_Settings.Valid("timeMultiplier", multiplier)) return false;
|
||||
float previousDuration = m_Manager.GetDayDuration();
|
||||
bool previousAdvancement = m_Manager.GetIsDayAutoAdvanced();
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
// A unit's low-ammo radio check-in can request this airport-loaded delivery.
|
||||
// Each trip creates at most twelve compatible magazines at the airfield; those
|
||||
// exact identities must physically arrive before any native inventory transfer.
|
||||
class ERR_AirAmmoMission
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ReconFlightComponent m_Flight;
|
||||
protected ERR_HQUnit m_Unit;
|
||||
protected ref ERR_HQRoute m_Order, m_RecipientOrder;
|
||||
protected ref array<IEntity> m_Cargo = {}, m_Spent = {}, m_Members = {};
|
||||
protected ref array<ref ERR_ConvoyCargoCallback> m_Loads = {};
|
||||
protected ref ERR_InfantryAmmoTransfer m_Move;
|
||||
protected ref array<ref ERR_InfantryAmmoTransfer> m_Operations = {};
|
||||
protected ref array<IEntity> m_PreviouslySubmitted;
|
||||
protected float m_NextDiagnostic;
|
||||
bool PendingNative()
|
||||
{
|
||||
foreach (ERR_ConvoyCargoCallback loading : m_Loads) if (!loading.Finished) return true;
|
||||
foreach (ERR_InfantryAmmoTransfer operation : m_Operations) if (operation.Unresolved()) return true;
|
||||
return false;
|
||||
}
|
||||
protected AIWaypoint m_Waypoint;
|
||||
protected vector m_Point, m_Pad;
|
||||
protected int m_Message, m_Token, m_State, m_Created, m_Delivered;
|
||||
protected float m_Deadline, m_NextTick;
|
||||
protected bool m_Finished;
|
||||
bool Finished() { return m_Finished;
|
||||
}
|
||||
void ERR_AirAmmoMission(ERR_HQ hq, ERR_GameMode mode, ERR_ReconFlightComponent flight, ERR_HQUnit recipient, vector reported, int message, int trip, array<IEntity> submitted)
|
||||
{
|
||||
m_PreviouslySubmitted = submitted;
|
||||
m_HQ = hq;
|
||||
m_Mode = mode;
|
||||
m_Flight = flight;
|
||||
m_Unit = recipient;
|
||||
m_Point = reported;
|
||||
m_Message = message;
|
||||
m_Token = 200000 + trip;
|
||||
m_Deadline = GetGame().GetWorld().GetWorldTime() + 600000;
|
||||
if (!m_HQ.ReserveAirAmmo(recipient, m_Token)) { m_Finished = true;
|
||||
return;
|
||||
}
|
||||
array<AIAgent> agents = {};
|
||||
recipient.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) m_Members.Insert(agent.GetControlledEntity());
|
||||
bool clear;
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
float angle = i * Math.PI / 6;
|
||||
if (ERR_ReconAirports.FindClearPad(reported + Vector(Math.Sin(angle), 0, Math.Cos(angle)) * 80, m_Pad) && vector.DistanceXZ(reported, m_Pad) <= 200) { clear = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!clear || m_Members.IsEmpty()) { Finish("no safe landing area near delivered request");
|
||||
return;
|
||||
}
|
||||
m_Order = hq.Downlink(flight.Crew(), "AIR AMMUNITION", "load twelve magazines and fly to reported unit position " + reported.ToString(), message);
|
||||
m_RecipientOrder = hq.Downlink(recipient.Group, "AMMUNITION RENDEZVOUS", "meet supply helicopter at " + m_Pad.ToString(), message);
|
||||
if (!m_Order || !m_RecipientOrder) Finish("physical radio downlink unavailable");
|
||||
}
|
||||
protected ResourceName Magazine(IEntity actor)
|
||||
{
|
||||
if (!actor) return "";
|
||||
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
|
||||
if (!manager) return "";
|
||||
array<BaseWeaponComponent> weapons = {};
|
||||
manager.GetWeapons(weapons);
|
||||
foreach (BaseWeaponComponent weapon : weapons)
|
||||
{
|
||||
if (!weapon || (weapon.GetWeaponType() != EWeaponType.WT_RIFLE && weapon.GetWeaponType() != EWeaponType.WT_MACHINEGUN && weapon.GetWeaponType() != EWeaponType.WT_SNIPERRIFLE)) continue;
|
||||
array<BaseMuzzleComponent> muzzles = {};
|
||||
weapon.GetMuzzlesList(muzzles);
|
||||
foreach (BaseMuzzleComponent muzzle : muzzles)
|
||||
{
|
||||
BaseMagazineComponent magazine = muzzle.GetMagazine();
|
||||
if (magazine && magazine.GetOwner().GetPrefabData()) return magazine.GetOwner().GetPrefabData().GetPrefabName();
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
protected bool Owned()
|
||||
{
|
||||
return m_Unit && m_Unit.Group && m_Unit.Group.GetPlayerCount(true) == 0 && m_Unit.Group.GetFaction() == m_Mode.EnemyFaction()
|
||||
&& m_HQ.LogisticsToken(m_Unit.Group) == m_Token && m_Flight && !m_Flight.Lost() && m_Flight.Phase() != ERR_ReconPhase.RELEASED;
|
||||
}
|
||||
protected void Finish(string reason)
|
||||
{
|
||||
if (m_Finished) return;
|
||||
m_Finished = true;
|
||||
if (m_Move) m_Move.Stop();
|
||||
if (m_Waypoint) { if (m_Unit && m_Unit.Group) m_Unit.Group.RemoveWaypoint(m_Waypoint);
|
||||
ERR_Actors.DeleteSpawned(m_Waypoint);
|
||||
m_Waypoint = null;
|
||||
}
|
||||
if (m_Unit) m_HQ.ReleaseLogistics(m_Unit.Group, m_Token);
|
||||
if (m_Unit && m_Unit.Group)
|
||||
{
|
||||
array<AIAgent> releasedAgents = {};
|
||||
m_Unit.Group.GetAgents(releasedAgents);
|
||||
foreach (AIAgent releasedAgent : releasedAgents) if (releasedAgent) releasedAgent.SetPermanentLOD(-1);
|
||||
}
|
||||
if (m_Flight) m_Flight.ReturnHome();
|
||||
ERR_HQ.Event("AIR AMMUNITION", string.Format("delivered=%1; %2; remaining original cargo retained", m_Delivered, reason));
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Finished || now < m_NextTick) return;
|
||||
m_NextTick = now + 1000;
|
||||
if (!m_Mode.Active() || !Owned() || now >= m_Deadline) { Finish("mission expired or ownership lost");
|
||||
return;
|
||||
}
|
||||
if (m_HQ.AirAssault().MortarsBusy(m_Pad, now)) { Finish("mortar fire conflicts with landing corridor");
|
||||
return;
|
||||
}
|
||||
if (m_State == 0)
|
||||
{
|
||||
int aircraft = 1, recipient = 1;
|
||||
if (m_Order) aircraft = m_Order.Advance(now, m_HQ.Journal());
|
||||
if (m_RecipientOrder) recipient = m_RecipientOrder.Advance(now, m_HQ.Journal());
|
||||
if (aircraft == 1) m_Order = null;
|
||||
if (recipient == 1) m_RecipientOrder = null;
|
||||
if (aircraft < 0 || recipient < 0) { Finish("radio order interrupted");
|
||||
return;
|
||||
}
|
||||
if (aircraft == 0 || recipient == 0) return;
|
||||
m_Unit.ReleaseAssignment();
|
||||
m_State = 1;
|
||||
}
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_Flight.Aircraft().FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { Finish("aircraft cargo storage unavailable");
|
||||
return;
|
||||
}
|
||||
if (m_State == 1)
|
||||
{
|
||||
if (!m_Flight.AtAirport()) { Finish("loading requires airport");
|
||||
return;
|
||||
}
|
||||
if (m_Created < 12)
|
||||
{
|
||||
ResourceName prefab = Magazine(m_Members[m_Created % m_Members.Count()]);
|
||||
m_Created++;
|
||||
if (prefab.IsEmpty()) return;
|
||||
ERR_ConvoyCargoCallback callback = new ERR_ConvoyCargoCallback();
|
||||
m_Loads.Insert(callback);
|
||||
if (!inventory.TrySpawnPrefabToStorage(prefab, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, callback)) callback.Finished = true;
|
||||
return;
|
||||
}
|
||||
foreach (ERR_ConvoyCargoCallback operation : m_Loads) if (!operation.Finished) return;
|
||||
array<IEntity> items = {};
|
||||
ERR_Inventory.Items(inventory, items);
|
||||
foreach (IEntity item : items) if (item && item.FindComponent(BaseMagazineComponent) && !m_Cargo.Contains(item) && !m_PreviouslySubmitted.Contains(item)) m_Cargo.Insert(item);
|
||||
if (m_Cargo.IsEmpty()) { Finish("no physical cargo loaded");
|
||||
return;
|
||||
}
|
||||
m_Flight.LaunchDelivery(m_Pad);
|
||||
m_State = 2;
|
||||
ERR_HQ.Event("AIR AMMUNITION DEPARTURE", "native cargo aboard; destination from delivered unit request");
|
||||
return;
|
||||
}
|
||||
if (m_State == 2)
|
||||
{
|
||||
if (m_Flight.Phase() != ERR_ReconPhase.PARKED || vector.DistanceXZ(m_Flight.Aircraft().GetOrigin(), m_Pad) > 10) return;
|
||||
array<vector> anchors = {};
|
||||
if (!ERR_InfantryAmmoAccess.Points(m_Flight.Aircraft(), anchors)) { Finish("no authored aircraft cargo access");
|
||||
return;
|
||||
}
|
||||
vector accessPoint = anchors[0];
|
||||
vector outward = accessPoint - m_Flight.Aircraft().GetOrigin();
|
||||
outward[1] = 0;
|
||||
if (outward.Length() < 0.1) outward = "1 0 0";
|
||||
accessPoint = ERR_Actors.Ground(accessPoint + outward.Normalized() * 2);
|
||||
m_Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), accessPoint));
|
||||
if (!m_Waypoint) { Finish("rendezvous waypoint unavailable");
|
||||
return;
|
||||
}
|
||||
m_Waypoint.SetCompletionRadius(0.5);
|
||||
m_Unit.Group.AddWaypointAt(m_Waypoint, 0);
|
||||
m_State = 3;
|
||||
}
|
||||
if (m_State == 3)
|
||||
{
|
||||
if (now >= m_NextDiagnostic)
|
||||
{
|
||||
m_NextDiagnostic = now + 15000;
|
||||
float closest = 1e20;
|
||||
foreach (IEntity approaching : m_Members)
|
||||
if (ERR_Actors.CanFight(approaching)) closest = Math.Min(closest, vector.Distance(approaching.GetOrigin(), m_Flight.Aircraft().GetOrigin()));
|
||||
ERR_HQ.Event("AIR AMMUNITION RENDEZVOUS", string.Format("nearest original recipient=%1m; confirmed magazines=%2", closest, m_Delivered));
|
||||
}
|
||||
SCR_AIGroupUtilityComponent utility = m_Unit.Group.GetGroupUtilityComponent();
|
||||
if (utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty()) { Finish("recipient engaged in combat; delivery aborted"); return; }
|
||||
m_Unit.Group.ActivateAI();
|
||||
array<AIAgent> receivingAgents = {};
|
||||
m_Unit.Group.GetAgents(receivingAgents);
|
||||
foreach (AIAgent receivingAgent : receivingAgents)
|
||||
{
|
||||
receivingAgent.SetPermanentLOD(0);
|
||||
receivingAgent.ActivateAI();
|
||||
}
|
||||
}
|
||||
if (m_Move)
|
||||
{
|
||||
int result = m_Move.Tick(now);
|
||||
if (result == 0) return;
|
||||
if (result > 0) m_Delivered++;
|
||||
m_Move = null;
|
||||
if (m_Delivered >= 12) { Finish("delivery complete");
|
||||
return;
|
||||
}
|
||||
}
|
||||
foreach (IEntity member : m_Members)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != m_Unit.Group || vector.Distance(member.GetOrigin(), m_Flight.Aircraft().GetOrigin()) > 15) continue;
|
||||
foreach (IEntity magazine : m_Cargo)
|
||||
{
|
||||
if (!magazine || m_Spent.Contains(magazine) || !ERR_InfantryAmmoTransfer.Compatible(member, magazine)) continue;
|
||||
m_Move = new ERR_InfantryAmmoTransfer(m_Mode, m_Flight.Aircraft(), m_Flight.Crew(), member, m_Unit.Group, magazine);
|
||||
m_Move.Tick(now);
|
||||
if (m_Move.Submitted()) { m_Spent.Insert(magazine);
|
||||
m_PreviouslySubmitted.Insert(magazine);
|
||||
m_Operations.Insert(m_Move);
|
||||
return;
|
||||
}
|
||||
if (now >= m_NextDiagnostic)
|
||||
{
|
||||
m_NextDiagnostic = now + 15000;
|
||||
ERR_HQ.Event("AIR AMMUNITION WAIT", m_Move.Reason());
|
||||
}
|
||||
m_Move = null;
|
||||
}
|
||||
}
|
||||
if (m_Spent.Count() >= m_Cargo.Count()) Finish("all original cargo operations completed");
|
||||
}
|
||||
bool BlocksMortar(vector point) { return !m_Finished && vector.DistanceXZ(point, m_Pad) < 400;
|
||||
}
|
||||
void Stop() { Finish("mission stopped");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
class ERR_AirPassenger
|
||||
{
|
||||
IEntity Actor;
|
||||
BaseCompartmentSlot Seat;
|
||||
bool Locked = true;
|
||||
bool Jumped;
|
||||
ERR_AssaultParachute Chute;
|
||||
}
|
||||
class ERR_AirAssaultTeam
|
||||
{
|
||||
SCR_AIGroup Group;
|
||||
ref array<ref ERR_AirPassenger> Members = {};
|
||||
ref ERR_AirDropOrder Order;
|
||||
bool Released;
|
||||
void Landed() {} // Canopy completion is observed by the owning load for the entire original team.
|
||||
void Update()
|
||||
{
|
||||
if (Released || !Group || !Order || Group.GetPlayerCount(true) > 0) return;
|
||||
bool survivor;
|
||||
foreach (ERR_AirPassenger member : Members)
|
||||
{
|
||||
if (!ERR_Actors.Alive(member.Actor)) continue;
|
||||
if (!member.Jumped || CompartmentAccessComponent.GetVehicleIn(member.Actor) || member.Actor.GetOrigin()[1] - SCR_TerrainHelper.GetTerrainY(member.Actor.GetOrigin()) > 3) return;
|
||||
survivor = survivor || ERR_Actors.CanFight(member.Actor);
|
||||
}
|
||||
if (!survivor) return;
|
||||
Released = true;
|
||||
Group.ActivateAI();
|
||||
Group.SetPermanentLOD(-1);
|
||||
ERR_HQ.Get().LandAirTeam(Group, Order);
|
||||
}
|
||||
}
|
||||
class ERR_AirDropGeometry
|
||||
{
|
||||
static float Along(vector position, vector center, vector direction)
|
||||
{ vector offset = position - center;
|
||||
offset[1] = 0;
|
||||
return vector.Dot(offset, direction);
|
||||
}
|
||||
static bool Corridor(vector position, vector center, vector direction, vector velocity)
|
||||
{
|
||||
float along = Along(position, center, direction);
|
||||
vector lateral = position - center - direction * along;
|
||||
lateral[1] = 0;
|
||||
velocity[1] = 0;
|
||||
return along >= -175 && along <= 250 && lateral.Length() <= 45 && velocity.Length() >= 15 && vector.Dot(velocity.Normalized(), direction) >= 0.8;
|
||||
}
|
||||
static bool LandingLine(vector center, vector direction)
|
||||
{
|
||||
for (int step = -4; step <= 5; step++)
|
||||
{
|
||||
vector point = ERR_Actors.Ground(center + direction * (step * 50));
|
||||
if (point[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !ERR_HQRoadIntercept.Standable(point)) return false;
|
||||
TraceBox trace = new TraceBox();
|
||||
trace.Start = point + "0 3 0";
|
||||
trace.End = point + "0 190 0";
|
||||
trace.Mins = "-4 -1 -4";
|
||||
trace.Maxs = "4 1 4";
|
||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Eight original passengers, two original teams, one consumed load per aircraft.
|
||||
class ERR_AirAssaultLoad
|
||||
{
|
||||
static const ResourceName PARACHUTE = "{C31F210ABE0781A0}Prefabs/ERR/AssaultParachute.et";
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_ReconFlightComponent m_Flight;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected int m_Serial, m_NextPassenger;
|
||||
protected ref array<BaseCompartmentSlot> m_Seats = {};
|
||||
protected ref array<ref ERR_AirPassenger> m_Passengers = {};
|
||||
protected ref array<ref ERR_AirAssaultTeam> m_Teams = {};
|
||||
protected ref ERR_AirDropOrder m_Order;
|
||||
protected float m_CoverUntil;
|
||||
bool Cover(float now, out vector center) { if (!m_Order || !m_Used || m_Approaching || now >= m_CoverUntil) return false;
|
||||
center = m_Order.Target;
|
||||
return true;
|
||||
}
|
||||
bool NeedsService(float now) { return m_Used && !m_Approaching && now >= m_CoverUntil;
|
||||
}
|
||||
protected float m_LoadDeadline, m_NextBoard, m_NextJump, m_AirspaceUntil;
|
||||
protected bool m_Ready, m_Failed, m_Stopped, m_Used, m_Approaching, m_RunCommitted;
|
||||
protected vector m_Center, m_Direction, m_RunStart, m_RunEnd;
|
||||
static void SetLock(IEntity actor, bool locked)
|
||||
{
|
||||
ERR_HQ hq = ERR_HQ.Get();
|
||||
if (hq && hq.AirAssault()) hq.AirAssault().Lock(actor, locked);
|
||||
}
|
||||
void ReuseRemaining()
|
||||
{
|
||||
m_Used = false;
|
||||
m_Order = null;
|
||||
m_CoverUntil = 0;
|
||||
foreach (ERR_AirPassenger member : m_Passengers)
|
||||
if (member.Actor && CompartmentAccessComponent.GetVehicleIn(member.Actor) == m_Aircraft) { member.Jumped = false;
|
||||
member.Locked = true;
|
||||
}
|
||||
}
|
||||
int Count() { return m_Passengers.Count();
|
||||
}
|
||||
ERR_AirPassenger At(int index) { return m_Passengers[index];
|
||||
}
|
||||
bool Used() { return m_Used;
|
||||
}
|
||||
bool Preparing() { return !m_Ready && !m_Failed && !m_Stopped;
|
||||
}
|
||||
bool Ready()
|
||||
{
|
||||
return m_Ready && !m_Failed && !m_Stopped && !m_Used && m_Flight && !m_Flight.Lost()
|
||||
&& (m_Flight.Phase() == ERR_ReconPhase.SEARCH || m_Flight.Phase() == ERR_ReconPhase.SHADOW) && Aboard() > 0;
|
||||
}
|
||||
void ERR_AirAssaultLoad(ERR_GameMode mode, ERR_ReconFlightComponent flight, int serial)
|
||||
{ m_Mode = mode;
|
||||
m_Flight = flight;
|
||||
m_Aircraft = flight.Aircraft();
|
||||
m_Serial = serial;
|
||||
}
|
||||
protected void Seats(IEntity entity)
|
||||
{
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(entity.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
manager.GetCompartments(seats);
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
if (seat && seat.GetType() == ECompartmentType.CARGO && seat.IsCompartmentAccessible() && !seat.IsOccupied() && !m_Seats.Contains(seat)) m_Seats.Insert(seat);
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child) { Seats(child);
|
||||
child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
protected void Failure(string reason)
|
||||
{
|
||||
m_Failed = true;
|
||||
m_Approaching = false;
|
||||
foreach (ERR_AirPassenger member : m_Passengers) member.Locked = false;
|
||||
ERR_HQ.Event("AIR LOAD UNAVAILABLE", reason + "; original partial entities retained, reconnaissance remains available");
|
||||
}
|
||||
void Load(vector pad)
|
||||
{
|
||||
m_LoadDeadline = GetGame().GetWorld().GetWorldTime() + 30000;
|
||||
Seats(m_Aircraft);
|
||||
ResourceName soldier = m_Mode.FactionSelection().Soldier();
|
||||
Resource chute = Resource.Load(PARACHUTE);
|
||||
if (m_Seats.Count() < 8 || soldier.IsEmpty() || !chute || !chute.IsValid()) { Failure("eight usable cargo seats, soldier or canopy unavailable");
|
||||
return;
|
||||
}
|
||||
for (int t = 0; t < 2; t++)
|
||||
{
|
||||
ERR_AirAssaultTeam team = new ERR_AirAssaultTeam();
|
||||
team.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{C31F210ABE078195}Prefabs/ERR/ReconCrew.et", pad));
|
||||
m_Teams.Insert(team);
|
||||
if (!team.Group) { Failure("passenger group spawn failed");
|
||||
return;
|
||||
}
|
||||
m_Mode.Own(team.Group);
|
||||
team.Group.SetFaction(m_Mode.EnemyFaction());
|
||||
team.Group.SetPermanentLOD(0);
|
||||
}
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
ERR_AirPassenger member = new ERR_AirPassenger();
|
||||
member.Seat = m_Seats[i];
|
||||
AIGroup group = m_Teams[i % 2].Group;
|
||||
member.Actor = member.Seat.SpawnCharacterInCompartment(soldier, group, "{C31F210ABE078195}Prefabs/ERR/ReconCrew.et");
|
||||
m_Passengers.Insert(member);
|
||||
m_Teams[i % 2].Members.Insert(member);
|
||||
if (!member.Actor || group != m_Teams[i % 2].Group) { Failure("native passenger creation or group identity failed");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
protected void Boarding(float now)
|
||||
{
|
||||
foreach (ERR_AirAssaultTeam loadingTeam : m_Teams)
|
||||
{
|
||||
if (!loadingTeam.Group) continue;
|
||||
loadingTeam.Group.ActivateAI();
|
||||
array<AIAgent> loadingAgents = {};
|
||||
loadingTeam.Group.GetAgents(loadingAgents);
|
||||
foreach (AIAgent loadingAgent : loadingAgents) { loadingAgent.SetPermanentLOD(0);
|
||||
loadingAgent.ActivateAI();
|
||||
}
|
||||
}
|
||||
if (now >= m_LoadDeadline) { Failure(string.Format("native passenger boarding timed out; seated=%1/%2", Aboard(), Count()));
|
||||
return;
|
||||
}
|
||||
bool all = m_Passengers.Count() == 8;
|
||||
foreach (ERR_AirPassenger member : m_Passengers)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(member.Actor)) { all = false;
|
||||
continue;
|
||||
}
|
||||
if (CompartmentAccessComponent.GetVehicleIn(member.Actor) == m_Aircraft && member.Seat.GetOccupant() == member.Actor) continue;
|
||||
all = false;
|
||||
if (now < m_NextBoard || CompartmentAccessComponent.GetVehicleIn(member.Actor) || member.Seat.IsOccupied()) continue;
|
||||
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(member.Actor.FindComponent(SCR_CompartmentAccessComponent));
|
||||
if (access) access.MoveInVehicle(m_Aircraft, ECompartmentType.CARGO, false, member.Seat);
|
||||
}
|
||||
if (now >= m_NextBoard) m_NextBoard = now + 1000;
|
||||
if (!all) return;
|
||||
for (int t = 0; t < m_Teams.Count(); t++)
|
||||
{
|
||||
ERR_AirAssaultTeam team = m_Teams[t];
|
||||
foreach (ERR_AirPassenger original : team.Members)
|
||||
if (ERR_Actors.Group(original.Actor) != team.Group || !ERR_Actors.SetFaction(original.Actor, m_Mode.EnemyFaction())) { Failure("original passenger faction/group changed");
|
||||
return;
|
||||
}
|
||||
ERR_ForceRecord record = m_Mode.Forces().Register(string.Format("airborne-%1-team-%2", m_Serial, t), ERR_ForceKind.PATROL, m_Aircraft.GetOrigin(), 4, null, team.Group, now);
|
||||
if (!record) { Failure("finite passenger ledger refused registration");
|
||||
return;
|
||||
}
|
||||
record.Pinned = true;
|
||||
m_Mode.RegisterGroup(team.Group);
|
||||
ERR_HQ.Get().ReserveGroup(team.Group);
|
||||
team.Group.ActivateAI();
|
||||
array<AIAgent> agents = {};
|
||||
team.Group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents) { agent.SetPermanentLOD(0);
|
||||
agent.ActivateAI();
|
||||
}
|
||||
}
|
||||
m_Ready = true;
|
||||
ERR_HQ.Event("AIR LOAD READY", "eight original passengers seated in two four-person teams");
|
||||
}
|
||||
int Aboard()
|
||||
{
|
||||
if (!m_Aircraft) return 0;
|
||||
int count;
|
||||
foreach (ERR_AirPassenger member : m_Passengers)
|
||||
if (ERR_Actors.CanFight(member.Actor) && !ERR_Actors.Player(member.Actor) && CompartmentAccessComponent.GetVehicleIn(member.Actor) == m_Aircraft) count++;
|
||||
return count;
|
||||
}
|
||||
bool HoldsSeat(IEntity actor)
|
||||
{
|
||||
if (m_Stopped || m_Failed || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor)) return false;
|
||||
foreach (ERR_AirAssaultTeam team : m_Teams)
|
||||
foreach (ERR_AirPassenger member : team.Members)
|
||||
if (member.Actor == actor && member.Locked && ERR_Actors.Group(actor) == team.Group)
|
||||
return (m_Flight && m_Flight.Phase() < ERR_ReconPhase.PARKED && CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft) || (member.Chute && CompartmentAccessComponent.GetVehicleIn(actor) == member.Chute);
|
||||
return false;
|
||||
}
|
||||
void Lock(IEntity actor, bool locked)
|
||||
{ foreach (ERR_AirPassenger member : m_Passengers) if (member.Actor == actor) member.Locked = locked;
|
||||
}
|
||||
bool Begin(ERR_AirDropOrder order, float now)
|
||||
{
|
||||
if (!Ready() || !order || now >= order.ExpiresAt) return false;
|
||||
vector radial = m_Aircraft.GetOrigin() - order.Target;
|
||||
radial[1] = 0;
|
||||
float heading = Math.Atan2(radial[0], radial[2]);
|
||||
bool found;
|
||||
for (int attempt = 0; attempt < 12; attempt++)
|
||||
{
|
||||
float angle = heading + attempt * Math.PI / 6;
|
||||
radial = Vector(Math.Sin(angle), 0, Math.Cos(angle));
|
||||
m_Center = ERR_Actors.Ground(order.Target + radial * 450);
|
||||
m_Direction = Vector(-radial[2], 0, radial[0]);
|
||||
if (ERR_AirDropGeometry.LandingLine(m_Center, m_Direction)) { found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) return false;
|
||||
m_RunStart = m_Center - m_Direction * 350;
|
||||
m_RunEnd = m_Center + m_Direction * 450;
|
||||
m_CoverUntil = now + 300000;
|
||||
m_Order = order;
|
||||
m_Used = true;
|
||||
m_Approaching = true;
|
||||
m_RunCommitted = false;
|
||||
m_NextPassenger = 0;
|
||||
m_NextJump = now;
|
||||
foreach (ERR_AirAssaultTeam team : m_Teams) team.Order = order;
|
||||
m_AirspaceUntil = order.ExpiresAt + 150000;
|
||||
return true;
|
||||
}
|
||||
bool Approach(out vector point)
|
||||
{
|
||||
point = m_RunStart;
|
||||
if (m_RunCommitted) point = m_RunEnd;
|
||||
return m_Approaching && !m_Stopped;
|
||||
}
|
||||
void Abort()
|
||||
{ if (m_Approaching && m_Order) m_CoverUntil = GetGame().GetWorld().GetWorldTime() + 300000;
|
||||
m_Approaching = false;
|
||||
m_RunCommitted = false;
|
||||
}
|
||||
bool BlocksMortar(vector impact, float now)
|
||||
{
|
||||
if (!m_Order || vector.DistanceXZ(impact, m_Order.Target) >= 1200) return false;
|
||||
foreach (ERR_AirPassenger member : m_Passengers)
|
||||
if (member.Chute && member.Actor && CompartmentAccessComponent.GetVehicleIn(member.Actor) == member.Chute) return true;
|
||||
return now < m_AirspaceUntil;
|
||||
}
|
||||
void Update(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active()) return;
|
||||
foreach (ERR_AirAssaultTeam team : m_Teams) team.Update();
|
||||
if (Preparing()) { Boarding(now);
|
||||
return;
|
||||
}
|
||||
if (!m_Approaching) return;
|
||||
if (!m_Flight || m_Flight.Lost() || m_Flight.Phase() >= ERR_ReconPhase.RETURNING || !ERR_Actors.Alive(m_Aircraft) || now >= m_Order.ExpiresAt)
|
||||
{ Abort();
|
||||
ERR_HQ.Event("AIR DROP ABORTED", "authorization expired or aircraft unavailable; no new passengers created");
|
||||
return;
|
||||
}
|
||||
foreach (ERR_AirAssaultTeam originalTeam : m_Teams)
|
||||
if (!originalTeam.Group || originalTeam.Group.GetPlayerCount(true) > 0 || originalTeam.Group.GetFaction() != m_Mode.EnemyFaction()) { Abort();
|
||||
return;
|
||||
}
|
||||
vector position = m_Aircraft.GetOrigin();
|
||||
if (!m_RunCommitted && vector.DistanceXZ(position, m_RunStart) <= 80) m_RunCommitted = true;
|
||||
if (m_RunCommitted && ERR_AirDropGeometry.Along(position, m_Center, m_Direction) > 300) { Abort();
|
||||
return;
|
||||
}
|
||||
float height = position[1] - SCR_TerrainHelper.GetTerrainY(position);
|
||||
if (!m_RunCommitted || now < m_NextJump || height < 90 || height > 180 || !m_Aircraft.GetPhysics()
|
||||
|| !ERR_AirDropGeometry.Corridor(position, m_Center, m_Direction, m_Aircraft.GetPhysics().GetVelocity())) return;
|
||||
while (m_NextPassenger < m_Passengers.Count())
|
||||
{
|
||||
int index = m_NextPassenger++;
|
||||
ERR_AirPassenger passenger = m_Passengers[index];
|
||||
if (passenger.Jumped || !ERR_Actors.CanFight(passenger.Actor) || ERR_Actors.Player(passenger.Actor) || CompartmentAccessComponent.GetVehicleIn(passenger.Actor) != m_Aircraft) continue;
|
||||
vector exit = m_Aircraft.CoordToParent("12 -4 0");
|
||||
ERR_AssaultParachute chute = ERR_AssaultParachute.Cast(ERR_Actors.Spawn(PARACHUTE, exit));
|
||||
m_NextJump = now + 2000;
|
||||
if (!chute) { ERR_HQ.Event("AIR DROP FAILED", "canopy creation refused; original passenger retained");
|
||||
continue;
|
||||
}
|
||||
m_Mode.Own(chute);
|
||||
passenger.Chute = chute;
|
||||
vector landing = m_Center + m_Direction * ERR_AirDropGeometry.Along(position, m_Center, m_Direction);
|
||||
passenger.Jumped = chute.BeginJump(passenger.Actor, m_Teams[index % 2], landing);
|
||||
ERR_HQ.Event("AIR DROP JUMP", string.Format("original passenger=%1 transfer requested=%2", index, passenger.Jumped));
|
||||
break;
|
||||
}
|
||||
if (m_NextPassenger >= m_Passengers.Count()) Abort();
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
Abort();
|
||||
foreach (ERR_AirPassenger passenger : m_Passengers) passenger.Locked = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
class ERR_AirAssaultSystem
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_AirAssaultLoad> m_Loads = {};
|
||||
protected ref ERR_AirSupportPolicy m_Policy = new ERR_AirSupportPolicy();
|
||||
protected ref ERR_HQRoute m_Route;
|
||||
protected ref ERR_AirDropOrder m_Order;
|
||||
protected ERR_AirAssaultLoad m_OrderedLoad;
|
||||
protected float m_NextDecision;
|
||||
protected bool m_Stopped;
|
||||
protected string m_LoggedReason;
|
||||
void ERR_AirAssaultSystem(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq;
|
||||
m_Mode = mode;
|
||||
}
|
||||
string Status() { return "Airborne support: " + m_Policy.Reason + string.Format("; finite loads %1/4", m_Loads.Count());
|
||||
}
|
||||
ERR_AirAssaultLoad Load(ERR_ReconFlightComponent flight, vector pad, int serial)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode.Active() || m_Loads.Count() >= 4 || m_Mode.Forces().Count() > 253) return null;
|
||||
ERR_AirAssaultLoad load = new ERR_AirAssaultLoad(m_Mode, flight, m_Loads.Count() + 1);
|
||||
m_Loads.Insert(load);
|
||||
// Consume the finite load before any native spawn; never refund partial failures.
|
||||
load.Load(pad);
|
||||
return load;
|
||||
}
|
||||
bool HoldsSeat(IEntity actor)
|
||||
{
|
||||
if (m_Stopped || !m_Mode.Active()) return false;
|
||||
foreach (ERR_AirAssaultLoad load : m_Loads) if (load.HoldsSeat(actor)) return true;
|
||||
return false;
|
||||
}
|
||||
void Lock(IEntity actor, bool locked)
|
||||
{
|
||||
foreach (ERR_AirAssaultLoad load : m_Loads) load.Lock(actor, locked);
|
||||
}
|
||||
bool MortarsBusy(vector point, float now)
|
||||
{
|
||||
array<ref ERR_MortarBattery> batteries = {};
|
||||
m_HQ.Mortars().Batteries(batteries);
|
||||
foreach (ERR_MortarBattery battery : batteries)
|
||||
{
|
||||
if (now < battery.AirspaceUntil && vector.DistanceXZ(point, battery.AirspaceImpact) < battery.AirspaceRadius + 900) return true;
|
||||
ERR_HQReport report = battery.Report;
|
||||
if (battery.PendingRadioReport) report = battery.PendingRadioReport;
|
||||
if ((battery.Activity || battery.RadioOrder || battery.CommandOpen || battery.Released) && report && vector.DistanceXZ(point, report.Position) < 1200) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool BlocksMortar(vector impact, float now)
|
||||
{
|
||||
if (m_Stopped) return false;
|
||||
if (m_Order && vector.DistanceXZ(impact, m_Order.Target) < 1200) return true;
|
||||
foreach (ERR_AirAssaultLoad load : m_Loads) if (load.BlocksMortar(impact, now)) return true;
|
||||
return false;
|
||||
}
|
||||
void Maintain(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode.Active()) return;
|
||||
foreach (ERR_AirAssaultLoad load : m_Loads) load.Update(now);
|
||||
}
|
||||
protected void CancelOrder(float now, string reason)
|
||||
{
|
||||
m_Route = null;
|
||||
m_Order = null;
|
||||
m_OrderedLoad = null;
|
||||
m_Policy.Retry(now);
|
||||
m_Policy.Reason = reason;
|
||||
ERR_HQ.Event("AIR DROP WITHHELD", reason);
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode.Active() || m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return;
|
||||
if (m_Route)
|
||||
{
|
||||
if (!m_OrderedLoad || !m_OrderedLoad.Ready() || now >= m_Order.ExpiresAt) { CancelOrder(now, "order expired or original passenger load unavailable");
|
||||
return;
|
||||
}
|
||||
int delivery = m_Route.Advance(now, m_HQ.Journal());
|
||||
if (delivery < 0) { CancelOrder(now, "helicopter downlink interrupted; troops retained");
|
||||
return;
|
||||
}
|
||||
if (delivery == 0) return;
|
||||
ERR_HQBelief belief = m_HQ.Memory().GetById(m_Order.BeliefId);
|
||||
ERR_AirSupportSituation situation;
|
||||
if (belief) situation = m_HQ.AirSituation(belief, m_OrderedLoad, now);
|
||||
if (!belief || !ERR_AirSupportPolicy.Refusal(situation).IsEmpty() || vector.DistanceXZ(belief.PredictedPosition, m_Order.Target) > 150)
|
||||
{ CancelOrder(now, "memory, ground response or airspace changed during radio delivery");
|
||||
return;
|
||||
}
|
||||
if (!m_OrderedLoad.Begin(m_Order, now)) { CancelOrder(now, "no clear drop corridor; continue reconnaissance");
|
||||
return;
|
||||
}
|
||||
m_Policy.Committed(now);
|
||||
m_Policy.Reason = "HQ-authorized drop underway";
|
||||
ERR_HQ.Event("AIR DROP AUTHORIZED", string.Format("belief=%1 report=M%2 target=%3; immutable delivered order", m_Order.BeliefId, m_Order.MessageId, m_Order.Target));
|
||||
m_Route = null;
|
||||
m_Order = null;
|
||||
m_OrderedLoad = null;
|
||||
return;
|
||||
}
|
||||
if (now < m_NextDecision) return;
|
||||
m_NextDecision = now + 5000;
|
||||
ERR_ReconFlightComponent flight = m_HQ.Recon().Current();
|
||||
ERR_AirAssaultLoad load;
|
||||
if (flight) load = flight.Assault();
|
||||
ERR_HQBelief best;
|
||||
ERR_AirSupportSituation bestSituation;
|
||||
float score = -1;
|
||||
for (int index = 0; index < m_HQ.Memory().Count(); index++)
|
||||
{
|
||||
ERR_HQBelief candidate = m_HQ.Memory().At(index);
|
||||
if (!candidate || candidate.Kind != ERR_HQMemoryKind.SIGHTING) continue;
|
||||
ERR_AirSupportSituation candidateSituation = m_HQ.AirSituation(candidate, load, now);
|
||||
float value = candidate.Confidence / (1 + candidate.Radius / 500);
|
||||
if (!ERR_AirSupportPolicy.Refusal(candidateSituation).IsEmpty()) value -= 10;
|
||||
if (value <= score && best) continue;
|
||||
score = value;
|
||||
best = candidate;
|
||||
bestSituation = candidateSituation;
|
||||
}
|
||||
bool approve = m_Policy.Consider(bestSituation, now);
|
||||
if (m_Mode.Settings().Runtime("supportSquadLimit") <= 0) { approve = false;
|
||||
m_Policy.Reason = "HQ support disabled";
|
||||
}
|
||||
if (m_LoggedReason != m_Policy.Reason) { m_LoggedReason = m_Policy.Reason;
|
||||
ERR_HQ.Event("AIR DROP DECISION", Status());
|
||||
}
|
||||
if (!approve || !best || !load) return;
|
||||
ERR_HQRoute route = m_HQ.Downlink(flight.Crew(), "AIR DROP", m_Policy.Reason, best.MessageId);
|
||||
if (!route) { m_Policy.Reason = "no physical radio downlink to helicopter";
|
||||
return;
|
||||
}
|
||||
m_Route = route;
|
||||
m_OrderedLoad = load;
|
||||
m_Order = new ERR_AirDropOrder();
|
||||
m_Order.BeliefId = best.BeliefId;
|
||||
m_Order.BeliefRevision = best.BeliefRevision;
|
||||
m_Order.MessageId = best.MessageId;
|
||||
m_Order.Target = best.PredictedPosition;
|
||||
m_Order.ExpiresAt = now + 120000;
|
||||
m_Order.Report = best.Report.Snapshot();
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
m_Route = null;
|
||||
m_Order = null;
|
||||
foreach (ERR_AirAssaultLoad load : m_Loads) load.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_HQ
|
||||
{
|
||||
ERR_AirSupportSituation AirSituation(ERR_HQBelief belief, ERR_AirAssaultLoad load, float now)
|
||||
{
|
||||
ERR_AirSupportSituation result = new ERR_AirSupportSituation();
|
||||
result.AircraftReady = load && load.Ready();
|
||||
if (!belief) return result;
|
||||
result.BeliefId = belief.BeliefId;
|
||||
result.Samples = belief.SampleCount;
|
||||
result.Age = (now - belief.ObservedAt) / 1000;
|
||||
result.Confidence = belief.Confidence;
|
||||
result.Radius = belief.Radius;
|
||||
result.MotionKnown = belief.HasVelocity;
|
||||
result.Speed = belief.Speed;
|
||||
result.Infantry = belief.TravelMode == "on foot";
|
||||
if (belief.Report) result.Spotted = belief.Report.SeenEscapists;
|
||||
result.ArtilleryBusy = m_AirAssault.MortarsBusy(belief.PredictedPosition, now);
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
// HQ knows friendly positions/strength from delivered check-ins, never live locations.
|
||||
ERR_HQWatchEntry status = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!status || status.ObservedAt < 0 || now < status.ObservedAt || now - status.ObservedAt > 180000) continue;
|
||||
if (vector.DistanceXZ(status.Position, belief.PredictedPosition) < 1000) result.ConfirmedLosses += status.ConfirmedLosses;
|
||||
if (status.Closed || status.Fit <= 0 || unit.ReservedForSupport || unit.HomeGuard || unit.LogisticsReservation > 0 || m_Watch.ArmedSearchBlocked(unit.ForceSourceId, now)) continue;
|
||||
ERR_HQTask task = unit.PlannedTask();
|
||||
float distance = vector.DistanceXZ(status.Position, belief.PredictedPosition);
|
||||
if (task)
|
||||
{
|
||||
if (task.BeliefId == belief.BeliefId && now < task.ExpiresAt && distance <= 900) result.CommittedFit += status.Fit;
|
||||
}
|
||||
else if (distance <= 600) result.GroundFit += status.Fit;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void LandAirTeam(SCR_AIGroup group, ERR_AirDropOrder order)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
unit.ReservedForSupport = false;
|
||||
if (!order || !unit.Eligible(m_Mode)) return;
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), ERR_Actors.Ground(order.Target)));
|
||||
if (!waypoint) return;
|
||||
unit.Release();
|
||||
unit.Waypoint = waypoint;
|
||||
unit.Assignment = order.Report.Snapshot();
|
||||
unit.Task = new ERR_HQTask();
|
||||
unit.Task.BeliefId = order.BeliefId;
|
||||
unit.Task.BeliefRevision = order.BeliefRevision;
|
||||
unit.Task.Role = ERR_HQPursuitRole.SEARCH;
|
||||
unit.Task.Destination = order.Target;
|
||||
unit.Task.QueuedAt = GetGame().GetWorld().GetWorldTime();
|
||||
unit.Task.ExpiresAt = unit.Task.QueuedAt + 120000;
|
||||
group.AddWaypointAt(waypoint, 0);
|
||||
ERR_HQ.Event("AIR TEAM LANDED", "original team searching carried HQ order at " + order.Target.ToString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_MortarSystem
|
||||
{
|
||||
override bool Safe(ERR_MortarBattery battery, vector impact, float flight)
|
||||
{
|
||||
if (m_HQ.AirAssault().BlocksMortar(impact, GetGame().GetWorld().GetWorldTime()) || m_HQ.Airfield().BlocksMortar(impact))
|
||||
{ SafetyReason = "HQ airborne drop corridor or descending troops";
|
||||
return false;
|
||||
}
|
||||
return super.Safe(battery, impact, flight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// Owner's original search-helicopter launch/loss cadence, connected to this mission's HQ owner.
|
||||
class ERR_ReconSchedule
|
||||
{
|
||||
float Deadline = -1;
|
||||
int Minutes = 15;
|
||||
bool Started;
|
||||
bool Stopped;
|
||||
void Begin(float now) { if (!Started && !Stopped) { Started = true; Deadline = now + Minutes * 60000.0; } }
|
||||
bool Due(float now, bool active) { return Started && !Stopped && !active && Deadline >= 0 && now >= Deadline; }
|
||||
void Launched() { Deadline = -1; }
|
||||
void Lost(float now) { if (!Stopped) Deadline = now + Minutes * 60000.0; }
|
||||
void Change(int minutes, float now, bool active) { Minutes = minutes; if (Started && !Stopped && !active) Deadline = now + Minutes * 60000.0; }
|
||||
void Stop() { Stopped = true; Deadline = -1; }
|
||||
}
|
||||
class ERR_AirReconSystem
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref ERR_ReconAirports m_Airports = new ERR_ReconAirports();
|
||||
protected ref ERR_ReconSchedule m_Schedule = new ERR_ReconSchedule();
|
||||
protected ref array<ERR_ReconFlightComponent> m_Flights = {};
|
||||
protected ERR_ReconFlightComponent m_Current;
|
||||
protected int m_Serial, m_PadAttempts;
|
||||
protected float m_NextPad;
|
||||
protected string m_Reason = "countdown starts after prison escape";
|
||||
void ERR_AirReconSystem(ERR_GameMode mode) { m_Mode = mode; m_Schedule.Minutes = mode.Settings().Runtime("heliTimerMinutes"); }
|
||||
bool SupportPad(out vector point)
|
||||
{
|
||||
foreach (vector airport : m_Airports.m_Positions)
|
||||
if (ERR_ReconAirports.FindClearPad(airport, point)) return true;
|
||||
return false;
|
||||
}
|
||||
ERR_ReconFlightComponent Current() { return m_Current; }
|
||||
bool Owns(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_ReconFlightComponent flight : m_Flights) if (flight && flight.Crew() == group) return true;
|
||||
return false;
|
||||
}
|
||||
bool SetDelay(int minutes)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Schedule.Stopped || !m_Mode.Settings().Set("heliTimerMinutes", minutes)) return false;
|
||||
m_Schedule.Change(minutes, GetGame().GetWorld().GetWorldTime(), m_Current != null);
|
||||
if (!m_Current) m_PadAttempts = 0;
|
||||
ERR_HQ.Event("HELICOPTER TIMER", Status());
|
||||
return true;
|
||||
}
|
||||
void Return() { if (Replication.IsServer() && m_Current) m_Current.ReturnHome(); }
|
||||
string Status()
|
||||
{
|
||||
string state = m_Reason;
|
||||
if (m_Current) state = m_Current.Status();
|
||||
else if (m_Schedule.Started && !m_Schedule.Stopped)
|
||||
{
|
||||
float due = m_Schedule.Deadline; if (m_PadAttempts > 0) due = m_NextPad;
|
||||
state += string.Format("; next attempt in %1 seconds", Math.Ceil(Math.Max(0, due - GetGame().GetWorld().GetWorldTime()) / 1000));
|
||||
}
|
||||
return string.Format("Search helicopter: delay %1 minutes; %2", m_Schedule.Minutes, state);
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Schedule.Stopped || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
|
||||
m_Airports.Update();
|
||||
if (m_Current)
|
||||
{
|
||||
m_Current.Update(now);
|
||||
if (!m_Current.Lost()) return;
|
||||
m_Reason = m_Current.Status(); m_Current.Release(false);
|
||||
m_Current = null; m_Schedule.Lost(now); m_PadAttempts = 0;
|
||||
ERR_HQ.Event("HELICOPTER LOST", Status());
|
||||
return;
|
||||
}
|
||||
if (m_Mode.Phase() < ERR_Phase.RADIO_SEARCH) return;
|
||||
if (!m_Schedule.Started) { m_Schedule.Begin(now); ERR_HQ.Event("HELICOPTER TIMER", Status()); }
|
||||
if (m_Schedule.Due(now, false) && m_PadAttempts == 0)
|
||||
{ m_PadAttempts = 3; m_NextPad = now; m_Schedule.Lost(now); }
|
||||
if (m_PadAttempts == 0 || now < m_NextPad) return;
|
||||
m_PadAttempts--; m_NextPad = now + 30000;
|
||||
// All waves share the normal finite ledger cap. Never create an untracked crew.
|
||||
if (m_Serial >= 64 || m_Mode.Forces().Count() >= 256) { m_Reason = "round aircraft/force bookkeeping capacity reached"; m_Schedule.Stop(); return; }
|
||||
vector pad; bool found;
|
||||
if (!m_Airports.m_Positions.IsEmpty())
|
||||
{
|
||||
int first = Math.RandomInt(0, m_Airports.m_Positions.Count());
|
||||
for (int attempt = 0; attempt < m_Airports.m_Positions.Count(); attempt++)
|
||||
if (ERR_ReconAirports.FindClearPad(m_Airports.m_Positions[(first + attempt) % m_Airports.m_Positions.Count()], pad)) { found = true; break; }
|
||||
}
|
||||
if (!found) { m_Reason = "no clear airfield departure pad; bounded retry pending"; ERR_HQ.Event("HELICOPTER PAD", Status()); return; }
|
||||
m_PadAttempts = 0; m_Serial++;
|
||||
Vehicle aircraft = Vehicle.Cast(ERR_Actors.Spawn("{C31F210ABE078192}Prefabs/ERR/ReconHelicopter.et", pad, Vector(Math.RandomFloat(0, 360), 0, 0)));
|
||||
if (!aircraft) { m_Reason = "aircraft creation failed; next attempt after full delay"; return; }
|
||||
m_Mode.Own(aircraft);
|
||||
ERR_ReconFlightComponent flight = ERR_ReconFlightComponent.Cast(aircraft.FindComponent(ERR_ReconFlightComponent));
|
||||
if (!flight) { m_Reason = "flight component missing; partial aircraft retained"; return; }
|
||||
m_Current = flight; m_Flights.Insert(flight); m_Schedule.Launched();
|
||||
flight.Initialize(m_Mode, pad, m_Serial);
|
||||
ERR_HQ.Event("HELICOPTER LAUNCH", string.Format("wave=%1 airfield pad=%2", m_Serial, pad));
|
||||
}
|
||||
void Removed(ERR_ReconFlightComponent flight)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Schedule.Stopped || !flight || m_Current != flight || !m_Mode || !m_Mode.Active()) return;
|
||||
m_Current = null; m_PadAttempts = 0; m_Reason = "aircraft removed; replacement delayed";
|
||||
m_Schedule.Lost(GetGame().GetWorld().GetWorldTime());
|
||||
ERR_HQ.Event("HELICOPTER LOST", Status());
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Schedule.Stop(); m_Reason = "mission stopped";
|
||||
foreach (ERR_ReconFlightComponent flight : m_Flights) if (flight) flight.Release(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Decisions consume detached HQ memory and delivered friendly check-ins only.
|
||||
class ERR_AirSupportSituation
|
||||
{
|
||||
int BeliefId;
|
||||
int Samples;
|
||||
int Spotted;
|
||||
int GroundFit;
|
||||
int CommittedFit;
|
||||
int ConfirmedLosses;
|
||||
float Age;
|
||||
float Confidence;
|
||||
float Radius;
|
||||
float Speed;
|
||||
bool MotionKnown;
|
||||
bool Infantry;
|
||||
bool AircraftReady;
|
||||
bool ArtilleryBusy;
|
||||
}
|
||||
class ERR_AirSupportPolicy
|
||||
{
|
||||
int Candidate;
|
||||
float ConsideringSince = -1;
|
||||
float NextDrop;
|
||||
string Reason = "waiting for delivered intelligence";
|
||||
static string Refusal(ERR_AirSupportSituation s)
|
||||
{
|
||||
if (!s || !s.AircraftReady) return "no ready helicopter passenger load";
|
||||
if (!ERR_HQMemoryRules.Finite(s.Age) || !ERR_HQMemoryRules.Finite(s.Confidence) || !ERR_HQMemoryRules.Finite(s.Radius) || !ERR_HQMemoryRules.Finite(s.Speed)) return "invalid memory sample";
|
||||
if (s.BeliefId <= 0 || s.Samples < 2 || s.Age < 0 || s.Age > 90 || s.Confidence < 0.7 || s.Confidence > 1 || s.Radius < 0 || s.Radius > 160) return "memory is too uncertain; continue reconnaissance";
|
||||
if (!s.Infantry || !s.MotionKnown || s.Speed < 0 || s.Speed > 6) return "target movement does not justify an infantry drop";
|
||||
int needed = Math.Clamp(s.Spotted * 2, 4, 12);
|
||||
if (s.CommittedFit >= needed) return "ground forces already committed in sufficient strength";
|
||||
if (s.GroundFit + s.CommittedFit >= needed && s.ConfirmedLosses < 2) return "nearby ground forces can handle this contact";
|
||||
if (s.ArtilleryBusy) return "mortar order or shells occupy the approach area";
|
||||
return "";
|
||||
}
|
||||
bool Consider(ERR_AirSupportSituation s, float now)
|
||||
{
|
||||
if (!ERR_HQMemoryRules.Finite(now) || now < 0) return false;
|
||||
string refusal = Refusal(s);
|
||||
if (!refusal.IsEmpty()) { Candidate = 0;
|
||||
ConsideringSince = -1;
|
||||
Reason = refusal;
|
||||
return false;
|
||||
}
|
||||
if (now < NextDrop) { Reason = "reinforcement cooldown; retain remaining troops";
|
||||
return false;
|
||||
}
|
||||
if (Candidate != s.BeliefId || ConsideringSince < 0) { Candidate = s.BeliefId;
|
||||
ConsideringSince = now;
|
||||
}
|
||||
float delay = 45000;
|
||||
if (s.ConfirmedLosses >= 2) delay = 20000;
|
||||
if (now - ConsideringSince < delay) { Reason = "confirming persistent contact and ground-force shortfall";
|
||||
return false;
|
||||
}
|
||||
Reason = "persistent infantry contact exceeds available ground response";
|
||||
return true;
|
||||
}
|
||||
void Committed(float now) { NextDrop = now + 480000;
|
||||
Candidate = 0;
|
||||
ConsideringSince = -1;
|
||||
}
|
||||
void Retry(float now) { NextDrop = Math.Max(NextDrop, now + 15000);
|
||||
Candidate = 0;
|
||||
ConsideringSince = -1;
|
||||
}
|
||||
}
|
||||
|
||||
// One immutable authorization; receiver revalidates memory and physical safety.
|
||||
class ERR_AirDropOrder
|
||||
{
|
||||
int BeliefId;
|
||||
int BeliefRevision;
|
||||
int MessageId;
|
||||
vector Target;
|
||||
float ExpiresAt;
|
||||
ref ERR_HQReport Report;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
// Aircraft weapons are physical magazines. Only the landed airport service
|
||||
// calls Service; the covering orbit consumes normal native weapon ammunition.
|
||||
class ERR_AircraftAmmo
|
||||
{
|
||||
static void Magazines(IEntity entity, array<BaseMagazineComponent> magazines, bool gunsOnly = false)
|
||||
{
|
||||
if (!entity || ChimeraCharacter.Cast(entity))
|
||||
return;
|
||||
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(entity.FindComponent(BaseWeaponManagerComponent));
|
||||
array<BaseWeaponComponent> weapons = {};
|
||||
if (manager)
|
||||
manager.GetWeapons(weapons);
|
||||
BaseWeaponComponent mounted = BaseWeaponComponent.Cast(entity.FindComponent(BaseWeaponComponent));
|
||||
if (mounted && !weapons.Contains(mounted))
|
||||
weapons.Insert(mounted);
|
||||
foreach (BaseWeaponComponent weapon : weapons)
|
||||
{
|
||||
if (!weapon || (gunsOnly && weapon.GetWeaponType() != EWeaponType.WT_MACHINEGUN && weapon.GetWeaponType() != EWeaponType.WT_AUTOCANNON))
|
||||
continue;
|
||||
array<BaseMuzzleComponent> muzzles = {};
|
||||
weapon.GetMuzzlesList(muzzles);
|
||||
foreach (BaseMuzzleComponent muzzle : muzzles)
|
||||
{
|
||||
BaseMagazineComponent magazine = muzzle.GetMagazine();
|
||||
if (magazine && !magazines.Contains(magazine))
|
||||
magazines.Insert(magazine);
|
||||
}
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
Magazines(child, magazines, gunsOnly);
|
||||
child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
|
||||
static bool HasUsableGun(IEntity aircraft)
|
||||
{
|
||||
array<BaseMagazineComponent> magazines = {};
|
||||
Magazines(aircraft, magazines, true);
|
||||
foreach (BaseMagazineComponent magazine : magazines)
|
||||
{
|
||||
if (magazine.GetAmmoCount() > magazine.GetMaxAmmoCount() * 0.25)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Native rocket pods hold projectile entities rather than magazines.
|
||||
static int ServicePods(IEntity entity, int available)
|
||||
{
|
||||
if (!Replication.IsServer() || !entity || available <= 0 || ChimeraCharacter.Cast(entity)) return 0;
|
||||
int consumed;
|
||||
SCR_RocketEjectorMuzzleComponent pod = SCR_RocketEjectorMuzzleComponent.Cast(entity.FindComponent(SCR_RocketEjectorMuzzleComponent));
|
||||
if (pod && !pod.GetDefaultRocketPrefab().IsEmpty())
|
||||
{
|
||||
for (int barrel = 0; barrel < pod.GetBarrelsCount() && consumed < available; barrel++)
|
||||
{
|
||||
if (!pod.CanReloadBarrel(barrel)) continue;
|
||||
consumed++;
|
||||
IEntity rocket = ERR_Actors.Spawn(pod.GetDefaultRocketPrefab(), entity.GetOrigin());
|
||||
if (!rocket) continue;
|
||||
pod.ReloadBarrel(barrel, rocket);
|
||||
if (pod.GetBarrelProjectile(barrel) != rocket && !rocket.GetParent()) ERR_Actors.DeleteSpawned(rocket);
|
||||
}
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child && consumed < available)
|
||||
{
|
||||
consumed += ServicePods(child, available - consumed);
|
||||
child = child.GetSibling();
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
static int Service(IEntity aircraft, int available)
|
||||
{
|
||||
if (!Replication.IsServer() || available <= 0) return 0;
|
||||
array<BaseMagazineComponent> magazines = {};
|
||||
Magazines(aircraft, magazines);
|
||||
int used;
|
||||
foreach (BaseMagazineComponent magazine : magazines)
|
||||
{
|
||||
int moved = Math.Min(available - used, Math.Max(0, magazine.GetMaxAmmoCount() - magazine.GetAmmoCount()));
|
||||
magazine.SetAmmoCount(magazine.GetAmmoCount() + moved);
|
||||
used += moved;
|
||||
}
|
||||
return used;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
// One physical airport depot and a separate logistics airframe per round.
|
||||
class ERR_AirfieldSupport
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Store, m_FuelTruck;
|
||||
protected ERR_ReconFlightComponent m_Cargo;
|
||||
protected ref ERR_HQRoute m_ReturnOrder;
|
||||
protected ERR_ReconFlightComponent m_ReturnFlight;
|
||||
protected ref ERR_AirAmmoMission m_Mission;
|
||||
protected ref array<ref ERR_AirAmmoMission> m_RetiredMissions = {};
|
||||
protected ref array<IEntity> m_SubmittedCargo = {};
|
||||
protected float m_ServiceAt = -1, m_NextTick, m_NextAttempt, m_NextDepot;
|
||||
protected bool m_Started, m_Stopped, m_Serviced;
|
||||
protected int m_Rounds = 24000, m_Rockets = 512, m_Services, m_Trips;
|
||||
protected float m_Fuel = 12000;
|
||||
protected float m_CargoServiceAt = -1, m_NextCargo;
|
||||
protected int m_CargoAttempts;
|
||||
protected vector m_Airport;
|
||||
protected bool m_CargoSpawned, m_CargoServiced;
|
||||
protected ref map<string, int> m_Requests = new map<string, int>();
|
||||
bool BlocksMortar(vector point) { return m_Mission && m_Mission.BlocksMortar(point);
|
||||
}
|
||||
ERR_ReconFlightComponent Cargo() { return m_Cargo;
|
||||
}
|
||||
IEntity Store() { return m_Store;
|
||||
}
|
||||
string Status() { return string.Format("Airport: %1 services; gun rounds=%2 fuel=%3; ammo trips=%4 rockets=%5", m_Services, m_Rounds, Math.Round(m_Fuel), m_Trips, m_Rockets);
|
||||
}
|
||||
void ERR_AirfieldSupport(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq;
|
||||
m_Mode = mode;
|
||||
}
|
||||
protected IEntity Spawn(ResourceName prefab, vector point)
|
||||
{ IEntity entity = ERR_Actors.Spawn(prefab, ERR_Actors.Ground(point));
|
||||
if (entity) m_Mode.Own(entity);
|
||||
return entity;
|
||||
}
|
||||
protected void Start(vector airport)
|
||||
{
|
||||
m_Started = true;
|
||||
m_Airport = airport;
|
||||
m_Store = Spawn("{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et", airport + "35 0 0");
|
||||
m_FuelTruck = Spawn("{16C1F16C9B053801}Prefabs/Vehicles/Wheeled/Ural4320/Ural4320_transport.et", airport + "40 0 12");
|
||||
if (m_FuelTruck) ERR_Actors.SetFaction(m_FuelTruck, m_Mode.EnemyFaction());
|
||||
Spawn("{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et", airport + "35 0 8");
|
||||
}
|
||||
protected void SpawnCargo(float now)
|
||||
{
|
||||
if (m_CargoSpawned || m_CargoAttempts >= 3 || now < m_NextCargo || !DepotAvailable()) return;
|
||||
m_CargoAttempts++; m_NextCargo = now + 30000;
|
||||
vector pad;
|
||||
if (!ERR_ReconAirports.FindClearPad(m_Airport, pad) || vector.DistanceXZ(pad, m_Airport) < 30 || m_Mode.Forces().Count() >= 254) return;
|
||||
m_CargoSpawned = true;
|
||||
Vehicle aircraft = Vehicle.Cast(Spawn("{C31F210ABE078192}Prefabs/ERR/ReconHelicopter.et", pad));
|
||||
if (!aircraft) return;
|
||||
m_Cargo = ERR_ReconFlightComponent.Cast(aircraft.FindComponent(ERR_ReconFlightComponent));
|
||||
if (m_Cargo) m_Cargo.Initialize(m_Mode, pad, 10001, true);
|
||||
ERR_HQ.Event("AIRFIELD SUPPORT", "physical service depot and separate ammunition helicopter initialized");
|
||||
}
|
||||
bool DepotAvailable()
|
||||
{ return m_Store && m_FuelTruck && ERR_Actors.Alive(m_FuelTruck) && SCR_FactionManager.SGetFaction(m_FuelTruck) == m_Mode.EnemyFaction();
|
||||
}
|
||||
protected void Service(ERR_ReconFlightComponent flight, float now)
|
||||
{
|
||||
if (!flight.AtAirport()) { m_ServiceAt = -1;
|
||||
m_Serviced = false;
|
||||
return;
|
||||
}
|
||||
if (m_Serviced || !DepotAvailable() || flight.Lost() || vector.DistanceXZ(flight.Aircraft().GetOrigin(), m_Airport) > 500) return;
|
||||
if (m_ServiceAt < 0) { m_ServiceAt = now;
|
||||
return;
|
||||
}
|
||||
if (now - m_ServiceAt < 60000) return;
|
||||
m_Serviced = true;
|
||||
m_Services++;
|
||||
Replenish(flight);
|
||||
flight.ReloadPassengers();
|
||||
ERR_HQ.Event("AIRFIELD SERVICE", Status() + "; original airframe and crew retained");
|
||||
}
|
||||
protected void Replenish(ERR_ReconFlightComponent flight)
|
||||
{
|
||||
m_Rounds -= ERR_AircraftAmmo.Service(flight.Aircraft(), m_Rounds);
|
||||
m_Rockets -= ERR_AircraftAmmo.ServicePods(flight.Aircraft(), m_Rockets);
|
||||
FuelManagerComponent fuel = FuelManagerComponent.Cast(flight.Aircraft().FindComponent(FuelManagerComponent));
|
||||
if (fuel)
|
||||
{
|
||||
array<BaseFuelNode> nodes = {};
|
||||
fuel.GetFuelNodesList(nodes);
|
||||
foreach (BaseFuelNode node : nodes)
|
||||
{ float amount = Math.Min(m_Fuel, node.GetMaxFuel() - node.GetFuel());
|
||||
if (amount > 0) { node.SetFuel(node.GetFuel() + amount);
|
||||
m_Fuel -= amount;
|
||||
} }
|
||||
}
|
||||
}
|
||||
protected bool CargoServiced(float now)
|
||||
{
|
||||
if (!m_Cargo || !m_Cargo.AtAirport()) { m_CargoServiceAt = -1; m_CargoServiced = false; return false; }
|
||||
if (m_Trips == 0 || m_CargoServiced) return true;
|
||||
if (!DepotAvailable()) return false;
|
||||
if (m_CargoServiceAt < 0) m_CargoServiceAt = now;
|
||||
if (now - m_CargoServiceAt < 60000) return false;
|
||||
Replenish(m_Cargo); m_CargoServiced = true;
|
||||
return true;
|
||||
}
|
||||
void Maintain(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active()) return;
|
||||
if (m_Cargo) m_Cargo.Update(now);
|
||||
if (m_Mission) m_Mission.Tick(now);
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active() || now < m_NextTick) return;
|
||||
m_NextTick = now + 1000;
|
||||
ERR_ReconFlightComponent recon = m_HQ.Recon().Current();
|
||||
if (!m_Started && now >= m_NextDepot)
|
||||
{
|
||||
m_NextDepot = now + 30000;
|
||||
vector airport;
|
||||
if (m_HQ.Recon().SupportPad(airport)) Start(airport);
|
||||
}
|
||||
if (m_Started) SpawnCargo(now);
|
||||
bool cargoReady = CargoServiced(now);
|
||||
if (recon)
|
||||
{
|
||||
Service(recon, now);
|
||||
if (m_Serviced && recon.AtAirport() && (!recon.Assault() || !recon.Assault().Preparing())) recon.RestartFlight();
|
||||
if (m_ReturnOrder)
|
||||
{
|
||||
int delivered = m_ReturnOrder.Advance(now, m_HQ.Journal());
|
||||
if (delivered != 0) { if (delivered == 1 && m_ReturnFlight == recon) recon.ReturnHome();
|
||||
m_ReturnOrder = null;
|
||||
m_ReturnFlight = null;
|
||||
}
|
||||
}
|
||||
else if (recon.Assault() && recon.Assault().NeedsService(now) && (recon.Phase() == ERR_ReconPhase.SEARCH || recon.Phase() == ERR_ReconPhase.SHADOW))
|
||||
{ m_ReturnOrder = m_HQ.Downlink(recon.Crew(), "AIRCRAFT RELOAD", "cover complete; return to airport for another squad and ammunition", 0);
|
||||
m_ReturnFlight = recon;
|
||||
}
|
||||
}
|
||||
if (!cargoReady || !m_Cargo || m_Cargo.Lost() || !m_Cargo.AtAirport() || !DepotAvailable() || now < m_NextAttempt || m_Trips >= 12) return;
|
||||
if (m_Mission && (!m_Mission.Finished() || m_Mission.PendingNative())) return;
|
||||
m_NextAttempt = now + 10000;
|
||||
ERR_HQUnit recipient = m_HQ.AirAmmoRecipient(now, m_Requests);
|
||||
if (!recipient) return;
|
||||
ERR_HQWatchEntry demand = m_HQ.Watch().Find(recipient.ForceSourceId);
|
||||
if (!demand) return;
|
||||
m_Trips++;
|
||||
m_Requests.Set(demand.SourceId, demand.MessageId);
|
||||
if (m_Mission) m_RetiredMissions.Insert(m_Mission);
|
||||
m_Mission = new ERR_AirAmmoMission(m_HQ, m_Mode, m_Cargo, recipient, demand.Position, demand.MessageId, m_Trips, m_SubmittedCargo);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
m_ReturnOrder = null;
|
||||
if (m_Mission) m_Mission.Stop();
|
||||
if (m_Cargo) m_Cargo.Release(false);
|
||||
}
|
||||
}
|
||||
modded class ERR_HQ
|
||||
{
|
||||
bool ReserveAirAmmo(ERR_HQUnit unit, int token)
|
||||
{
|
||||
if (!Replication.IsServer() || !MissionActive() || !unit || token <= 0 || !unit.Eligible(m_Mode) || unit.HomeGuard || !unit.Radio || unit.PendingOrder || unit.Downlink || Fighting(unit)) return false;
|
||||
// Keep its delivered search task until the replacement order actually arrives.
|
||||
unit.LogisticsReservation = token;
|
||||
return true;
|
||||
}
|
||||
ERR_HQUnit AirAmmoRecipient(float now, map<string, int> served)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
ERR_HQWatchEntry request = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!unit.Eligible(m_Mode) || unit.ReservedForSupport || unit.HomeGuard || unit.LogisticsReservation > 0 || unit.PendingOrder || unit.Downlink || Fighting(unit)) continue;
|
||||
if (!ERR_InfantryResupplyRules.Need(request, now) || request.MessageId <= served.Get(request.SourceId)) continue;
|
||||
return unit;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
ERR_HQRoute SupportUplink(SCR_AIGroup group, string detail)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group || !unit.Radio || unit.Uplink || OriginOccupied(unit.Radio, unit)) continue;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||
if (!route) return null;
|
||||
route.MessageId = m_Journal.Message();
|
||||
route.Payload = detail;
|
||||
m_Journal.Write("SUPPORT REPORT QUEUED", detail, route.MessageId);
|
||||
return route;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
class ERR_AssaultParachuteClass : ParachuteDeployedEntityClass
|
||||
{
|
||||
}
|
||||
|
||||
// Reuse the framework's canopy, cargo compartment and movement replication.
|
||||
// AI owns no PlayerController: flight, transfer and landing run on the server.
|
||||
class ERR_AssaultParachute : ParachuteDeployedEntity
|
||||
{
|
||||
protected ref ERR_AirAssaultTeam m_ERR_Team;
|
||||
protected IEntity m_ERR_Soldier;
|
||||
protected IEntity m_ERR_DepartureVehicle;
|
||||
protected BaseCompartmentSlot m_ERR_DepartureSeat;
|
||||
protected vector m_vERR_Landing;
|
||||
protected float m_fERR_TransferDeadline;
|
||||
protected float m_fERR_NextTransfer;
|
||||
protected float m_fERR_NextExit;
|
||||
protected float m_fERR_FlightDeadline;
|
||||
protected float m_fERR_NextFlightLog;
|
||||
protected vector m_vERR_LastProgress;
|
||||
protected float m_fERR_ProgressAt;
|
||||
protected float m_fERR_EscapeUntil;
|
||||
protected int m_iERR_EscapeAttempts;
|
||||
protected vector m_vERR_Escape;
|
||||
protected bool m_bERR_Started;
|
||||
protected bool m_bERR_VerticalDescent;
|
||||
protected bool m_bERR_Transferring;
|
||||
protected bool m_bERR_DidBoard;
|
||||
protected bool m_bERR_Finished;
|
||||
protected bool m_bERR_Broken;
|
||||
protected bool m_bERR_Cleanup;
|
||||
|
||||
override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (SCR_Global.IsEditMode())
|
||||
return;
|
||||
SetFlags(EntityFlags.ACTIVE);
|
||||
// The framework's generic destruction path deletes its vehicle after
|
||||
// 100ms. AI must first confirm the native compartment is empty.
|
||||
if (m_DamageManager)
|
||||
{
|
||||
m_DamageManager.GetOnDamage().Remove(OnParachuteDamaged);
|
||||
m_DamageManager.GetOnDamage().Insert(ERR_OnDamage);
|
||||
}
|
||||
}
|
||||
|
||||
bool BeginJump(IEntity soldier, ERR_AirAssaultTeam team, vector landing)
|
||||
{
|
||||
if (!Replication.IsServer())
|
||||
return false;
|
||||
m_bERR_Started = true;
|
||||
m_vERR_LastProgress = GetOrigin();
|
||||
m_fERR_ProgressAt = GetGame().GetWorld().GetWorldTime();
|
||||
m_ERR_Soldier = soldier;
|
||||
m_ERR_Team = team;
|
||||
m_vERR_Landing = landing;
|
||||
CompartmentAccessComponent access;
|
||||
if (soldier)
|
||||
access = CompartmentAccessComponent.Cast(soldier.FindComponent(CompartmentAccessComponent));
|
||||
// Physics handles are local-only; reacquire them instead of retaining
|
||||
// the framework's cached pointer across engine callbacks.
|
||||
Physics physics = GetPhysics();
|
||||
if (!access || !m_Compartment || m_Compartment.IsOccupied() || !physics || !physics.IsDynamic())
|
||||
{
|
||||
m_bERR_Finished = true;
|
||||
Print("Reintroduced [air assault]: invalid canopy seat/physics; passenger retained", LogLevel.WARNING);
|
||||
return false;
|
||||
}
|
||||
physics.ChangeSimulationState(SimulationState.SIMULATION);
|
||||
physics.SetActive(ActiveState.ALWAYS_ACTIVE);
|
||||
m_ERR_DepartureVehicle = CompartmentAccessComponent.GetVehicleIn(soldier);
|
||||
m_ERR_DepartureSeat = access.GetCompartment();
|
||||
m_fERR_TransferDeadline = GetGame().GetWorld().GetWorldTime() + 2000;
|
||||
m_fERR_FlightDeadline = m_fERR_TransferDeadline + 120000;
|
||||
m_bERR_Transferring = true;
|
||||
ERR_AirAssaultLoad.SetLock(soldier, false);
|
||||
vector exitTransform[4];
|
||||
GetWorldTransform(exitTransform);
|
||||
if (!access.GetOutVehicle_NoDoor(exitTransform, false, true, true))
|
||||
{
|
||||
m_bERR_Transferring = false;
|
||||
m_bERR_Finished = true;
|
||||
ERR_AirAssaultLoad.SetLock(soldier, true);
|
||||
Print("Reintroduced [air assault]: helicopter exit rejected; passenger retained", LogLevel.WARNING);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void ERR_OnDamage(BaseDamageContext damageContext)
|
||||
{
|
||||
if (Replication.IsServer() && m_DamageManager && m_DamageManager.IsDestroyed())
|
||||
DestroyParachute();
|
||||
}
|
||||
|
||||
override void DestroyParachute()
|
||||
{
|
||||
if (!Replication.IsServer())
|
||||
return;
|
||||
m_bERR_Broken = true;
|
||||
// Leave the damaged canopy/occupant to physics until native ejection
|
||||
// succeeds. Never delete an occupied replicated hierarchy.
|
||||
AskServerExit();
|
||||
}
|
||||
|
||||
protected void ERR_Transfer(float now)
|
||||
{
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERR_Soldier.FindComponent(CompartmentAccessComponent));
|
||||
if (m_Compartment.GetOccupant() == m_ERR_Soldier)
|
||||
{
|
||||
m_bERR_Transferring = false;
|
||||
m_bERR_DidBoard = true;
|
||||
m_ERR_DepartureVehicle = null;
|
||||
m_ERR_DepartureSeat = null;
|
||||
ERR_AirAssaultLoad.SetLock(m_ERR_Soldier, true);
|
||||
ERR_HQ.Event("PARACHUTE BOARDED", string.Format("canopy=%1 soldier=%2 position=%3", this, m_ERR_Soldier, GetOrigin()));
|
||||
return;
|
||||
}
|
||||
if (now >= m_fERR_TransferDeadline || !access || !ERR_Actors.CanFight(m_ERR_Soldier))
|
||||
{
|
||||
// If the native transfer fails, try to return the same soldier to
|
||||
// their original seat. Do not spawn a replacement or delete them.
|
||||
if (access && !access.IsInCompartment() && m_ERR_DepartureVehicle && m_ERR_DepartureSeat && !m_ERR_DepartureSeat.IsOccupied())
|
||||
access.GetInVehicle(m_ERR_DepartureVehicle, m_ERR_DepartureSeat, true, 0, ECloseDoorAfterActions.INVALID, true);
|
||||
m_bERR_Transferring = false;
|
||||
m_bERR_Finished = true;
|
||||
Print("Reintroduced [air assault]: canopy transfer failed; attempted return to original seat", LogLevel.WARNING);
|
||||
return;
|
||||
}
|
||||
if (now >= m_fERR_NextTransfer && !access.IsInCompartment())
|
||||
{
|
||||
m_fERR_NextTransfer = now + 100;
|
||||
access.GetInVehicle(this, m_Compartment, true, 0, ECloseDoorAfterActions.INVALID, true);
|
||||
}
|
||||
}
|
||||
|
||||
override void AskServerExit()
|
||||
{
|
||||
if (!Replication.IsServer() || m_bERR_Transferring || !m_Compartment || !m_Compartment.IsOccupied())
|
||||
return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now < m_fERR_NextExit)
|
||||
return;
|
||||
vector position = GetOrigin();
|
||||
float ground = SCR_TerrainHelper.GetTerrainY(position);
|
||||
if (!m_bERR_Broken && position[1] - ground > 2)
|
||||
return;
|
||||
m_fERR_NextExit = now + 1000;
|
||||
IEntity occupant = m_Compartment.GetOccupant();
|
||||
// A GM/player takeover must never be controlled by this AI controller.
|
||||
if (occupant != m_ERR_Soldier || ERR_Actors.Player(occupant))
|
||||
return;
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(occupant.FindComponent(CompartmentAccessComponent));
|
||||
if (!access)
|
||||
return;
|
||||
vector exitTransform[4];
|
||||
GetWorldTransform(exitTransform);
|
||||
if (!m_bERR_Broken)
|
||||
exitTransform[3] = Vector(position[0], ground + 0.2, position[2]);
|
||||
ERR_AirAssaultLoad.SetLock(occupant, false);
|
||||
access.GetOutVehicle_NoDoor(exitTransform, m_bERR_Broken, true, true);
|
||||
}
|
||||
|
||||
protected void ERR_Finish()
|
||||
{
|
||||
if (m_Compartment && m_Compartment.IsOccupied())
|
||||
return;
|
||||
if (m_bERR_Cleanup)
|
||||
return;
|
||||
m_bERR_Finished = true;
|
||||
ERR_HQ.Event("PARACHUTE FINISHED", string.Format("canopy=%1 soldier=%2 position=%3 boarded=%4 damaged=%5", this, m_ERR_Soldier, GetOrigin(), m_bERR_DidBoard, m_bERR_Broken));
|
||||
if (m_ERR_Soldier && CompartmentAccessComponent.GetVehicleIn(m_ERR_Soldier) == null)
|
||||
{
|
||||
ERR_AirAssaultLoad.SetLock(m_ERR_Soldier, false);
|
||||
AIControlComponent control = AIControlComponent.Cast(m_ERR_Soldier.FindComponent(AIControlComponent));
|
||||
AIAgent agent; if (control) agent = control.GetControlAIAgent();
|
||||
if (agent)
|
||||
{
|
||||
agent.SetPermanentLOD(-1);
|
||||
agent.ActivateAI();
|
||||
}
|
||||
vector position = GetOrigin();
|
||||
if (m_bERR_DidBoard && !m_bERR_Broken && position[1] - SCR_TerrainHelper.GetTerrainY(position) < 3 && m_ERR_Team && ERR_Actors.CanFight(m_ERR_Soldier))
|
||||
m_ERR_Team.Landed();
|
||||
}
|
||||
SCR_GarbageSystem garbage = SCR_GarbageSystem.GetByEntityWorld(this);
|
||||
if (garbage)
|
||||
m_bERR_Cleanup = garbage.Insert(this);
|
||||
if (m_bERR_Cleanup)
|
||||
{
|
||||
Physics physics = GetPhysics();
|
||||
if (physics)
|
||||
physics.SetActive(ActiveState.ACTIVE);
|
||||
m_ERR_Soldier = null;
|
||||
m_ERR_Team = null;
|
||||
ClearEventMask(EntityEvent.FRAME | EntityEvent.SIMULATE);
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer, landing and replication must run even if physics was asleep.
|
||||
// Do not call the parent's frame handler: it installs human input controls.
|
||||
override void EOnFrame(IEntity owner, float timeSlice)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_bERR_Started || !m_Compartment)
|
||||
return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (m_bERR_Transferring && !m_ERR_Soldier)
|
||||
{
|
||||
m_bERR_Transferring = false;
|
||||
m_bERR_Finished = true;
|
||||
}
|
||||
if (m_bERR_Transferring && m_ERR_Soldier)
|
||||
ERR_Transfer(now);
|
||||
// A native get-in completion can arrive after the timeout tick.
|
||||
// Resume descent for any late occupant instead of freezing them aloft.
|
||||
if (m_bERR_Finished && m_Compartment.IsOccupied())
|
||||
{
|
||||
m_bERR_Finished = false;
|
||||
m_bERR_DidBoard = true;
|
||||
}
|
||||
if (m_bERR_Finished || (!m_bERR_Transferring && !m_Compartment.IsOccupied()))
|
||||
{
|
||||
ERR_Finish();
|
||||
return;
|
||||
}
|
||||
if (m_Compartment.IsOccupied() && (m_Compartment.GetOccupant() != m_ERR_Soldier || ERR_Actors.Player(m_Compartment.GetOccupant())))
|
||||
return;
|
||||
Physics physics = GetPhysics();
|
||||
if (!physics)
|
||||
return;
|
||||
if (physics.GetSimulationState() != SimulationState.SIMULATION)
|
||||
physics.ChangeSimulationState(SimulationState.SIMULATION);
|
||||
physics.SetActive(ActiveState.ALWAYS_ACTIVE);
|
||||
if (m_fERR_FlightDeadline > 0 && now >= m_fERR_FlightDeadline)
|
||||
{
|
||||
// A guidance timeout is not canopy damage. Keep the soldier seated
|
||||
// and descend vertically instead of cutting them loose at altitude.
|
||||
m_fERR_FlightDeadline = 0;
|
||||
m_bERR_VerticalDescent = true;
|
||||
Print("Reintroduced [air assault]: canopy guidance expired; continuing vertical descent", LogLevel.WARNING);
|
||||
}
|
||||
vector progressPosition = GetOrigin();
|
||||
if (m_bERR_Transferring || vector.Distance(progressPosition, m_vERR_LastProgress) > 2)
|
||||
{
|
||||
m_vERR_LastProgress = progressPosition;
|
||||
m_fERR_ProgressAt = now;
|
||||
}
|
||||
else if (!m_bERR_Broken && now - m_fERR_ProgressAt > 5000 && progressPosition[1] - SCR_TerrainHelper.GetTerrainY(progressPosition) > 2)
|
||||
{
|
||||
// A roof/tree collision must not eject a soldier at terrain altitude.
|
||||
// Try a short lateral escape using collision-aware physics, retaining
|
||||
// the occupied canopy. No teleport, forced damage, or deletion.
|
||||
int escapeSide = m_iERR_EscapeAttempts++ % 4;
|
||||
float angle = escapeSide * Math.PI / 2;
|
||||
m_vERR_Escape = Vector(Math.Cos(angle) * 3, 0, Math.Sin(angle) * 3);
|
||||
m_fERR_EscapeUntil = now + 5000;
|
||||
m_fERR_ProgressAt = now;
|
||||
ERR_HQ.Event("PARACHUTE OBSTRUCTED", string.Format("canopy=%1 soldier=%2 position=%3 lateral attempt=%4", this, m_ERR_Soldier, progressPosition, m_iERR_EscapeAttempts));
|
||||
}
|
||||
AskServerExit();
|
||||
if (now >= m_fERR_NextFlightLog)
|
||||
{
|
||||
m_fERR_NextFlightLog = now + 10000;
|
||||
vector position = GetOrigin();
|
||||
ERR_HQ.Event("PARACHUTE STATUS", string.Format("canopy=%1 soldier=%2 position=%3 height=%4 velocity=%5 occupied=%6 transferring=%7 damaged=%8", this, m_ERR_Soldier, position, Math.Round(position[1] - SCR_TerrainHelper.GetTerrainY(position)), physics.GetVelocity(), m_Compartment.IsOccupied(), m_bERR_Transferring, m_bERR_Broken));
|
||||
}
|
||||
// Preserve the framework's proxy movement protocol, at five updates
|
||||
// per second for at most eight canopies per aircraft.
|
||||
m_NetAccTime += timeSlice;
|
||||
if (m_NetAccTime >= 0.2)
|
||||
{
|
||||
m_NetAccTime = 0;
|
||||
vector transform[4];
|
||||
GetWorldTransform(transform);
|
||||
Rpc(RpcDo_SyncMovement, transform, physics.GetVelocity(), physics.GetAngularVelocity(), 0.0, 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
override void EOnSimulate(IEntity owner, float timeSlice)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_bERR_Started || m_bERR_Finished || m_bERR_Broken || !m_Compartment)
|
||||
return;
|
||||
if (m_Compartment.IsOccupied() && (m_Compartment.GetOccupant() != m_ERR_Soldier || ERR_Actors.Player(m_Compartment.GetOccupant())))
|
||||
return;
|
||||
if (!m_bERR_Transferring && !m_Compartment.IsOccupied())
|
||||
return;
|
||||
Physics physics = GetPhysics();
|
||||
if (!physics)
|
||||
return;
|
||||
vector position = GetOrigin();
|
||||
vector toward = m_vERR_Landing - position;
|
||||
toward[1] = 0;
|
||||
float height = position[1] - SCR_TerrainHelper.GetTerrainY(position);
|
||||
vector velocity;
|
||||
if (!m_bERR_VerticalDescent && toward.LengthSq() > 0.01)
|
||||
velocity = toward.Normalized() * Math.Min(7, toward.Length() * 0.4);
|
||||
if (GetGame().GetWorld().GetWorldTime() < m_fERR_EscapeUntil) velocity = m_vERR_Escape;
|
||||
velocity[1] = -5;
|
||||
if (height < 10)
|
||||
velocity[1] = -2;
|
||||
if (m_bERR_Transferring)
|
||||
velocity = vector.Zero;
|
||||
// Let physics move the entire occupied hierarchy without resetting its
|
||||
// transform between fixed steps.
|
||||
physics.SetAngularVelocity(vector.Zero);
|
||||
physics.SetVelocity(velocity);
|
||||
}
|
||||
|
||||
void ~ERR_AssaultParachute()
|
||||
{
|
||||
if (m_DamageManager)
|
||||
m_DamageManager.GetOnDamage().Remove(ERR_OnDamage);
|
||||
}
|
||||
|
||||
override void EOnDeactivate(IEntity owner)
|
||||
{
|
||||
if (m_DamageManager)
|
||||
m_DamageManager.GetOnDamage().Remove(ERR_OnDamage);
|
||||
super.EOnDeactivate(owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
class ERR_BodyRecoveryJob
|
||||
{
|
||||
IEntity Body;
|
||||
vector Position;
|
||||
SCR_AIGroup Team;
|
||||
AIWaypoint Waypoint;
|
||||
ref ERR_PrisonPatrol Radio = new ERR_PrisonPatrol();
|
||||
ref array<string> Missing = {};
|
||||
ref ERR_HQRoute Order, Report;
|
||||
float Started, SearchAt = -1, RetryAt;
|
||||
int State;
|
||||
bool Delivered;
|
||||
float NextDiagnostic;
|
||||
// 0 native team, 1 radio order, 2 walk/search, 3 evidence uplink, 4 done
|
||||
}
|
||||
class ERR_BodyRecovery
|
||||
{
|
||||
protected ERR_HQ m_HQ;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref array<ref ERR_BodyRecoveryJob> m_Jobs = {};
|
||||
protected ref set<IEntity> m_ReportedBodies = new set<IEntity>();
|
||||
protected int m_Waves;
|
||||
protected float m_NextTick;
|
||||
protected bool m_Stopped;
|
||||
void ERR_BodyRecovery(ERR_HQ hq, ERR_GameMode mode) { m_HQ = hq;
|
||||
m_Mode = mode;
|
||||
}
|
||||
string Status() { return string.Format("Recovery: %1/%2 reports searched; teams dispatched=%3/8", Completed(), m_Jobs.Count(), m_Waves);
|
||||
}
|
||||
protected int Completed()
|
||||
{
|
||||
int count;
|
||||
foreach (ERR_BodyRecoveryJob job : m_Jobs) { if (job.Delivered) count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
void ReportBody(ERR_HQReport report)
|
||||
{
|
||||
if (m_Stopped || !report || !report.Target || m_ReportedBodies.Contains(report.Target) || m_Jobs.Count() >= 128 || !m_Mode.Evidence().Find(report.Target)) return;
|
||||
ERR_BodyRecoveryJob job = new ERR_BodyRecoveryJob();
|
||||
job.Body = report.Target;
|
||||
job.Position = report.Position;
|
||||
m_ReportedBodies.Insert(report.Target);
|
||||
m_Jobs.Insert(job);
|
||||
ERR_HQ.Event("BODY RECOVERY REQUEST", "delivered body sighting; search ordered at reported position " + job.Position.ToString());
|
||||
}
|
||||
protected bool SpawnTeam(ERR_BodyRecoveryJob job, float now)
|
||||
{
|
||||
foreach (ERR_BodyRecoveryJob previous : m_Jobs)
|
||||
{
|
||||
if (previous == job || previous.State != 4 || !previous.Team || previous.Team.GetPlayerCount(true) > 0 || previous.Team.GetFaction() != m_Mode.EnemyFaction() || m_Mode.ForceLiving(previous.Team) == 0) continue;
|
||||
job.Team = previous.Team;
|
||||
previous.Team = null;
|
||||
job.State = 1;
|
||||
job.Started = now;
|
||||
m_HQ.ReserveGroup(job.Team);
|
||||
return true;
|
||||
}
|
||||
if (m_Waves >= 8 || m_Mode.Forces().Count() >= 254) return false;
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
ERR_LocationComponent base;
|
||||
float best = 1e20;
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
if (!location || (location.Kind() != ERR_LocationKind.RADIO && location.Kind() != ERR_LocationKind.DEPOT)) continue;
|
||||
float distance = vector.DistanceXZ(location.Position(), job.Position);
|
||||
if (distance < best) { base = location;
|
||||
best = distance;
|
||||
}
|
||||
}
|
||||
if (!base) return false;
|
||||
vector point = ERR_Actors.Ground(base.Position() + "20 0 20");
|
||||
if (!ERR_HQRoadIntercept.Standable(point)) return false;
|
||||
// Only reject visible pop-in; these live player positions never choose a search destination.
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
foreach (IEntity player : players) if (vector.DistanceXZ(player.GetOrigin(), point) < 300) return false;
|
||||
m_Waves++;
|
||||
job.Team = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", point));
|
||||
job.Started = now;
|
||||
if (!job.Team) { job.State = 4;
|
||||
return false;
|
||||
}
|
||||
m_Mode.Own(job.Team);
|
||||
if (!ERR_PrisonPatrol.Configure(job.Team, m_Mode.FactionSelection().Choice("enemy").Group, m_Mode.EnemyFaction())) { job.State = 4;
|
||||
return false;
|
||||
}
|
||||
ERR_HQ.Event("BODY RECOVERY TEAM", "special search team departing friendly site; bodies remain retained");
|
||||
return true;
|
||||
}
|
||||
protected void RemoveWaypoint(ERR_BodyRecoveryJob job)
|
||||
{
|
||||
if (job.Waypoint) { if (job.Team) job.Team.RemoveWaypoint(job.Waypoint);
|
||||
ERR_Actors.DeleteSpawned(job.Waypoint);
|
||||
job.Waypoint = null;
|
||||
}
|
||||
}
|
||||
protected void Finish(ERR_BodyRecoveryJob job, bool delivered)
|
||||
{
|
||||
RemoveWaypoint(job);
|
||||
job.State = 4;
|
||||
job.Delivered = delivered;
|
||||
job.Order = null;
|
||||
job.Report = null;
|
||||
if (delivered) { m_HQ.CompromisedSites(job.Missing, job.Position);
|
||||
m_Mode.Evidence().Delivered(job.Body);
|
||||
}
|
||||
if (!delivered) m_HQ.ReleaseRecovery(job.Team);
|
||||
}
|
||||
void Tick(float now)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active() || now < m_NextTick) return;
|
||||
m_NextTick = now + 1000;
|
||||
m_HQ.SecureKnownSites(now);
|
||||
int active;
|
||||
foreach (ERR_BodyRecoveryJob existing : m_Jobs) if (existing.Team && existing.State < 4) active++;
|
||||
foreach (ERR_BodyRecoveryJob job : m_Jobs)
|
||||
{
|
||||
if (job.State == 4 || now < job.RetryAt) continue;
|
||||
if (!job.Body) { Finish(job, false);
|
||||
continue;
|
||||
}
|
||||
if (!job.Team)
|
||||
{
|
||||
if (active >= 2) continue;
|
||||
job.RetryAt = now + 30000;
|
||||
if (SpawnTeam(job, now)) active++;
|
||||
continue;
|
||||
}
|
||||
if (job.Team.GetPlayerCount(true) > 0 || job.Team.GetFaction() != m_Mode.EnemyFaction() || now - job.Started > 1800000) { Finish(job, false);
|
||||
continue;
|
||||
}
|
||||
job.Team.ActivateAI();
|
||||
array<AIAgent> activeAgents = {};
|
||||
job.Team.GetAgents(activeAgents);
|
||||
foreach (AIAgent activeAgent : activeAgents)
|
||||
{
|
||||
if (!activeAgent) continue;
|
||||
activeAgent.SetPermanentLOD(0);
|
||||
activeAgent.ActivateAI();
|
||||
}
|
||||
if (job.State > 0 && m_Mode.ForceLiving(job.Team) == 0) { Finish(job, false); continue; }
|
||||
if (job.State == 0)
|
||||
{
|
||||
if (now - job.Started > 60000) { Finish(job, false);
|
||||
continue;
|
||||
}
|
||||
if (!job.Team.IsExpandComplete() || job.Team.GetAgentsCount() == 0) continue;
|
||||
ERR_PrepareResult radio = job.Radio.Radio(m_Mode, job.Team, job.Started + 60000);
|
||||
if (radio != ERR_PrepareResult.READY) continue;
|
||||
m_Mode.RegisterGroup(job.Team);
|
||||
ERR_ForceRecord recoveryRecord = m_Mode.Forces().FindGroup(job.Team);
|
||||
if (recoveryRecord) ERR_HQ.Event("SPAWN PURPOSE", "force=" + recoveryRecord.SourceId + "; purpose=body inspection; position=" + job.Team.GetOrigin().ToString() + "; reported body=" + job.Position.ToString());
|
||||
m_HQ.ReserveGroup(job.Team);
|
||||
job.Team.ActivateAI();
|
||||
job.State = 1;
|
||||
}
|
||||
if (job.State == 1)
|
||||
{
|
||||
if (!job.Order) { job.Order = m_HQ.Downlink(job.Team, "BODY SEARCH", "inspect reported military body " + job.Position.ToString(), 0);
|
||||
if (!job.Order) continue;
|
||||
}
|
||||
int order = job.Order.Advance(now, m_HQ.Journal());
|
||||
if (order < 0) { job.Order = null;
|
||||
job.RetryAt = now + 15000;
|
||||
continue;
|
||||
}
|
||||
if (order == 0) continue;
|
||||
job.Order = null;
|
||||
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), job.Position));
|
||||
if (!job.Waypoint) { Finish(job, false);
|
||||
continue;
|
||||
}
|
||||
job.Waypoint.SetCompletionRadius(1.5);
|
||||
job.Team.AddWaypointAt(job.Waypoint, 0);
|
||||
job.State = 2;
|
||||
}
|
||||
if (job.State == 2)
|
||||
{
|
||||
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
|
||||
if (utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty()) { job.SearchAt = -1;
|
||||
continue;
|
||||
}
|
||||
array<AIAgent> agents = {};
|
||||
job.Team.GetAgents(agents);
|
||||
IEntity searcher;
|
||||
foreach (AIAgent agent : agents)
|
||||
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity()) && !ERR_Actors.Player(agent.GetControlledEntity()) && vector.Distance(agent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()) <= 3) searcher = agent.GetControlledEntity();
|
||||
if (now >= job.NextDiagnostic)
|
||||
{
|
||||
job.NextDiagnostic = now + 30000;
|
||||
float nearest = 1e20;
|
||||
foreach (AIAgent nearbyAgent : agents)
|
||||
if (nearbyAgent && nearbyAgent.GetControlledEntity()) nearest = Math.Min(nearest, vector.Distance(nearbyAgent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
|
||||
ERR_HQ.Event("BODY SEARCH PROGRESS", "nearest searcher=" + nearest.ToString() + "m; physical inspection required");
|
||||
}
|
||||
if (!searcher) { job.SearchAt = -1;
|
||||
continue;
|
||||
}
|
||||
if (job.SearchAt < 0) job.SearchAt = now;
|
||||
if (now - job.SearchAt < 10000) continue;
|
||||
if (!m_Mode.Evidence().Inspect(job.Body, searcher, job.Missing)) continue;
|
||||
RemoveWaypoint(job);
|
||||
job.State = 3;
|
||||
ERR_HQ.Event("BODY SEARCH COMPLETE", "local inspection complete; missing document report awaits radio");
|
||||
}
|
||||
if (job.State == 3)
|
||||
{
|
||||
if (!job.Report) { job.Report = m_HQ.SupportUplink(job.Team, "searched body; missing documents=" + job.Missing.Count().ToString());
|
||||
if (!job.Report) continue;
|
||||
}
|
||||
int receipt = job.Report.Advance(now, m_HQ.Journal());
|
||||
if (receipt < 0) { job.Report = null;
|
||||
job.RetryAt = now + 15000;
|
||||
}
|
||||
else if (receipt > 0) Finish(job, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
void Stop() { m_Stopped = true;
|
||||
foreach (ERR_BodyRecoveryJob job : m_Jobs) { RemoveWaypoint(job);
|
||||
job.Order = null;
|
||||
job.Report = null;
|
||||
} }
|
||||
}
|
||||
modded class ERR_HQ
|
||||
{
|
||||
protected ref map<string, float> m_Compromised = new map<string, float>();
|
||||
protected float m_NextSecurity;
|
||||
void SecureKnownSites(float now)
|
||||
{
|
||||
if (now < m_NextSecurity) return;
|
||||
m_NextSecurity = now + 30000;
|
||||
array<string> sites = {};
|
||||
foreach (string id, float until : m_Compromised) if (now < until) sites.Insert(id);
|
||||
TrySecureSites(sites);
|
||||
}
|
||||
void CompromisedSites(array<string> sites, vector bodyPosition)
|
||||
{
|
||||
foreach (string id : sites)
|
||||
{
|
||||
if (m_Compromised.Count() >= 64 && !m_Compromised.Contains(id)) continue;
|
||||
m_Compromised.Set(id, GetGame().GetWorld().GetWorldTime() + 1800000);
|
||||
}
|
||||
m_NextSecurity = 0;
|
||||
SecureKnownSites(GetGame().GetWorld().GetWorldTime());
|
||||
}
|
||||
void ReleaseRecovery(SCR_AIGroup group)
|
||||
{ foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) unit.ReservedForSupport = false;
|
||||
}
|
||||
protected void TrySecureSites(array<string> sites)
|
||||
{
|
||||
// A missing issued document means the site MAY be compromised. Never read
|
||||
// player discoveries and never turn this hypothesis into an artillery target.
|
||||
foreach (string id : sites)
|
||||
{
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
ERR_LocationComponent target;
|
||||
foreach (ERR_LocationComponent location : locations) if (location.Id() == id) target = location;
|
||||
if (!target) continue;
|
||||
bool secured;
|
||||
foreach (ERR_HQUnit assigned : m_Units)
|
||||
{
|
||||
ERR_HQTask assignedTask = assigned.PlannedTask();
|
||||
if (assignedTask && assignedTask.SecuritySite == id && GetGame().GetWorld().GetWorldTime() < assignedTask.ExpiresAt) secured = true;
|
||||
}
|
||||
if (secured) continue;
|
||||
ERR_HQ.Event("INTEL COMPROMISE", "missing issued document concerns " + id + "; defensive reinforcement requested");
|
||||
ERR_HQUnit best;
|
||||
float distance = 1e20;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
ERR_HQWatchEntry status = m_Watch.Find(unit.ForceSourceId);
|
||||
if (!unit.Eligible(m_Mode) || unit.HomeGuard || unit.Assignment || unit.PendingOrder || unit.Downlink || Fighting(unit) || !ERR_InfantryResupplyRules.Fresh(status, GetGame().GetWorld().GetWorldTime())) continue;
|
||||
float candidate = vector.DistanceXZ(status.Position, target.Position());
|
||||
if (candidate < distance && candidate < 2500) { best = unit;
|
||||
distance = candidate;
|
||||
}
|
||||
}
|
||||
if (!best) continue;
|
||||
ERR_HQRoute route = Downlink(best.Group, "SITE SECURITY", "secure potentially compromised site " + id, 0);
|
||||
if (!route) continue;
|
||||
ERR_HQTask task = new ERR_HQTask();
|
||||
task.SecuritySite = id;
|
||||
task.Role = ERR_HQPursuitRole.SEARCH;
|
||||
task.Destination = target.Position();
|
||||
task.QueuedAt = GetGame().GetWorld().GetWorldTime();
|
||||
task.ExpiresAt = task.QueuedAt + 600000;
|
||||
ERR_HQReport lineage = new ERR_HQReport();
|
||||
lineage.Position = target.Position();
|
||||
lineage.ObservedAt = task.QueuedAt;
|
||||
lineage.Distress = true;
|
||||
best.PendingTask = task;
|
||||
best.PendingOrder = lineage;
|
||||
best.Downlink = route;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,14 @@ class ERR_ConvoyPool
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected int m_Limit;
|
||||
protected bool m_Supply;
|
||||
protected int m_BodyLimit = 60;
|
||||
protected int m_GroupLimit = 128;
|
||||
protected ref array<ref ERR_ConvoyReceipt> m_Receipts = {};
|
||||
void ERR_ConvoyPool(ERR_GameMode mode, int maxConvoys)
|
||||
void ERR_ConvoyPool(ERR_GameMode mode, int maxConvoys, bool supply = false)
|
||||
{
|
||||
m_Mode = mode; m_Limit = Math.Clamp(maxConvoys, 0, 8);
|
||||
m_Mode = mode; m_Supply = supply; m_Limit = Math.Clamp(maxConvoys, 0, 20);
|
||||
if (supply) { m_BodyLimit = 60 + 25 * m_Limit; m_GroupLimit = 256; }
|
||||
}
|
||||
static int CrewCount(int vehicle)
|
||||
{
|
||||
@@ -26,6 +30,22 @@ class ERR_ConvoyPool
|
||||
if (vehicle == 0 || vehicle == 3 || vehicle == 5) return 2;
|
||||
return 0;
|
||||
}
|
||||
int PlannedCount(int index)
|
||||
{
|
||||
if (m_Supply && index == 2) return 1;
|
||||
if (m_Supply && (index == 6 || index == 7)) return 7;
|
||||
return CrewCount(index);
|
||||
}
|
||||
int PlannedBodies()
|
||||
{
|
||||
if (m_Supply) return 25;
|
||||
return 19;
|
||||
}
|
||||
int PlannedGroups()
|
||||
{
|
||||
if (m_Supply) return 8;
|
||||
return 6;
|
||||
}
|
||||
protected bool Current()
|
||||
{
|
||||
return Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Mode.Forces();
|
||||
@@ -57,25 +77,26 @@ class ERR_ConvoyPool
|
||||
{
|
||||
if (!Current() || sourceId.IsEmpty() || sourceId.Length() > 64) return null;
|
||||
foreach (ERR_ConvoyReceipt known : m_Receipts) if (known.SourceId == sourceId) return known;
|
||||
if (m_Receipts.Count() >= m_Limit || CommittedBodies() + 19 > 60 || CommittedGroups() + 6 > 128) return null;
|
||||
if (m_Receipts.Count() >= m_Limit || CommittedBodies() + PlannedBodies() > m_BodyLimit || CommittedGroups() + PlannedGroups() > m_GroupLimit) return null;
|
||||
ERR_ConvoyReceipt receipt = new ERR_ConvoyReceipt(); receipt.Id = m_Receipts.Count() + 1; receipt.SourceId = sourceId;
|
||||
if (m_Supply) { receipt.OutstandingBodies = 25; receipt.OutstandingGroups = 8; receipt.Records.Insert(-1); receipt.Records.Insert(-1); }
|
||||
m_Receipts.Insert(receipt);
|
||||
return receipt;
|
||||
}
|
||||
bool Consume(ERR_ConvoyReceipt receipt)
|
||||
{
|
||||
if (!Current() || !Owns(receipt) || receipt.Consumed || CommittedBodies() > 60 || CommittedGroups() > 128) return false;
|
||||
if (!Current() || !Owns(receipt) || receipt.Consumed || CommittedBodies() > m_BodyLimit || CommittedGroups() > m_GroupLimit) return false;
|
||||
receipt.Consumed = true;
|
||||
return true;
|
||||
}
|
||||
bool BindRecord(ERR_ConvoyReceipt receipt, int vehicle, ERR_ForceRecord record)
|
||||
{
|
||||
if (!Current() || !Owns(receipt) || !receipt.Consumed || vehicle < 0 || vehicle >= 6 || !record) return false;
|
||||
if (!Current() || !Owns(receipt) || !receipt.Consumed || vehicle < 0 || vehicle >= PlannedGroups() || !record) return false;
|
||||
if (receipt.Records[vehicle] >= 0) return receipt.Records[vehicle] == record.Id;
|
||||
if (m_Mode.Forces().Find(record.Id) != record || record.Kind != ERR_ForceKind.CONVOY || !record.Group || !record.Physical || record.Eliminated) return false;
|
||||
if (record.SourceId != receipt.SourceId + "-vehicle-" + vehicle.ToString() || record.InitialStrength != CrewCount(vehicle) || record.Strength != record.InitialStrength) return false;
|
||||
if (record.SourceId != receipt.SourceId + "-vehicle-" + vehicle.ToString() || record.InitialStrength != PlannedCount(vehicle) || record.Strength != record.InitialStrength) return false;
|
||||
receipt.Records[vehicle] = record.Id;
|
||||
receipt.OutstandingBodies -= CrewCount(vehicle); receipt.OutstandingGroups--;
|
||||
receipt.OutstandingBodies -= PlannedCount(vehicle); receipt.OutstandingGroups--;
|
||||
return true;
|
||||
}
|
||||
// There is intentionally no refund/reset method. A consumed startup attempt
|
||||
|
||||
@@ -8,6 +8,7 @@ class ERR_ConvoyCombatOrder
|
||||
string SourceId;
|
||||
SCR_AIGroup Group;
|
||||
bool Hold;
|
||||
vector Position;
|
||||
float ObservedAt = -1;
|
||||
float IssuedAt = -1;
|
||||
float ExpiresAt = -1;
|
||||
|
||||
@@ -11,7 +11,11 @@ class ERR_ConvoyResources
|
||||
return "";
|
||||
}
|
||||
static ResourceName GroupPrefab() { return "{7A968AB297475FA7}Prefabs/ERR/ConvoyCrew.et"; }
|
||||
static ResourceName Soldier() { return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; }
|
||||
static ResourceName Soldier()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.FactionSelection() && mode.FactionSelection().Ready() && mode.FactionSelection().Active("enemy") != "ussr") return mode.FactionSelection().Soldier();
|
||||
return "{DCB41B3746FDD1BE}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Rifleman.et"; }
|
||||
static ResourceName Rifle() { return "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et"; }
|
||||
static ResourceName Magazine() { return "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et"; }
|
||||
static bool Available()
|
||||
@@ -225,6 +229,18 @@ class ERR_ConvoyService
|
||||
{
|
||||
return !m_Stopped && Replication.IsServer() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Pool;
|
||||
}
|
||||
protected ERR_ConvoyTask NewTask() { return new ERR_ConvoyTask(); }
|
||||
protected ERR_ConvoyVehicle NewVehicle() { return new ERR_ConvoyVehicle(); }
|
||||
ERR_ConvoyTask At(int index)
|
||||
{
|
||||
if (index < 0 || index >= m_Tasks.Count()) return null;
|
||||
return m_Tasks[index];
|
||||
}
|
||||
ERR_ConvoyTask Find(string source)
|
||||
{
|
||||
foreach (ERR_ConvoyTask task : m_Tasks) if (task.Receipt && task.Receipt.SourceId == source) return task;
|
||||
return null;
|
||||
}
|
||||
protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||
{
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
@@ -259,14 +275,13 @@ class ERR_ConvoyService
|
||||
ERR_ConvoyTask Dispatch(ERR_ConvoyReceipt receipt, array<vector> positions, array<vector> angles, ERR_ForceRoute route, float now)
|
||||
{
|
||||
if (!Current() || !ERR_ForceRules.Finite(now) || now < 0 || !ERR_ConvoyRules.Plan(positions, angles, route) || !ERR_ConvoyResources.Available() || !ERR_ConvoyRules.GroundClear(positions, angles)) return null;
|
||||
// The selected stock crew and group are USSR. Other factions require a new
|
||||
// reviewed catalog; never silently rewrite affiliations on incompatible kit.
|
||||
if (!m_Mode.EnemyFaction() || m_Mode.EnemyFaction().GetFactionKey() != "USSR" || !m_Pool.Consume(receipt)) return null;
|
||||
ERR_ConvoyTask task = new ERR_ConvoyTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
|
||||
// The server role selection owns the memberless group affiliation.
|
||||
if (!m_Mode.EnemyFaction() || !m_Pool.Consume(receipt)) return null;
|
||||
ERR_ConvoyTask task = NewTask(); task.Receipt = receipt; task.Route = route; task.Deadline = now + 30000;
|
||||
m_Tasks.Insert(task);
|
||||
for (int index = 0; index < 6; index++)
|
||||
{
|
||||
ERR_ConvoyVehicle unit = new ERR_ConvoyVehicle(); unit.Index = index; task.Vehicles.Insert(unit);
|
||||
ERR_ConvoyVehicle unit = NewVehicle(); unit.Index = index; task.Vehicles.Insert(unit);
|
||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), positions[index]));
|
||||
if (unit.Group) m_Mode.Own(unit.Group);
|
||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(index), positions[index], angles[index]));
|
||||
@@ -278,7 +293,7 @@ class ERR_ConvoyService
|
||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||
if (movement) movement.SetCruiseSpeed(35);
|
||||
}
|
||||
ERR_HQ.Event("CONVOY CREATED", "six physical vehicles and 19 finite crew; native readiness and ledger admission pending");
|
||||
ERR_HQ.Event("CONVOY CREATED", "six physical vehicles; finite crew created; native readiness and ledger admission pending");
|
||||
return task;
|
||||
}
|
||||
protected string ReadinessReason(ERR_ConvoyVehicle unit)
|
||||
|
||||
@@ -7,12 +7,11 @@ class ERR_EnemyEquipmentRules
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return false;
|
||||
if (!group || !actor || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group) return false;
|
||||
SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent();
|
||||
return utility && utility.IsMilitary() && group.GetPlayerCount(true) == 0 && group.GetFaction() == mode.EnemyFaction() && !ERR_EnemyGunnerSkill.CapturedGroup(mode, group);
|
||||
return utility && utility.IsMilitary() && group.GetPlayerCount(true) == 0 && mode.IsMissionEnemy(group.GetFaction()) && !ERR_EnemyGunnerSkill.CapturedGroup(mode, group);
|
||||
}
|
||||
static EAISkill Floor(EAISkill skill)
|
||||
{
|
||||
if (skill < EAISkill.EXPERT) return EAISkill.EXPERT;
|
||||
return skill;
|
||||
return EAISkill.CYLON;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +157,7 @@ class ERR_EnemyEquipment
|
||||
void ERR_EnemyEquipment(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void RegisterGroup(SCR_AIGroup group, int radioId = 0, bool fitRadios = true)
|
||||
{
|
||||
if (m_Stopped || !Replication.IsServer() || !group || m_Groups.Count() >= 128) return;
|
||||
if (m_Stopped || !Replication.IsServer() || !group || m_Groups.Count() >= 256) return;
|
||||
foreach (ERR_EnemyEquipmentGroup existing : m_Groups) if (existing.Group == group) return;
|
||||
ERR_EnemyEquipmentGroup state = new ERR_EnemyEquipmentGroup(); state.Group = group;
|
||||
state.ReadyAt = GetGame().GetWorld().GetWorldTime() + 2000;
|
||||
|
||||
@@ -2,12 +2,24 @@
|
||||
// Owned extension and inspected vanilla API provenance: docs/ai-equipment-migration.md.
|
||||
class ERR_EnemyGunnerSkill
|
||||
{
|
||||
static ERR_ConvoyTask TaskForGroup(ERR_HQ hq, SCR_AIGroup group)
|
||||
{
|
||||
if (!hq || !group || !hq.Convoys()) return null;
|
||||
for (int i = 0; i < hq.Convoys().Count(); i++)
|
||||
{
|
||||
ERR_ConvoyTask task = hq.Convoys().At(i);
|
||||
if (!task || task.Vehicles.Count() > 6) continue;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
if (vehicle && vehicle.Group == group) return task;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
static bool CapturedGroup(ERR_GameMode mode, SCR_AIGroup group)
|
||||
{
|
||||
if (!mode || !group) return false;
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (!hq) return false;
|
||||
ERR_ConvoyTask task = hq.ConvoyTask();
|
||||
ERR_ConvoyTask task = TaskForGroup(hq, group);
|
||||
if (!task || task.Vehicles.Count() > 6) return false;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
if (unit && unit.Group == group && ERR_ConvoyRules.Captured(unit.Entity, group)) return true;
|
||||
@@ -18,7 +30,7 @@ class ERR_EnemyGunnerSkill
|
||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) return false;
|
||||
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||
if (!hq) return false;
|
||||
ERR_ConvoyTask task = hq.ConvoyTask();
|
||||
ERR_ConvoyTask task = TaskForGroup(hq, group);
|
||||
if (!task || !task.Ready || task.Stopped || !task.Receipt || !task.Receipt.Consumed || task.Vehicles.Count() > 6 || !mode.Forces()) return false;
|
||||
foreach (ERR_ConvoyVehicle unit : task.Vehicles)
|
||||
{
|
||||
|
||||
@@ -114,6 +114,7 @@ class ERR_EnemyOptics
|
||||
int draw = Math.RandomInt(0, remaining.Count());
|
||||
int index = remaining[draw]; remaining.Remove(draw);
|
||||
ResourceName prefab = candidates[index];
|
||||
if (!mode.Content() || !mode.Content().Allows(prefab)) continue;
|
||||
for (int slotIndex = 0; slotIndex < storage.GetSlotsCount(); slotIndex++)
|
||||
{
|
||||
InventoryStorageSlot slot = storage.GetSlot(slotIndex);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Mission-owned military AI use maximum decision skill, independently of aim error.
|
||||
modded class SCR_AICombatComponent
|
||||
{
|
||||
bool ERR_EnhancedEnemy()
|
||||
{
|
||||
return m_ERRSkillMode && ERR_EnemyEquipmentRules.Eligible(m_ERRSkillMode, m_ERRSkillGroup, GetOwner());
|
||||
}
|
||||
override void ERR_RefreshEnemySkill()
|
||||
{
|
||||
super.ERR_RefreshEnemySkill();
|
||||
if (ERR_EnhancedEnemy() && m_Perception && m_Utility && m_Utility.m_ThreatSystem)
|
||||
UpdatePerceptionFactor(m_Perception, m_Utility.m_ThreatSystem);
|
||||
}
|
||||
override void UpdatePerceptionFactor(PerceptionComponent perceptionComp, SCR_AIThreatSystem threatSystem)
|
||||
{
|
||||
if (!ERR_EnhancedEnemy()) { super.UpdatePerceptionFactor(perceptionComp, threatSystem); return; }
|
||||
// Use native alert recognition even while safe/suppressed. Native visibility,
|
||||
// vegetation, darkness, viewing direction and binocular equipment still apply.
|
||||
if (perceptionComp) perceptionComp.SetPerceptionFactor(PERCEPTION_FACTOR_ALERTED * m_fEquipmentPerceptionFactor * m_fPerceptionFactor);
|
||||
}
|
||||
override void ERR_ReleaseEnemySkill(ERR_GameMode mode)
|
||||
{
|
||||
super.ERR_ReleaseEnemySkill(mode);
|
||||
if (m_Perception && m_Utility && m_Utility.m_ThreatSystem)
|
||||
UpdatePerceptionFactor(m_Perception, m_Utility.m_ThreatSystem);
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_AIGetAimErrorOffset
|
||||
{
|
||||
override float GetRandomFactor(EAISkill skill, float mu)
|
||||
{
|
||||
// CYLON normally eliminates aim error. Keep the ordinary REGULAR distribution
|
||||
// for these soldiers while leaving skill-dependent decisions at maximum.
|
||||
if (m_CombatComponent && m_CombatComponent.ERR_EnhancedEnemy()) skill = EAISkill.REGULAR;
|
||||
return super.GetRandomFactor(skill, mu);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ class ERR_HQUnit
|
||||
int Id;
|
||||
SCR_AIGroup Group;
|
||||
SCR_AIGroupUtilityComponent Utility;
|
||||
SCR_AIGroupPerception Detection;
|
||||
float NextDetectionLog;
|
||||
IEntity Radio;
|
||||
float NextReport;
|
||||
ref ERR_HQRoute Uplink;
|
||||
@@ -88,6 +90,20 @@ class ERR_HQ : ERR_CommandService
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref ERR_MortarSystem m_Mortars;
|
||||
protected ref ERR_EnemyEquipment m_Equipment;
|
||||
protected ref ERR_HunterService m_Hunters;
|
||||
protected ref ERR_ObjectiveDefence m_ObjectiveDefence;
|
||||
protected ref array<SCR_AICombatComponent> m_SpecialSkills = {};
|
||||
ERR_HunterService Hunters() { return m_Hunters; }
|
||||
protected ref ERR_ScoutSystem m_Scouts;
|
||||
protected ref ERR_AirfieldSupport m_Airfield;
|
||||
ERR_AirfieldSupport Airfield() { return m_Airfield; }
|
||||
protected ref ERR_BodyRecovery m_Recovery;
|
||||
ERR_BodyRecovery Recovery() { return m_Recovery; }
|
||||
protected ref ERR_AirAssaultSystem m_AirAssault;
|
||||
ERR_AirAssaultSystem AirAssault() { return m_AirAssault; }
|
||||
protected ref ERR_AirReconSystem m_Recon;
|
||||
ERR_AirReconSystem Recon() { return m_Recon; }
|
||||
ERR_ScoutSystem Scouts() { return m_Scouts; }
|
||||
protected ref ERR_ConvoyPool m_ConvoyPool;
|
||||
protected ref ERR_ConvoyService m_Convoys;
|
||||
protected ref ERR_ConvoyTask m_ConvoyTask;
|
||||
@@ -100,6 +116,7 @@ class ERR_HQ : ERR_CommandService
|
||||
protected ref ERR_MortarLogistics m_Logistics;
|
||||
protected ref ERR_InfantryResupply m_Resupply;
|
||||
protected ref ERR_ConvoyStartup m_ConvoyStartup;
|
||||
protected ref ERR_SupplyConvoySystem m_SupplyConvoys;
|
||||
protected ref array<ref ERR_HQUnit> m_Units = {};
|
||||
protected ref array<ref ERR_HQReport> m_Contacts = {};
|
||||
protected ref ERR_HQMemory m_Memory = new ERR_HQMemory();
|
||||
@@ -113,7 +130,42 @@ class ERR_HQ : ERR_CommandService
|
||||
protected float m_NextNetwork;
|
||||
protected float m_NextAssignment;
|
||||
|
||||
void ERR_HQ(ERR_GameMode mode) { m_Mode = mode; m_Equipment = new ERR_EnemyEquipment(mode); m_ConvoyPool = new ERR_ConvoyPool(mode, 1); m_Convoys = new ERR_ConvoyService(mode, m_ConvoyPool); m_Mortars = new ERR_MortarSystem(this); m_Logistics = new ERR_MortarLogistics(this, mode); m_Resupply = new ERR_InfantryResupply(this, mode); m_ConvoyStartup = new ERR_ConvoyStartup(mode.Settings().Get("automaticConvoyEnabled") == 1); }
|
||||
void ERR_HQ(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode;
|
||||
m_Equipment = new ERR_EnemyEquipment(mode);
|
||||
m_Hunters = new ERR_HunterService(mode, null);
|
||||
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
|
||||
m_Scouts = new ERR_ScoutSystem(mode);
|
||||
m_Recon = new ERR_AirReconSystem(mode);
|
||||
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
||||
m_Airfield = new ERR_AirfieldSupport(this, mode); m_Recovery = new ERR_BodyRecovery(this, mode);
|
||||
bool legacyFixture;
|
||||
#ifdef WORKBENCH
|
||||
// These pre-existing diagnostic scenarios specifically exercise the retained
|
||||
// finite-route controller. Ordinary Workbench sessions use the full fleet.
|
||||
string fixtureEnabled, fixtureOutcome;
|
||||
legacyFixture = System.GetCLIParam("errSmoke", fixtureEnabled) && fixtureEnabled == "1"
|
||||
&& System.GetCLIParam("errSmokeOutcome", fixtureOutcome) && (fixtureOutcome == "convoy" || fixtureOutcome == "hq-memory");
|
||||
#endif
|
||||
if (legacyFixture)
|
||||
{
|
||||
m_ConvoyPool = new ERR_ConvoyPool(mode, 1);
|
||||
m_Convoys = new ERR_ConvoyService(mode, m_ConvoyPool);
|
||||
}
|
||||
else
|
||||
{
|
||||
int convoyCount = mode.Settings().Runtime("convoyCount");
|
||||
m_ConvoyPool = new ERR_ConvoyPool(mode, convoyCount, true);
|
||||
ERR_SupplyConvoyService supply = new ERR_SupplyConvoyService(mode, m_ConvoyPool);
|
||||
m_Convoys = supply;
|
||||
m_SupplyConvoys = new ERR_SupplyConvoySystem(mode, m_ConvoyPool, supply, convoyCount, mode.Settings().Get("automaticConvoyEnabled") == 1);
|
||||
}
|
||||
m_Mortars = new ERR_MortarSystem(this);
|
||||
m_Logistics = new ERR_MortarLogistics(this, mode);
|
||||
m_Resupply = new ERR_InfantryResupply(this, mode);
|
||||
m_ConvoyStartup = new ERR_ConvoyStartup(mode.Settings().Get("automaticConvoyEnabled") == 1);
|
||||
}
|
||||
static ERR_HQ Get()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
@@ -134,6 +186,7 @@ class ERR_HQ : ERR_CommandService
|
||||
ERR_ConvoyPool ConvoyPool() { return m_ConvoyPool; }
|
||||
ERR_ConvoyService Convoys() { return m_Convoys; }
|
||||
ERR_ConvoyTask ConvoyTask() { return m_ConvoyTask; }
|
||||
ERR_SupplyConvoySystem SupplyConvoys() { return m_SupplyConvoys; }
|
||||
bool PrepareConvoy(vector seed)
|
||||
{
|
||||
if (!Replication.IsServer() || !MissionActive() || m_ConvoyTask) return false;
|
||||
@@ -188,6 +241,12 @@ class ERR_HQ : ERR_CommandService
|
||||
protected void AutomaticConvoy(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Mode || m_Mode != ERR_GameMode.Get() || !m_Mode.Active()) return;
|
||||
if (m_SupplyConvoys)
|
||||
{
|
||||
m_SupplyConvoys.Tick(now, ConvoyAdmissionReady());
|
||||
if (!m_ConvoyTask && m_Convoys.Count() > 0) m_ConvoyTask = m_Convoys.At(0);
|
||||
return;
|
||||
}
|
||||
if (!m_ConvoyStartup || m_ConvoyStartup.Finished()) return;
|
||||
string previous = m_ConvoyStartup.Status();
|
||||
if (m_ConvoyStartup.Decide(now, m_ConvoyTask != null, ConvoyAdmissionReady()))
|
||||
@@ -197,6 +256,13 @@ class ERR_HQ : ERR_CommandService
|
||||
}
|
||||
bool StartScenarioConvoy()
|
||||
{
|
||||
if (m_SupplyConvoys)
|
||||
{
|
||||
if (!ConvoyAdmissionReady()) return false;
|
||||
bool started = m_SupplyConvoys.StartOne();
|
||||
if (!m_ConvoyTask && m_Convoys.Count() > 0) m_ConvoyTask = m_Convoys.At(0);
|
||||
return started;
|
||||
}
|
||||
if (!m_Mode || !m_Mode.Sites()) return false;
|
||||
ERR_SiteRuntime site = m_Mode.Sites().Find("everon-supply-1");
|
||||
if (!site || !site.Definition) return false;
|
||||
@@ -205,7 +271,8 @@ class ERR_HQ : ERR_CommandService
|
||||
string ConvoyStatus()
|
||||
{
|
||||
string detail = m_Convoys.Status();
|
||||
if (m_ConvoyStartup) detail += "; automatic startup: " + m_ConvoyStartup.Status();
|
||||
if (m_SupplyConvoys) detail += "; " + m_SupplyConvoys.Status();
|
||||
else if (m_ConvoyStartup) detail += "; automatic startup: " + m_ConvoyStartup.Status();
|
||||
if (!m_ConvoyFailure.IsEmpty()) detail += "; " + m_ConvoyFailure;
|
||||
return detail;
|
||||
}
|
||||
@@ -219,9 +286,45 @@ class ERR_HQ : ERR_CommandService
|
||||
if (unit.Group != group) continue;
|
||||
ReleaseUnit(unit, GetGame().GetWorld().GetWorldTime());
|
||||
unit.ReservedForSupport = true;
|
||||
m_Journal.Write("ROLE RESERVED", string.Format("group=%1 force=%2 home=%3; excluded from general HQ retasking", unit.Id, unit.ForceSourceId, unit.HomeId));
|
||||
}
|
||||
}
|
||||
void Dedicate(SCR_AIGroup group, string siteId)
|
||||
{
|
||||
ReserveGroup(group);
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == group) { unit.HomeId = siteId; unit.HomeGuard = true; }
|
||||
}
|
||||
protected void Detected(SCR_AIGroup group, SCR_AITargetInfo target)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Mode.Active() || m_Mode.Phase() <= ERR_Phase.PRISON || !group || !target || !target.m_Entity) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
unit.NextReport = 0;
|
||||
// Native combat reacts locally now; remote orders still require radio delivery.
|
||||
group.ActivateAI();
|
||||
if (now >= unit.NextDetectionLog)
|
||||
{
|
||||
unit.NextDetectionLog = now + 10000;
|
||||
m_Journal.Write("CONTACT LOCAL", string.Format("group=%1 force=%2 observed=%3 home=%4 dedicated=%5; native combat active, radio report due", unit.Id, unit.ForceSourceId, target.m_vWorldPos, unit.HomeId, unit.ReservedForSupport));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Temporary reservations never alter the permanent support reservation.
|
||||
void ReleaseMortarCrew(SCR_AIGroup group)
|
||||
{
|
||||
ERR_MortarBattery battery = m_Mortars.FindGroup(group);
|
||||
if (!battery) return;
|
||||
if (battery.Gun)
|
||||
{
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(battery.Gun.FindComponent(DamageManagerComponent));
|
||||
if (!damage || !damage.IsDestroyed()) return;
|
||||
}
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
if (unit.Group == group) unit.ReservedForSupport = false;
|
||||
}
|
||||
int LogisticsToken(SCR_AIGroup group)
|
||||
{
|
||||
foreach (ERR_HQUnit unit : m_Units) if (unit.Group == group) return unit.LogisticsReservation;
|
||||
@@ -316,7 +419,7 @@ class ERR_HQ : ERR_CommandService
|
||||
ERR_HQJournal Journal() { return m_Journal; }
|
||||
string Status()
|
||||
{
|
||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + string.Format("; %1 remembered tracks, %2 swept cells", m_Memory.Count(), m_Coverage.Count()) + "; " + m_Mode.Forces().Status() + "; " + m_Mortars.Status() + "; " + m_Logistics.Status() + "; " + m_Resupply.Status();
|
||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + string.Format("; %1 remembered tracks, %2 swept cells; aggression=%3/30 identified bodies=%4; ", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies()) + m_Hunters.Status() + "; " + m_Mode.Forces().Status() + "; " + m_Mortars.Status() + "; " + m_Logistics.Status() + "; " + m_Resupply.Status() + "; " + m_Recon.Status();
|
||||
}
|
||||
|
||||
protected void BindPhysical(ERR_HQUnit unit)
|
||||
@@ -328,11 +431,42 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!unit.StatusSampler.Bound()) unit.StatusSampler.Bind(unit.Group, record.SourceId, record.InitialStrength, m_Mode.EnemyFaction());
|
||||
if (unit.StatusSampler.Bound()) m_Bodies.RegisterGroup(unit.Group, unit.StatusSampler);
|
||||
}
|
||||
protected void BindSpecialSkills(SCR_AIGroup group)
|
||||
{
|
||||
array<AIAgent> specialists = {}; group.GetAgents(specialists);
|
||||
foreach (AIAgent specialist : specialists)
|
||||
{
|
||||
if (!specialist || !specialist.GetControlledEntity()) continue;
|
||||
SCR_AICombatComponent combat = SCR_AICombatComponent.Cast(specialist.GetControlledEntity().FindComponent(SCR_AICombatComponent));
|
||||
if (!combat) continue;
|
||||
combat.ERR_BindEnemySkill(m_Mode, group);
|
||||
if (!m_SpecialSkills.Contains(combat)) m_SpecialSkills.Insert(combat);
|
||||
}
|
||||
}
|
||||
protected void AuditGroup(ERR_HQUnit unit)
|
||||
{
|
||||
SCR_AIGroup group = unit.Group;
|
||||
string factionKey;
|
||||
if (group.GetFaction()) factionKey = group.GetFaction().GetFactionKey();
|
||||
m_Journal.Write("REGISTER", string.Format("group=%1 force=%2 faction=%3 position=%4 members=%5 home=%6 dedicated=%7; policy=maximum skill/regular aim/alert recognition", unit.Id, unit.ForceSourceId, factionKey, group.GetOrigin(), m_Mode.ForceLiving(group), unit.HomeId, unit.ReservedForSupport));
|
||||
array<AIAgent> auditAgents = {}; group.GetAgents(auditAgents);
|
||||
foreach (AIAgent auditAgent : auditAgents)
|
||||
{
|
||||
if (!auditAgent || !auditAgent.GetControlledEntity()) continue;
|
||||
IEntity actor = auditAgent.GetControlledEntity();
|
||||
string prefab; if (actor.GetPrefabData()) prefab = actor.GetPrefabData().GetPrefabName();
|
||||
m_Journal.Write("SPAWN MEMBER", string.Format("group=%1 force=%2 actor=%3 prefab=%4 position=%5", unit.Id, unit.ForceSourceId, actor, prefab, actor.GetOrigin()));
|
||||
}
|
||||
}
|
||||
|
||||
override void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group) return;
|
||||
// Other hostile factions retain their own intelligence, but receive the same
|
||||
// scoped skill/aim profile. Do not share enemy HQ knowledge across hostile sides.
|
||||
if (m_Mode.IsMissionEnemy(group.GetFaction()) && group.GetFaction() != m_Mode.EnemyFaction()) m_Equipment.RegisterGroup(group, 0, false);
|
||||
foreach (ERR_HQUnit known : m_Units) if (known.Group == group) { BindPhysical(known); return; }
|
||||
if (m_Units.Count() >= 128) return;
|
||||
if (m_Units.Count() >= 256) return;
|
||||
SCR_AIGroupUtilityComponent utility = SCR_AIGroupUtilityComponent.Cast(group.FindComponent(SCR_AIGroupUtilityComponent));
|
||||
if (!utility || group.GetFaction() != m_Mode.EnemyFaction()) return;
|
||||
ERR_HQUnit unit = new ERR_HQUnit();
|
||||
@@ -345,16 +479,25 @@ class ERR_HQ : ERR_CommandService
|
||||
if (home)
|
||||
{
|
||||
unit.HomeId = home.Definition.Id;
|
||||
// Objective security is permanent; it can report contacts but cannot be tasked
|
||||
// with searches, reinforcements, resupply or courier work away from its site.
|
||||
unit.ReservedForSupport = ERR_ObjectiveDefence.Objective(home.Definition.Kind);
|
||||
unit.HomeGuard = !home.Definition.Patrol || home.Definition.Patrol.IsEmpty();
|
||||
}
|
||||
int phase = unit.Id % 5;
|
||||
unit.NextReport = GetGame().GetWorld().GetWorldTime() + phase * 1000;
|
||||
m_Units.Insert(unit);
|
||||
m_Equipment.RegisterGroup(group, unit.Id, !m_Mortars || !m_Mortars.FindGroup(group));
|
||||
// Scouts keep their observation post and authored sniper/radio loadout.
|
||||
if ((m_Scouts && m_Scouts.Contains(group)) || (m_Recon && m_Recon.Owns(group)) || (m_Airfield && m_Airfield.Cargo() && m_Airfield.Cargo().Crew() == group))
|
||||
{
|
||||
unit.ReservedForSupport = true;
|
||||
BindSpecialSkills(group);
|
||||
}
|
||||
else m_Equipment.RegisterGroup(group, unit.Id, !m_Mortars || !m_Mortars.FindGroup(group));
|
||||
BindPhysical(unit);
|
||||
unit.NextStatus = GetGame().GetWorld().GetWorldTime() + 10000 + phase * 1000;
|
||||
unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());
|
||||
m_Journal.Write("REGISTER", string.Format("group %1", unit.Id));
|
||||
AuditGroup(unit);
|
||||
}
|
||||
|
||||
override void UnregisterGroup(SCR_AIGroup group)
|
||||
@@ -363,6 +506,7 @@ class ERR_HQ : ERR_CommandService
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
{
|
||||
if (unit.Group != group) continue;
|
||||
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Remove(Detected); unit.Detection = null; }
|
||||
ReleaseUnit(unit, GetGame().GetWorld().GetWorldTime());
|
||||
unit.Group = null;
|
||||
unit.Utility = null;
|
||||
@@ -443,7 +587,7 @@ class ERR_HQ : ERR_CommandService
|
||||
protected void Observe(ERR_HQUnit unit, float now)
|
||||
{
|
||||
if (!unit.Utility || unit.Uplink || now < unit.NextReport) return;
|
||||
unit.NextReport = now + m_Mode.Settings().Get("reportIntervalSeconds") * 1000;
|
||||
unit.NextReport = now + m_Mode.Settings().Runtime("reportIntervalSeconds") * 1000;
|
||||
array<ref ERR_HQVisualContact> samples = {};
|
||||
ERR_HQObservation.Gather(unit.Utility, now, samples);
|
||||
if (samples.IsEmpty()) return;
|
||||
@@ -572,7 +716,7 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!m_Watch.InvestigationDue(entry.SourceId, now)) continue;
|
||||
ERR_HQUnit best;
|
||||
ERR_HQRoute bestRoute;
|
||||
float distance = m_Mode.Settings().Get("responseRadiusMetres") + 1;
|
||||
float distance = m_Mode.Settings().Runtime("responseRadiusMetres") + 1;
|
||||
int first = -1;
|
||||
int second = -1;
|
||||
foreach (ERR_HQUnit existing : m_Units)
|
||||
@@ -616,7 +760,10 @@ class ERR_HQ : ERR_CommandService
|
||||
// Delivered scalar body evidence survives native corpse cleanup during radio transit.
|
||||
// Memory still applies its own entity-identity rules; casualty accounting needs no live target.
|
||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.BODY && m_Watch.IngestBody(report, now))
|
||||
m_Bodies.ConfirmDelivered(report.Target);
|
||||
{
|
||||
m_Bodies.ConfirmDelivered(report.Target); m_Recovery.ReportBody(report);
|
||||
m_Journal.Write("AGGRESSION", string.Format("identified source=%1 slot=%2 bodies=%3 aggression=%4/30 hunter allowance=%5/15", report.CasualtySourceId, report.CasualtySlot, m_Watch.IdentifiedBodies(), m_Watch.Aggression(), m_Watch.HunterTarget()), report.MessageId);
|
||||
}
|
||||
if (!m_Memory.Ingest(report, now)) return;
|
||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.SIGHTING) m_Coverage.Forget(report.Position);
|
||||
m_Journal.Write("BELIEF", string.Format("delivered report M%1 retained; tracks=%2", report.MessageId, m_Memory.Count()), report.MessageId);
|
||||
@@ -659,7 +806,7 @@ class ERR_HQ : ERR_CommandService
|
||||
{
|
||||
ERR_HQTask task = unit.PendingTask;
|
||||
unit.CancelPending();
|
||||
if (!task || task.ReleaseOnly) return;
|
||||
if (!task || task.ReleaseOnly || !task.SecuritySite.IsEmpty()) return;
|
||||
if (task.Investigation()) { FinishWatch(task, now); return; }
|
||||
foreach (ERR_HQUnit other : m_Units)
|
||||
{
|
||||
@@ -687,7 +834,7 @@ class ERR_HQ : ERR_CommandService
|
||||
{
|
||||
if (!WatchTaskValid(task)) { LostOrder(unit, now); return; }
|
||||
}
|
||||
else
|
||||
else if (task.SecuritySite.IsEmpty())
|
||||
{
|
||||
ERR_HQBelief belief = m_Memory.GetById(task.BeliefId);
|
||||
if (!belief || belief.BeliefRevision != task.BeliefRevision
|
||||
@@ -699,7 +846,7 @@ class ERR_HQ : ERR_CommandService
|
||||
int deliveryId;
|
||||
if (unit.Downlink) deliveryId = unit.Downlink.MessageId;
|
||||
ResourceName waypointResource = ERR_Resources.SearchWaypoint();
|
||||
if (task.RoadIntercept) waypointResource = ERR_Resources.DefendWaypoint();
|
||||
if (task.RoadIntercept || !task.SecuritySite.IsEmpty()) waypointResource = ERR_Resources.DefendWaypoint();
|
||||
AIWaypoint waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(waypointResource, ERR_Actors.Ground(task.Destination)));
|
||||
if (!waypoint) { LostOrder(unit, now); m_Journal.Write("ORDER FAILED", "waypoint spawn failed", order.MessageId); return; }
|
||||
unit.Release();
|
||||
@@ -718,7 +865,7 @@ class ERR_HQ : ERR_CommandService
|
||||
protected void Sweep(ERR_HQUnit unit, float now)
|
||||
{
|
||||
ERR_HQTask task = unit.Task;
|
||||
if (!task || task.RoadIntercept || !unit.Assignment || task.SweepQueued || !unit.Waypoint || !unit.Eligible(m_Mode)) return;
|
||||
if (!task || !task.SecuritySite.IsEmpty() || task.RoadIntercept || !unit.Assignment || task.SweepQueued || !unit.Waypoint || !unit.Eligible(m_Mode)) return;
|
||||
if (Fighting(unit) || vector.DistanceXZ(unit.Group.GetOrigin(), task.Destination) > 40)
|
||||
{ task.ArrivedAt = -1; return; }
|
||||
// Quiet combat state alone does not establish a negative search. Use this
|
||||
@@ -866,8 +1013,12 @@ class ERR_HQ : ERR_CommandService
|
||||
}
|
||||
protected void PlanConvoy(float now)
|
||||
{
|
||||
if (!m_ConvoyTask || m_ConvoyTask.Stopped || !m_ConvoyTask.Ready) return;
|
||||
foreach (ERR_ConvoyVehicle vehicle : m_ConvoyTask.Vehicles)
|
||||
for (int i = 0; i < m_Convoys.Count(); i++) PlanConvoyTask(m_Convoys.At(i), now);
|
||||
}
|
||||
protected void PlanConvoyTask(ERR_ConvoyTask task, float now)
|
||||
{
|
||||
if (!task || task.Stopped || !task.Ready) return;
|
||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||
{
|
||||
if (vehicle.Released || vehicle.Arrived || vehicle.SupplyToken > 0 || !vehicle.Registered || !vehicle.Group) continue;
|
||||
foreach (ERR_HQUnit unit : m_Units)
|
||||
@@ -880,11 +1031,11 @@ class ERR_HQ : ERR_CommandService
|
||||
if (vehicle.Combat.Held && contact.MessageId == vehicle.Combat.ReportId) break;
|
||||
if (contact.ObservedAt < vehicle.Combat.ObservedAt) break;
|
||||
}
|
||||
else if (!vehicle.Combat.Held || now - vehicle.Combat.ObservedAt < 60000) break;
|
||||
else if (vehicle.Combat.ReportId <= 0 || !vehicle.Combat.Held || now - vehicle.Combat.ObservedAt < 60000) break;
|
||||
ERR_HQRoute route = m_Network.Route(unit.Radio, true);
|
||||
if (!route) break;
|
||||
ERR_ConvoyCombatOrder order = new ERR_ConvoyCombatOrder();
|
||||
order.CommandId = m_Journal.Message(); order.SourceId = m_ConvoyTask.Receipt.SourceId;
|
||||
order.CommandId = m_Journal.Message(); order.SourceId = task.Receipt.SourceId;
|
||||
order.VehicleIndex = vehicle.Index; order.Group = unit.Group;
|
||||
order.Hold = contact != null; order.IssuedAt = now; order.ExpiresAt = now + 30000;
|
||||
order.ReportId = vehicle.Combat.ReportId; order.ObservedAt = vehicle.Combat.ObservedAt;
|
||||
@@ -913,7 +1064,11 @@ class ERR_HQ : ERR_CommandService
|
||||
// starve a slower radio downlink. New threat evidence always invalidates RESUME.
|
||||
bool current = !order.Hold && !contact;
|
||||
if (order.Hold && contact) current = contact.ObservedAt >= order.ObservedAt;
|
||||
if (current) accepted = m_Convoys.ApplyCombatOrder(m_ConvoyTask, order, now);
|
||||
if (current)
|
||||
{
|
||||
if (contact && order.Hold) order.Position = contact.Position;
|
||||
accepted = m_Convoys.ApplyCombatOrder(m_Convoys.Find(order.SourceId), order, now);
|
||||
}
|
||||
}
|
||||
if (accepted) m_Journal.Write("CONVOY ORDER DELIVERED", "original crew applied radio travel command", order.CommandId);
|
||||
else if (order) m_Journal.Write("CONVOY ORDER REJECTED", "stale evidence or unavailable original crew; current order retained", order.CommandId);
|
||||
@@ -922,9 +1077,9 @@ class ERR_HQ : ERR_CommandService
|
||||
|
||||
protected void Assign(float now)
|
||||
{
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Get("supportSquadLimit"));
|
||||
int globalLimit = m_Watch.SquadLimit(m_Mode.Settings().Runtime("supportSquadLimit"));
|
||||
int limit = ERR_HQPursuitRules.Limit(globalLimit);
|
||||
int radius = m_Mode.Settings().Get("responseRadiusMetres");
|
||||
int radius = m_Mode.Settings().Runtime("responseRadiusMetres");
|
||||
int assigned = Assigned();
|
||||
int roadQueries;
|
||||
// Snapshot bounded own-force eligibility once per five-second planning pass.
|
||||
@@ -944,8 +1099,8 @@ class ERR_HQ : ERR_CommandService
|
||||
if (!unit.PlanEligible || assigned > globalLimit) { FinishWatch(unit.Task, now); FinishWatch(unit.PendingTask, now); unit.Release(); assigned--; continue; }
|
||||
if (unit.Task && now >= unit.Task.ExpiresAt) { FinishWatch(unit.Task, now); unit.ReleaseAssignment(); }
|
||||
if (unit.PendingTask && now >= unit.PendingTask.ExpiresAt) { FinishWatch(unit.PendingTask, now); unit.CancelPending(); }
|
||||
if (unit.Task && !unit.Task.Investigation() && !m_Memory.GetById(unit.Task.BeliefId)) unit.ReleaseAssignment();
|
||||
if (unit.PendingTask && !unit.PendingTask.ReleaseOnly && !unit.PendingTask.Investigation() && !m_Memory.GetById(unit.PendingTask.BeliefId)) unit.CancelPending();
|
||||
if (unit.Task && unit.Task.SecuritySite.IsEmpty() && !unit.Task.Investigation() && !m_Memory.GetById(unit.Task.BeliefId)) unit.ReleaseAssignment();
|
||||
if (unit.PendingTask && !unit.PendingTask.ReleaseOnly && unit.PendingTask.SecuritySite.IsEmpty() && !unit.PendingTask.Investigation() && !m_Memory.GetById(unit.PendingTask.BeliefId)) unit.CancelPending();
|
||||
}
|
||||
assigned = Assigned();
|
||||
m_HomeAvailable.Clear();
|
||||
@@ -1039,11 +1194,27 @@ class ERR_HQ : ERR_CommandService
|
||||
}
|
||||
}
|
||||
|
||||
override void Maintain(float now)
|
||||
{
|
||||
// Aircraft already airborne keep flying even when the server is empty.
|
||||
m_Recon.Tick(now);
|
||||
m_AirAssault.Maintain(now);
|
||||
m_Airfield.Maintain(now);
|
||||
}
|
||||
override void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_Memory.Tick(now);
|
||||
m_Equipment.Tick(now);
|
||||
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills)
|
||||
{
|
||||
if (!specialSkill) continue;
|
||||
SCR_AIGroup specialGroup = ERR_Actors.Group(specialSkill.GetOwner());
|
||||
if (ERR_EnemyEquipmentRules.Eligible(m_Mode, specialGroup, specialSkill.GetOwner())) specialSkill.ERR_BindEnemySkill(m_Mode, specialGroup);
|
||||
else specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
||||
}
|
||||
m_Hunters.Tick(now);
|
||||
m_ObjectiveDefence.Tick(now);
|
||||
m_Convoys.Tick(now);
|
||||
AutomaticConvoy(now);
|
||||
m_Bodies.TickBudget(now);
|
||||
@@ -1053,6 +1224,11 @@ class ERR_HQ : ERR_CommandService
|
||||
for (int i = m_Units.Count() - 1; i >= 0; i--)
|
||||
{
|
||||
ERR_HQUnit unit = m_Units[i];
|
||||
if (!unit.Detection && unit.Utility && unit.Group)
|
||||
{
|
||||
unit.Detection = unit.Utility.m_Perception;
|
||||
if (unit.Detection) unit.Detection.GetOnEnemyDetected().Insert(Detected);
|
||||
}
|
||||
if (unit.Uplink)
|
||||
{
|
||||
int delivered = unit.Uplink.Advance(now, m_Journal);
|
||||
@@ -1084,12 +1260,23 @@ class ERR_HQ : ERR_CommandService
|
||||
m_Resupply.Tick(now);
|
||||
m_Logistics.Tick(now);
|
||||
m_Mortars.Tick(now);
|
||||
m_AirAssault.Tick(now);
|
||||
m_Airfield.Tick(now); m_Recovery.Tick(now);
|
||||
m_Journal.Flush();
|
||||
}
|
||||
|
||||
override void Stop()
|
||||
{
|
||||
m_Hunters.Stop(); m_ObjectiveDefence.Stop();
|
||||
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills) if (specialSkill) specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
||||
m_SpecialSkills.Clear();
|
||||
foreach (ERR_HQUnit detector : m_Units) if (detector.Detection) detector.Detection.GetOnEnemyDetected().Remove(Detected);
|
||||
m_Airfield.Stop(); m_Recovery.Stop();
|
||||
m_AirAssault.Stop();
|
||||
m_Recon.Stop();
|
||||
m_Scouts.Stop();
|
||||
m_ConvoyStartup.Stop();
|
||||
if (m_SupplyConvoys) m_SupplyConvoys.Stop();
|
||||
m_Resupply.Stop();
|
||||
m_Convoys.Stop();
|
||||
m_Equipment.Stop();
|
||||
|
||||
@@ -45,7 +45,7 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
||||
protected void ConvoyAutoSetting(ERR_GameMode mode, array<string> parts, array<string> replies)
|
||||
{
|
||||
string key = "automaticConvoyEnabled";
|
||||
string effect = "Applies next round only; current startup and manual convoy start unchanged. At most one finite initial convoy; losses are not replaced.";
|
||||
string effect = "Applies next round only; current startup and manual convoy start unchanged. The saved convoy count determines the finite initial fleet; losses are not replaced.";
|
||||
string range = "0 disabled, 1 enabled (default)";
|
||||
if (parts.Count() == 4 && parts[3] == "show")
|
||||
{ replies.Insert(key + "=" + mode.Settings().Get(key).ToString() + " saved for next round. " + effect); return; }
|
||||
@@ -55,8 +55,38 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
||||
else if (parts.Count() == 4 && parts[3] == "help") replies.Insert(range + ". " + effect);
|
||||
else replies.Insert("Invalid value or settings could not be saved; previous setting retained. " + range + ". " + effect);
|
||||
}
|
||||
protected void CountSetting(ERR_GameMode mode, string arguments, array<string> replies)
|
||||
{
|
||||
array<string> countParts = {}; arguments.Split(" ", countParts, true);
|
||||
int offset = 1;
|
||||
if (countParts[0] == "hq") offset = 3;
|
||||
if (countParts.Count() == offset || (countParts.Count() == offset + 1 && countParts[offset] == "show"))
|
||||
replies.Insert("Convoy count saved for next round: " + ERR_Settings.SelectionText(mode.Settings().Get("convoyCount")) + ". " + m_HQ.ConvoyStatus());
|
||||
else
|
||||
{
|
||||
int count;
|
||||
if (countParts.Count() == offset + 2 && countParts[offset] == "set" && ERR_CommandArguments.Selection("convoyCount", countParts[offset + 1], count) && mode.Settings().Set("convoyCount", count))
|
||||
replies.Insert("Convoy count saved for next round: " + ERR_Settings.SelectionText(count) + "; current convoys unchanged.");
|
||||
else replies.Insert("Use convoys show|set 0..20|random. Changes apply next round; lost convoys never respawn.");
|
||||
}
|
||||
}
|
||||
override bool Execute(ERR_GameMode mode, string arguments, array<string> replies)
|
||||
{
|
||||
if (arguments == "hq heli show") { replies.Insert(m_HQ.Recon().Status() + "; " + m_HQ.AirAssault().Status() + "; " + m_HQ.Airfield().Status() + "; " + m_HQ.Recovery().Status()); return true; }
|
||||
if (arguments == "hq heli return") { m_HQ.Recon().Return(); replies.Insert(m_HQ.Recon().Status() + "; " + m_HQ.AirAssault().Status() + "; " + m_HQ.Airfield().Status() + "; " + m_HQ.Recovery().Status()); return true; }
|
||||
if (arguments.IndexOf("hq heli timer set ") == 0)
|
||||
{
|
||||
array<string> timerParts = {}; arguments.Split(" ", timerParts, true);
|
||||
int minutes;
|
||||
if (timerParts.Count() == 5 && ERR_CommandArguments.Integer(timerParts[4], minutes) && m_HQ.Recon().SetDelay(minutes)) replies.Insert(m_HQ.Recon().Status() + "; " + m_HQ.AirAssault().Status() + "; " + m_HQ.Airfield().Status() + "; " + m_HQ.Recovery().Status());
|
||||
else replies.Insert("Use /esc hq heli timer set 1..1440 (minutes). Save failure preserves the current countdown.");
|
||||
return true;
|
||||
}
|
||||
if (arguments == "convoys" || arguments.IndexOf("convoys ") == 0 || arguments == "hq convoy count" || arguments.IndexOf("hq convoy count ") == 0)
|
||||
{
|
||||
CountSetting(mode, arguments, replies);
|
||||
return true;
|
||||
}
|
||||
if (arguments == "hq convoy auto" || arguments.IndexOf("hq convoy auto ") == 0)
|
||||
{
|
||||
if (!m_HQ) { replies.Insert("HQ unavailable."); return true; }
|
||||
@@ -91,10 +121,11 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
||||
override bool Help(string topic, array<string> replies)
|
||||
{
|
||||
if (topic != "hq") return false;
|
||||
replies.Insert("/esc convoys show|set 0..20|random | hq convoy count show|set N (default three; next round)");
|
||||
replies.Insert("/esc hq show | hq memory | hq log show|next|set on|off | hq convoy show|start");
|
||||
replies.Insert("/esc hq interval|squads|radius|stinger show|set N | hq convoy auto show|set 0|1|help");
|
||||
replies.Insert("Stinger: 0 off, 1 every eligible soldier, 2..10000 one in N; applies only before each actor rolls.");
|
||||
replies.Insert("Convoy auto defaults to 1; saves apply next round only. Auto and manual start share one finite initial deployment; losses are not replaced.");
|
||||
replies.Insert("Convoy auto: default 1, next round, finite fleet. /esc hq heli show | hq heli timer set 1..1440 | hq heli return");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ class ERR_HQBodyObserver
|
||||
foreach (ERR_HQBodyScan scan : m_Groups) if (scan.Group == group) known = true;
|
||||
if (!known)
|
||||
{
|
||||
if (m_Groups.Count() >= 128) { Cap(); return; }
|
||||
if (m_Groups.Count() >= 256) { Cap(); return; }
|
||||
ERR_HQBodyScan created = new ERR_HQBodyScan(); created.Group = group;
|
||||
m_Groups.Insert(created);
|
||||
}
|
||||
@@ -91,7 +91,7 @@ class ERR_HQBodyObserver
|
||||
bool found;
|
||||
foreach (ERR_HQBodyCandidate entry : m_Roster) if (entry.Corpse == actor) found = true;
|
||||
if (found) continue;
|
||||
if (m_Roster.Count() >= 128) { Cap(); return; }
|
||||
if (m_Roster.Count() >= 4096) { Cap(); return; }
|
||||
ERR_HQBodyCandidate candidate = new ERR_HQBodyCandidate(); candidate.Corpse = actor;
|
||||
candidate.Roster = sampler; candidate.SourceId = sampler.InitialSourceId();
|
||||
candidate.Slot = member; candidate.Strength = sampler.InitialStrength();
|
||||
|
||||
@@ -79,8 +79,8 @@ class ERR_HQCoverage
|
||||
report.BeliefRevision, report.SweepCompleted, report.ContactFound, report.ObservedAt, now)) return false;
|
||||
if (m_Messages.Contains(report.SourceId) && report.MessageId <= m_Messages.Get(report.SourceId)) return false;
|
||||
// One uplink per source gives monotonic message order. Never evict replay
|
||||
// watermarks; the mission already caps its total physical formations at 128.
|
||||
if (!m_Messages.Contains(report.SourceId) && m_Messages.Count() >= 128) return false;
|
||||
// watermarks; the mission already caps its total physical formations at 256.
|
||||
if (!m_Messages.Contains(report.SourceId) && m_Messages.Count() >= 256) return false;
|
||||
m_Messages.Set(report.SourceId, report.MessageId);
|
||||
int key = ERR_HQCoverageRules.Key(report.Position);
|
||||
if (m_Cells.Contains(key) && m_Cells.Get(key) >= report.ObservedAt) return false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user