Add bounded aircraft control and verify current packaged runtime closure
This commit is contained in:
+136
@@ -0,0 +1,136 @@
|
||||
ERR_ScenarioDefinition {
|
||||
MinimumRadioSources 2
|
||||
Sites {
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-prison-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"
|
||||
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"
|
||||
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 "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||
}
|
||||
RevealOnEscape 1
|
||||
}
|
||||
ERR_SiteDefinition {
|
||||
Id "everon-radio-1"
|
||||
Label "Radio station"
|
||||
DiscoverRadius 8
|
||||
Kind 1
|
||||
Prefab "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
|
||||
Position 6500 0 7200
|
||||
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"
|
||||
Label "Western supply cache"
|
||||
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 "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"
|
||||
Offset 0 0 -8
|
||||
Heading 180
|
||||
ClearanceRadius 4
|
||||
MaximumTerrainRange 1
|
||||
}
|
||||
Stock {
|
||||
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 "{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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
SCR_EntityCatalogMultiList {
|
||||
m_eEntityCatalogType VEHICLE
|
||||
m_aMultiLists {
|
||||
SCR_EntityCatalogMultiListEntry {
|
||||
m_sIdentifier "Mission transport"
|
||||
m_aEntities {
|
||||
SCR_EntityCatalogEntry {
|
||||
m_sEntityPrefab "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"
|
||||
m_aEntityDataList {
|
||||
SCR_EntityCatalogEditorData { m_eValidEditorModes 33 }
|
||||
SCR_EntityCatalogSpawnerData { m_eSlotTypes 24 }
|
||||
}
|
||||
}
|
||||
SCR_EntityCatalogEntry {
|
||||
m_sEntityPrefab "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"
|
||||
m_aEntityDataList {
|
||||
SCR_EntityCatalogEditorData { m_eValidEditorModes 33 }
|
||||
SCR_EntityCatalogSpawnerData { m_eSlotTypes VEHICLE_LARGE }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
SCR_Faction : "{E7B7EA4823DD81F0}Configs/Factions/BLUFOR.conf" {
|
||||
FactionKey "US"
|
||||
UIInfo SCR_FactionUIInfo "{528C961236B7DCD3}" {
|
||||
Name "#AR-Faction_US"
|
||||
Icon "{EB7F65DBC9392557}UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USA.edds"
|
||||
m_sNameUpper "#AR-Faction_US-UC"
|
||||
}
|
||||
"Identity of soldiers" FactionIdentity "{570D128F4022EF5A}" : "{B600946E9ACC17C2}Configs/Identities/FactionIdentity_US.conf" {
|
||||
}
|
||||
"Radio encryption key" "chickenNuggets"
|
||||
m_sFactionFlag "{46FB2FB2C2125A3C}UI/Textures/Flags/US-flag-base-3it.edds"
|
||||
m_FactionFlagMaterial "{610803CC3F3D6649}Assets/Props/Fabric/Flags/Data/Flag_1_2_USA.emat"
|
||||
m_FactionLabel FACTION_US
|
||||
m_CallsignInfo SCR_FactionCallsignInfo "{5DA0F2A6677ADA9E}" : "{CB7824D572DD0D5C}Configs/Callsigns/Callsigns_US.conf" {
|
||||
}
|
||||
m_iFactionRadioFrequency 48000
|
||||
m_aEntityCatalogs {
|
||||
SCR_EntityCatalogMultiList "{5C68A0EE815FA292}" : "{B56E8B4D4B195559}Configs/EntityCatalog/US/Characters_EntityCatalog_US.conf" {
|
||||
}
|
||||
SCR_EntityCatalogMultiList "{5C9E82F29B327997}" : "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf" {
|
||||
}
|
||||
SCR_EntityCatalogMultiList "{5C68A0EE8B97CA50}" : "{4E70E7C4C2C17D4A}Configs/EntityCatalog/US/Groups_EntityCatalog_US.conf" {
|
||||
}
|
||||
SCR_EntityCatalog "{5C68A0EEB7DB3E84}" : "{0E4B62F186F97C43}Configs/EntityCatalog/US/WeaponTripod_EntityCatalog_US.conf" {
|
||||
}
|
||||
SCR_EntityCatalogMultiList "{5C68A0EEB3F124F9}" : "{5F7EC52FC40A03E2}Configs/EntityCatalog/US/InventoryItems_EntityCatalog_US.conf" {
|
||||
}
|
||||
SCR_EntityCatalog "{5C9EDDD37394AD40}" : "{0BF36EFF9FFAF92B}Configs/EntityCatalog/US/SupplyContainerItems_EntityCatalog_US.conf" {
|
||||
}
|
||||
}
|
||||
m_aFlagNames +{
|
||||
}
|
||||
m_aBaseCallsigns {
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F493E764B}" {
|
||||
m_sCallsign "#AR-Campaign_PointAlabama"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Alabama_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Alabama_US_UC"
|
||||
m_iSignalIndex 0
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F49F79967}" {
|
||||
m_sCallsign "#AR-Campaign_PointAlaska"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Alaska_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Alaska_US_UC"
|
||||
m_iSignalIndex 1
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F482DA59B}" {
|
||||
m_sCallsign "#AR-Campaign_PointArizona"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Arizona_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Arizona_US_UC"
|
||||
m_iSignalIndex 2
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F483330D2}" {
|
||||
m_sCallsign "#AR-Campaign_PointArkansas"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Arkansas_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Arkansas_US_UC"
|
||||
m_iSignalIndex 3
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F48170DA8}" {
|
||||
m_sCallsign "#AR-Campaign_PointCalifornia"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_California_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_California_US_UC"
|
||||
m_iSignalIndex 4
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F487834E8}" {
|
||||
m_sCallsign "#AR-Campaign_PointColorado"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Colorado_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Colorado_US_UC"
|
||||
m_iSignalIndex 5
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F48410A86}" {
|
||||
m_sCallsign "#AR-Campaign_PointConnecticut"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Connecticut_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Connecticut_US_UC"
|
||||
m_iSignalIndex 6
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F48A4E31F}" {
|
||||
m_sCallsign "#AR-Campaign_PointDelaware"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Delaware_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Delaware_US_UC"
|
||||
m_iSignalIndex 7
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F488C6B57}" {
|
||||
m_sCallsign "#AR-Campaign_PointFlorida"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Florida_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Florida_US_UC"
|
||||
m_iSignalIndex 8
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F4896F764}" {
|
||||
m_sCallsign "#AR-Campaign_PointGeorgia"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Georgia_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Georgia_US_UC"
|
||||
m_iSignalIndex 9
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F48FBD90E}" {
|
||||
m_sCallsign "#AR-Campaign_PointHawaii"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Hawaii_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Hawaii_US_UC"
|
||||
m_iSignalIndex 10
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F48DFF562}" {
|
||||
m_sCallsign "#AR-Campaign_PointIdaho"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Idaho_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Idaho_US_UC"
|
||||
m_iSignalIndex 11
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F7724C851}" {
|
||||
m_sCallsign "#AR-Campaign_PointIllinois"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Illinois_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Illinois_US_UC"
|
||||
m_iSignalIndex 12
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77097F3F}" {
|
||||
m_sCallsign "#AR-Campaign_PointIndiana"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Indiana_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Indiana_US_UC"
|
||||
m_iSignalIndex 13
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77102B55}" {
|
||||
m_sCallsign "#AR-Campaign_PointIowa"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Iowa_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Iowa_US_UC"
|
||||
m_iSignalIndex 14
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F777ABD04}" {
|
||||
m_sCallsign "#AR-Campaign_PointKansas"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Kansas_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Kansas_US_UC"
|
||||
m_iSignalIndex 15
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F775EBBC1}" {
|
||||
m_sCallsign "#AR-Campaign_PointKentucky"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Kentucky_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Kentucky_US_UC"
|
||||
m_iSignalIndex 16
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77A28EB6}" {
|
||||
m_sCallsign "#AR-Campaign_PointLouisiana"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Louisiana_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Louisiana_US_UC"
|
||||
m_iSignalIndex 17
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77871C23}" {
|
||||
m_sCallsign "#AR-Campaign_PointMaine"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Maine_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Maine_US_UC"
|
||||
m_iSignalIndex 18
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77EB96A0}" {
|
||||
m_sCallsign "#AR-Campaign_PointMaryland"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Maryland_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Maryland_US_UC"
|
||||
m_iSignalIndex 19
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77CC2518}" {
|
||||
m_sCallsign "#AR-Campaign_PointMassachusetts"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Massachusetts_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Massachusetts_US_UC"
|
||||
m_iSignalIndex 20
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F77D3715B}" {
|
||||
m_sCallsign "#AR-Campaign_PointMichigan"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Michigan_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Michigan_US_UC"
|
||||
m_iSignalIndex 21
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F7635D418}" {
|
||||
m_sCallsign "#AR-Campaign_PointMinnesota"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Minnesota_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Minnesota_US_UC"
|
||||
m_iSignalIndex 22
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F761C3FF2}" {
|
||||
m_sCallsign "#AR-Campaign_PointMississippi"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Mississippi_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Mississippi_US_UC"
|
||||
m_iSignalIndex 23
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F7660CFAA}" {
|
||||
m_sCallsign "#AR-Campaign_PointMissouri"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Missouri_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Missouri_US_UC"
|
||||
m_iSignalIndex 24
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F764A6E6B}" {
|
||||
m_sCallsign "#AR-Campaign_PointMontana"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Montana_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Montana_US_UC"
|
||||
m_iSignalIndex 25
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F76AD4D35}" {
|
||||
m_sCallsign "#AR-Campaign_PointNebraska"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Nebraska_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nebraska_US_UC"
|
||||
m_iSignalIndex 26
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F76B1EF3A}" {
|
||||
m_sCallsign "#AR-Campaign_PointNevada"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Nevada_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nevada_US_UC"
|
||||
m_iSignalIndex 27
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F7695FF2F}" {
|
||||
m_sCallsign "#AR-Campaign_PointNewYork"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_NewYork_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_NewYork_US_UC"
|
||||
m_iSignalIndex 28
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F76FF94B1}" {
|
||||
m_sCallsign "#AR-Campaign_PointOhio"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Ohio_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Ohio_US_UC"
|
||||
m_iSignalIndex 29
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F76C68EED}" {
|
||||
m_sCallsign "#AR-Campaign_PointOklahoma"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Oklahoma_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Oklahoma_US_UC"
|
||||
m_iSignalIndex 30
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F752A9F29}" {
|
||||
m_sCallsign "#AR-Campaign_PointOregon"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Oregon_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Oregon_US_UC"
|
||||
m_iSignalIndex 31
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F750E6FB9}" {
|
||||
m_sCallsign "#AR-Campaign_PointPennsylvania"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Pennsylvania_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Pennsylvania_US_UC"
|
||||
m_iSignalIndex 32
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F7517240F}" {
|
||||
m_sCallsign "#AR-Campaign_PointTexas"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Texas_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Texas_US_UC"
|
||||
m_iSignalIndex 33
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F7574D867}" {
|
||||
m_sCallsign "#AR-Campaign_PointUtah"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Utah_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Utah_US_UC"
|
||||
m_iSignalIndex 34
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F755C4871}" {
|
||||
m_sCallsign "#AR-Campaign_PointVermont"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Vermont_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Vermont_US_UC"
|
||||
m_iSignalIndex 35
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F75A0502B}" {
|
||||
m_sCallsign "#AR-Campaign_PointVirginia"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Virginia_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Virginia_US_UC"
|
||||
m_iSignalIndex 36
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F758ABFD3}" {
|
||||
m_sCallsign "#AR-Campaign_PointWashington"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Washington_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Washington_US_UC"
|
||||
m_iSignalIndex 37
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F75EF8263}" {
|
||||
m_sCallsign "#AR-Campaign_PointWisconsin"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Wisconsin_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Wisconsin_US_UC"
|
||||
m_iSignalIndex 38
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C90D92F75F0F224}" {
|
||||
m_sCallsign "#AR-Campaign_PointWyoming"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Wyoming_US"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Wyoming_US_UC"
|
||||
m_iSignalIndex 39
|
||||
}
|
||||
}
|
||||
m_FactionHomeTerritoryConfig SCR_FactionHomeTerritoryConfig "{5AEE94D54FB58C33}" : "{B548A5B264939A98}Configs/Identities/Territories/FactionHomeTerritory_US.conf" {
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{5C67A1DDE0044806}Configs/ERR/Factions/US.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
SCR_Faction : "{B122E3B10BB3404B}Configs/Factions/OPFOR.conf" {
|
||||
FactionKey "USSR"
|
||||
UIInfo SCR_FactionUIInfo "{528C96127F8B6B08}" {
|
||||
Name "#AR-Faction_USSR"
|
||||
Icon "{40B12B0DF911B856}UI/Textures/Editor/EditableEntities/Factions/EditableEntity_Faction_USSR.edds"
|
||||
m_sNameUpper "#AR-Faction_USSR-UC"
|
||||
}
|
||||
"Identity of soldiers" FactionIdentity "{570CFDC6685B6B25}" : "{FBBCD73926989580}Configs/Identities/FactionIdentity_USSR.conf" {
|
||||
}
|
||||
"Radio encryption key" "coldBorscht"
|
||||
m_sFactionFlag "{50824EB9F2C215AB}UI/Textures/Flags/USSR-flag-base-3it.edds"
|
||||
m_FactionFlagMaterial "{B47B52D9DA68905D}Assets/Props/Fabric/Flags/Data/Flag_1_2_SovietArmy.emat"
|
||||
m_FactionLabel FACTION_USSR
|
||||
m_CallsignInfo SCR_FactionCallsignInfo "{5DA0F2A67DFB8809}" : "{958043CE1648EC43}Configs/Callsigns/Callsigns_USSR.conf" {
|
||||
}
|
||||
m_iFactionRadioFrequency 42000
|
||||
m_aEntityCatalogs {
|
||||
SCR_EntityCatalogMultiList "{5C68A0E9BD5E5DD9}" : "{3B679F1799D55F6F}Configs/EntityCatalog/USSR/Characters_EntityCatalog_USSR.conf" {
|
||||
}
|
||||
SCR_EntityCatalogMultiList "{5C9E82F285C09E24}" : "{E4785D3AF1D043C2}Configs/ERR/Factions/MissionVehicles.conf" {
|
||||
}
|
||||
SCR_EntityCatalogMultiList "{5C68A0E9B9AA3C2D}" : "{37D5B75AAC3E72EA}Configs/EntityCatalog/USSR/Groups_EntityCatalog_USSR.conf" {
|
||||
}
|
||||
SCR_EntityCatalog "{5C68A0E9A51460AB}" : "{DDF17C8C140BD1F2}Configs/EntityCatalog/USSR/WeaponTripod_EntityCatalog_USSR.conf" {
|
||||
}
|
||||
SCR_EntityCatalogMultiList "{5C68A0E9AFE1E631}" : "{C53421647C3D0D2E}Configs/EntityCatalog/USSR/InventoryItems_EntityCatalog_USSR.conf" {
|
||||
}
|
||||
SCR_EntityCatalog "{5C9EDDD378BA3298}" : "{B4CA6761AC935808}Configs/EntityCatalog/USSR/SupplyContainerItems_EntityCatalog_USSR.conf" {
|
||||
}
|
||||
}
|
||||
m_iIdentityVoiceSignal 100
|
||||
m_aBaseCallsigns {
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95F3FB520}" {
|
||||
m_sCallsign "#AR-Campaign_PointArena"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Arena_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Arena_RU_UC"
|
||||
m_iSignalIndex 0
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95F0034DB}" {
|
||||
m_sCallsign "#AR-Campaign_PointAvrora"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Avrora_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Avrora_RU_UC"
|
||||
m_iSignalIndex 1
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95F6514E7}" {
|
||||
m_sCallsign "#AR-Campaign_PointBekas"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Bekas_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Bekas_RU_UC"
|
||||
m_iSignalIndex 2
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95F4EE8BF}" {
|
||||
m_sCallsign "#AR-Campaign_PointBizon"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Bizon_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Bizon_RU_UC"
|
||||
m_iSignalIndex 3
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95F5021F9}" {
|
||||
m_sCallsign "#AR-Campaign_PointBolero"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Bolero_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Bolero_RU_UC"
|
||||
m_iSignalIndex 4
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95FBA4C89}" {
|
||||
m_sCallsign "#AR-Campaign_PointCafftan"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Cafftan_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Cafftan_RU_UC"
|
||||
m_iSignalIndex 5
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95F9C00A9}" {
|
||||
m_sCallsign "#AR-Campaign_PointKaban"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Kaban_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Kaban_RU_UC"
|
||||
m_iSignalIndex 6
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95FE17E10}" {
|
||||
m_sCallsign "#AR-Campaign_PointConsul"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Consul_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Consul_RU_UC"
|
||||
m_iSignalIndex 7
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95FCBA475}" {
|
||||
m_sCallsign "#AR-Campaign_PointDimok"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Dimok_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Dimok_RU_UC"
|
||||
m_iSignalIndex 8
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95E2B2963}" {
|
||||
m_sCallsign "#AR-Campaign_PointEffir"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Effir_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Effir_RU_UC"
|
||||
m_iSignalIndex 9
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95E3040F5}" {
|
||||
m_sCallsign "#AR-Campaign_PointFakir"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Fakir_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Fakir_RU_UC"
|
||||
m_iSignalIndex 10
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95E17C67D}" {
|
||||
m_sCallsign "#AR-Campaign_PointGips"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Gips_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Gips_RU_UC"
|
||||
m_iSignalIndex 11
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95E797F8A}" {
|
||||
m_sCallsign "#AR-Campaign_PointGussar"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Gussar_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Gussar_RU_UC"
|
||||
m_iSignalIndex 12
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95E431F83}" {
|
||||
m_sCallsign "#AR-Campaign_PointIgla"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Igla_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Igla_RU_UC"
|
||||
m_iSignalIndex 13
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95EA7F87E}" {
|
||||
m_sCallsign "#AR-Campaign_PointKhek"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Khek_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Khek_RU_UC"
|
||||
m_iSignalIndex 14
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95E88C6B9}" {
|
||||
m_sCallsign "#AR-Campaign_PointLednik"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Lednik_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Lednik_RU_UC"
|
||||
m_iSignalIndex 15
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95EEDE322}" {
|
||||
m_sCallsign "#AR-Campaign_PointMatros"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Matros_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Matros_RU_UC"
|
||||
m_iSignalIndex 16
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95EF6244A}" {
|
||||
m_sCallsign "#AR-Campaign_PointMuskat"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Muskat_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Muskat_RU_UC"
|
||||
m_iSignalIndex 17
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95EDAF04A}" {
|
||||
m_sCallsign "#AR-Campaign_PointNagan"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Nagan_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nagan_RU_UC"
|
||||
m_iSignalIndex 18
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95D3CCF27}" {
|
||||
m_sCallsign "#AR-Campaign_PointNitka"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Nitka_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Nitka_RU_UC"
|
||||
m_iSignalIndex 19
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95D076483}" {
|
||||
m_sCallsign "#AR-Campaign_PointOmega"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Omega_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Omega_RU_UC"
|
||||
m_iSignalIndex 20
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95D68D0F3}" {
|
||||
m_sCallsign "#AR-Campaign_PointPlazma"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Plazma_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Plazma_RU_UC"
|
||||
m_iSignalIndex 21
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95D4C0F8E}" {
|
||||
m_sCallsign "#AR-Campaign_PointPolyn"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Polyn_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Polyn_RU_UC"
|
||||
m_iSignalIndex 22
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95D56EEEE}" {
|
||||
m_sCallsign "#AR-Campaign_PointProlog"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Prolog_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Prolog_RU_UC"
|
||||
m_iSignalIndex 23
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95DBA92EE}" {
|
||||
m_sCallsign "#AR-Campaign_PointRabkor"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Rabkor_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Rabkor_RU_UC"
|
||||
m_iSignalIndex 24
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95C69680D}" {
|
||||
m_sCallsign "#AR-Campaign_PointRadon"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Radon_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Radon_RU_UC"
|
||||
m_iSignalIndex 25
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95C4FD930}" {
|
||||
m_sCallsign "#AR-Campaign_PointSadko"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Sadko_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Sadko_RU_UC"
|
||||
m_iSignalIndex 26
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95C5255FD}" {
|
||||
m_sCallsign "#AR-Campaign_PointSekret"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Sekret_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Sekret_RU_UC"
|
||||
m_iSignalIndex 27
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95CB621AE}" {
|
||||
m_sCallsign "#AR-Campaign_PointShtanga"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Shtanga_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Shtanga_RU_UC"
|
||||
m_iSignalIndex 28
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95C9545BC}" {
|
||||
m_sCallsign "#AR-Campaign_PointShuka"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Shuka_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Shuka_RU_UC"
|
||||
m_iSignalIndex 29
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95CFE26C3}" {
|
||||
m_sCallsign "#AR-Campaign_PointTabor"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Tabor_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Tabor_RU_UC"
|
||||
m_iSignalIndex 30
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95CDD263C}" {
|
||||
m_sCallsign "#AR-Campaign_PointTenor"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Tenor_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Tenor_RU_UC"
|
||||
m_iSignalIndex 31
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95B27128C}" {
|
||||
m_sCallsign "#AR-Campaign_PointTsikson"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Tsikson_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Tsikson_RU_UC"
|
||||
m_iSignalIndex 32
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95B08D83D}" {
|
||||
m_sCallsign "#AR-Campaign_PointUlan"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Ulan_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Ulan_RU_UC"
|
||||
m_iSignalIndex 33
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95B6D3FC2}" {
|
||||
m_sCallsign "#AR-Campaign_PointVermout"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Vermout_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Vermout_RU_UC"
|
||||
m_iSignalIndex 34
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95B712949}" {
|
||||
m_sCallsign "#AR-Campaign_PointViympel"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Viympel_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Viympel_RU_UC"
|
||||
m_iSignalIndex 35
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95B57D185}" {
|
||||
m_sCallsign "#AR-Campaign_PointYantar"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Yantar_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Yantar_RU_UC"
|
||||
m_iSignalIndex 36
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95BB17114}" {
|
||||
m_sCallsign "#AR-Campaign_PointYolka"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Yolka_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Yolka_RU_UC"
|
||||
m_iSignalIndex 37
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95B97C054}" {
|
||||
m_sCallsign "#AR-Campaign_PointZheton"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Zheton_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Zheton_RU_UC"
|
||||
m_iSignalIndex 38
|
||||
}
|
||||
SCR_MilitaryBaseCallsign "{5C9108F95BFC33E5}" {
|
||||
m_sCallsign "#AR-Campaign_PointZurna"
|
||||
m_sCallsignShort "#AR-Comm_Variable_Base_Zurna_RU"
|
||||
m_sCallsignUpperCase "#AR-Comm_Variable_Base_Zurna_RU_UC"
|
||||
m_iSignalIndex 39
|
||||
}
|
||||
}
|
||||
m_FactionHomeTerritoryConfig SCR_FactionHomeTerritoryConfig "{5AEE94D5517C30BD}" : "{31FC78AF00523A99}Configs/Identities/Territories/FactionHomeTerritory_USSR.conf" {
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{D7A5CFA62A36480D}Configs/ERR/Factions/USSR.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
SCR_GameOverScreenConfig : "{651583FFFB7FC765}Configs/GameOverScreen/BaseGameOverScreensConfig.conf" {
|
||||
m_aGameOverScreens {
|
||||
ERR_EscapeGameOverScreenInfo "{6B193DC8DDC63507}" {
|
||||
m_GameOverScreenId VICTORY
|
||||
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
|
||||
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{4984A5C1BFEFC04E}" {
|
||||
m_sTitle "Escape complete"
|
||||
m_sAudioOneShot "SOUND_ONVICTORY"
|
||||
}
|
||||
}
|
||||
ERR_FailureGameOverScreenInfo "{E66F29A7A8AD7438}" {
|
||||
m_GameOverScreenId LOOSE
|
||||
m_GameOverContentLayout "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout"
|
||||
m_OptionalParams SCR_BaseGameOverScreenInfoOptional "{3DBF0BDE715945FA}" {
|
||||
m_sTitle "Escape failed"
|
||||
m_sDebriefing "No surviving prisoners can complete the escape, or the extraction helicopter has been lost."
|
||||
m_sAudioOneShot "SOUND_ONDEFEAT"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
ERR_FactionCatalog {
|
||||
PlayerFactionKey "US"
|
||||
EnemyFactionKey "USSR"
|
||||
PrisonerPrefab "{3FF693715C9F8928}Prefabs/ERR/Prisoner.et"
|
||||
GuardGroupPrefab "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
|
||||
ExtractionPrefab "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
SCR_MissionHeader {
|
||||
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
||||
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
|
||||
m_sName "Rereintroduced: Everon Base (development)"
|
||||
m_sAuthor "Rereintroduced contributors"
|
||||
m_sDescription "Independent base escape mission using vanilla Reforger. Companion addons optional. Development scenario."
|
||||
m_sGameMode "Escape"
|
||||
m_iPlayerCount 12
|
||||
m_eSaveTypes 0
|
||||
m_bShowInScenarioMenu 1
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{AD62322D5213C440}Missions/ERR_Everon_Base.conf"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
ERR_LocationComponent "{C17CA14398A1CD7E}" { m_Kind 2 }
|
||||
RplComponent "{0BF5BAAA5FBFEECE}" { Streamable Disabled }
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{033503110AC34EAC}Prefabs/ERR/Extraction.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
SCR_FactionManager : "{A242612865F2A48E}Prefabs/MP/Managers/Factions/FactionManager_Base.et" {
|
||||
Factions {
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
MeshObject "{A557F6DC012E5101}" {
|
||||
Object "{268DBDABB3C94AD7}Assets/Props/Military/Sandbags/Sandbag_01_short.xob"
|
||||
Materials {
|
||||
MaterialAssignClass "{A557F6DC012E5102}" { SourceMaterial "Sandbags_01_Short" AssignedMaterial "{31529E064AA7C917}Assets/Props/Military/Sandbags/Data/Sandbags_01_short_Burlap.emat" }
|
||||
MaterialAssignClass "{A557F6DC012E5103}" { SourceMaterial "Sandbags_01_MLOD" AssignedMaterial "{2169E8EB73E1C127}Assets/Props/Military/Sandbags/Data/Sandbags_01_Burlap_MLOD.emat" }
|
||||
}
|
||||
}
|
||||
RigidBody "{A557F6DC012E5104}" { ModelGeometry 1 Static 1 }
|
||||
RplComponent "{A557F6DC012E5105}" {}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et"
|
||||
Configurations {
|
||||
ResourceClass PC {}
|
||||
ResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
|
||||
components {
|
||||
SCR_GameOverScreenManagerComponent "{56DEC646298AD219}" {
|
||||
m_GameOverScreenConfig SCR_GameOverScreenConfig "{56DEC6462FFDD82D}" : "{F6633922A25F9441}Configs/ERR/GameOverScreens.conf" {}
|
||||
}
|
||||
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
|
||||
m_Definition "{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"
|
||||
}
|
||||
SCR_RespawnSystemComponent "{56B2B4793051E7C9}" {
|
||||
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
|
||||
}
|
||||
}
|
||||
m_bUseSpawnPreload 0
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{978E045E53EF36C8}Prefabs/ERR/GameMode.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
GenericEntity : "{6D56FED1E55A8F84}Prefabs/Items/Misc/IntelligenceFolder_E_01/IntelligenceFolder_E_01.et" {
|
||||
components {
|
||||
ERR_DocumentComponent "{E4598D0BE2C251A3}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
SCR_AIGroup : "{E552DABF3636C2AD}Prefabs/Groups/OPFOR/Group_USSR_RifleSquad.et" {
|
||||
m_aUnitPrefabSlots {
|
||||
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"
|
||||
"{612F43A4D5AE765F}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_RTO.et"
|
||||
"{23ADBBC31B6A3DC6}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et"
|
||||
"{1C78331E156A3D65}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et"
|
||||
"{333DA6244C7DA34C}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et"
|
||||
"{BF643BE4ADBDFDD3}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_LAT.et"
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{D5A4D0D3AE1A3E26}Prefabs/ERR/Patrol.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS {}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
ERR_LocationComponent "{3B60CBEB3C04333C}" { m_Kind 0 }
|
||||
Hierarchy "{30ADADE77D1EC3B8}" {}
|
||||
RplComponent "{7FA5168C6FBD0C22}" {}
|
||||
}
|
||||
{
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "2879356FFFBE36E0"
|
||||
coords -6 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "C99EF9AFCEBA863D"
|
||||
coords -3 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "91048277F04ACA61"
|
||||
coords 0 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "8F2200A922FB0A6E"
|
||||
coords 3 0 6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "3C442429BBCF5D2F"
|
||||
coords -6 0 -6
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "9E855CC802C592CA"
|
||||
coords 6 0 -6
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "778DAC42C9208F67"
|
||||
coords -6 0 -3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "7E350EB9053673C4"
|
||||
coords 6 0 -3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "AD17884D5EA37D37"
|
||||
coords -6 0 0
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "1A157E23E1A995D8"
|
||||
coords 6 0 0
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "3A78A6F3ACB7ACB1"
|
||||
coords -6 0 3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "275B472B565FFF6C"
|
||||
coords 6 0 3
|
||||
angles 0 -90 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "1B20E11B3FC7B59A"
|
||||
coords -6 0 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "2735B52246DD6213"
|
||||
coords -3 0 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et" {
|
||||
ID "28101A0E33470B38"
|
||||
coords 3 0 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
GenericEntity : "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et" {
|
||||
ID "54D8F96ECC743335"
|
||||
scale 1.5
|
||||
coords 0 -0.1 -6
|
||||
angles 0 0 0
|
||||
}
|
||||
DecalEntity : "{4AD6EB80A8B1FA3A}Prefabs/Decals/Dirt/Decal_Dirt_Patch_03.et" {
|
||||
ID "FCFBA935CC13E016"
|
||||
components {
|
||||
Hierarchy "{EF4B704D646D1759}" {}
|
||||
}
|
||||
coords 0 0 0
|
||||
angles -90 0 0
|
||||
scale 10
|
||||
FarPlane 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{BE6B8D56349038A6}Prefabs/ERR/Prison.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
GenericEntity : "{34B08027DBDE322A}Prefabs/Structures/BuildingParts/Doors/Door_MetalGate_01_Short_LEFT_EXT_ST_B.et" {
|
||||
components {
|
||||
DoorComponent "{221C667A9F7D4B69}" {
|
||||
DoorAction ERR_PrisonDoorAction "{50888D3646A61640}" {
|
||||
ParentContextList { "SCR_DoorUserAction" }
|
||||
}
|
||||
}
|
||||
ActionsManagerComponent "{F32B667C3C823800}" {
|
||||
ActionContexts { UserActionContext "{50888D37F529F720}" { ContextName "SCR_DoorUserAction" Radius 2 } }
|
||||
additionalActions {
|
||||
ERR_UnlockPrisonAction "{CA7918FEA177BFB8}" {
|
||||
ParentContextList { "SCR_DoorUserAction" }
|
||||
UIInfo SCR_ActionUIInfo "{F734C81E11AA6BEF}" { Name "Pick prison lock" }
|
||||
Duration 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{C3FDC4DBCD58C2F5}Prefabs/ERR/PrisonDoor.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
Hierarchy "{4F20A90EDBD625A4}" {}
|
||||
MeshObject "{4F20A90EDBD625A1}" { Object "{0B96D139FEBCA006}Assets/Structures/Walls/Concrete/ConcreteWall_01_3m.xob" }
|
||||
RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 }
|
||||
RplComponent "{4F20A90EDBD625A3}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
SCR_ChimeraCharacter : "{836E7E39AAC5888B}Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Base.et" {
|
||||
components {
|
||||
ActionsManagerComponent "{520EA1D2F659CE02}" {
|
||||
additionalActions {
|
||||
ERR_LoadCasualtyAction "{E7016B8C499EC8A3}" {
|
||||
ParentContextList { "Chest" "LowerTorso" }
|
||||
UIInfo SCR_ActionUIInfo "{E7016B8C499EC8A4}" { Name "Load casualty into extraction helicopter" }
|
||||
Duration 5
|
||||
}
|
||||
}
|
||||
}
|
||||
SCR_EditableCharacterComponent "{520EA1D2F659C85C}" {
|
||||
m_UIInfo SCR_EditableEntityUIInfo "{5298E609432D192D}" {
|
||||
Name "Prisoner"
|
||||
}
|
||||
}
|
||||
SCR_CharacterInventoryStorageComponent "{520EA1D2DB118DAC}" {
|
||||
components {
|
||||
SCR_EquipmentStorageComponent "{56B49DA9722B635D}" {
|
||||
InitialStorageSlots {
|
||||
SCR_EquipmentStorageSlot BinocularSlot { Prefab "" }
|
||||
SCR_EquipmentStorageSlot WristwatchSlot {
|
||||
Prefab "{78ED4FEF62BBA728}Prefabs/Items/Equipment/Watches/Watch_SandY184A.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BaseLoadoutManagerComponent "{520EA1D2DB11821E}" {
|
||||
Slots {
|
||||
LoadoutSlotInfo Hat { Prefab "{F545C1BE7C99954A}Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et" }
|
||||
LoadoutSlotInfo Jacket { Prefab "{3CCA7A9BB4FD3197}Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et" }
|
||||
LoadoutSlotInfo Pants { Prefab "{604BB72BE8E023C2}Prefabs/Characters/Uniforms/Pants_US_BDU.et" }
|
||||
SCR_FeetLoadoutSlotInfo Boots { Prefab "{DAAFD15478BDE1C3}Prefabs/Characters/Footwear/CombatBoots_US_01.et" }
|
||||
LoadoutSlotInfo ArmoredVest { Prefab "" }
|
||||
LoadoutSlotInfo Back { Prefab "" }
|
||||
LoadoutSlotInfo Vest { Prefab "" }
|
||||
LoadoutSlotInfo HandWear { Prefab "" }
|
||||
}
|
||||
}
|
||||
SCR_InventoryStorageManagerComponent "{520EA1D2DB118DE5}" {
|
||||
InitialInventoryItems {
|
||||
ItemsInitConfigurationItem "{EA7AC3C2078B68C1}" {
|
||||
TargetStorage "Prefabs/Characters/Uniforms/Pants_US_BDU.et"
|
||||
PrefabsToSpawn {
|
||||
"{A81F501D3EF6F38E}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et"
|
||||
"{A81F501D3EF6F38E}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et"
|
||||
"{D70216B1B2889129}Prefabs/Items/Medicine/Tourniquet_01/Tourniquet_US_01.et"
|
||||
"{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
MetaFileClass {
|
||||
Name "{3FF693715C9F8928}Prefabs/ERR/Prisoner.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {}
|
||||
EntityTemplateResourceClass HEADLESS : PC {}
|
||||
}
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
MeshObject "{111C06CB740ED195}" { Object "{77C9FCF3FB1246E9}Assets/Items/Equipment/Radios/Radio_R107M/Radio_R107M_01_item.xob" }
|
||||
RigidBody "{4C174A44D07B91BA}" { ModelGeometry 1 Static 1 }
|
||||
ERR_LocationComponent "{C19CA6CC83470879}" { m_Kind 1 }
|
||||
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
|
||||
ActionsManagerComponent "{B081EC0768636EAB}" {
|
||||
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" { ContextName "ERR_Radio" Radius 2 } }
|
||||
additionalActions { ERR_TuneRadioAction "{7ABBB6B5D73DDD95}" {
|
||||
ParentContextList { "ERR_Radio" }
|
||||
UIInfo SCR_ActionUIInfo "{1B42BB7CEDECB994}" { Name "Contact extraction HQ" }
|
||||
Duration 8
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{F7E7E3D6ACF0C860}Prefabs/ERR/Radio.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
SCR_ManualCamera {
|
||||
m_bInputEnabled 0
|
||||
m_bStartAtEntity 1
|
||||
m_aComponents {
|
||||
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et"
|
||||
Configurations {
|
||||
EntityTemplateResourceClass PC {
|
||||
}
|
||||
EntityTemplateResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
GenericEntity : "{EBBC859CCD94D96D}Prefabs/Props/EmptyStorages/USSR_AmmoBox_empty.et" {
|
||||
components {
|
||||
MeshObject "{6530845A09309E37}" {
|
||||
Object "{819ED3266EE5113B}Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/EquipmentBoxWooden_Weapon_01.xob"
|
||||
Materials {
|
||||
MaterialAssignClass "{6530845A093091BB}" {
|
||||
SourceMaterial "EquipmentBoxWooden_Weapon_01"
|
||||
AssignedMaterial "{7D2E831AFA5CF50D}Assets/Props/Military/AmmoBox/EquipmentBoxWooden_01/Data/EquipmentBoxWooden_Weapon_01_USSR.emat"
|
||||
}
|
||||
MaterialAssignClass "{6530845A093091B2}" {
|
||||
SourceMaterial "EquipmentBoxWooden_Weapon_01_Decal"
|
||||
}
|
||||
}
|
||||
}
|
||||
SCR_UniversalInventoryStorageComponent "{6530845A09302DE0}" {
|
||||
MaxCumulativeVolume 120000
|
||||
MaxItemSize 120 40 40
|
||||
m_fMaxWeight 80
|
||||
Attributes SCR_ItemAttributeCollection "{6530845A09302DFF}" {
|
||||
ItemDisplayName SCR_InventoryUIInfo "{6530845A09302D03}" {
|
||||
Name "Supply cache"
|
||||
Description "Limited equipment shared between prisoners."
|
||||
}
|
||||
}
|
||||
}
|
||||
InventoryStorageManagerComponent "{18A1D42A468F584F}" {}
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
GenericEntity {
|
||||
components {
|
||||
ERR_LocationComponent "{8E7CFEA92C1811F0}" { m_Kind 4 }
|
||||
RplComponent "{98A4A31435A9CA04}" { Streamable Disabled }
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
MetaFileClass {
|
||||
Name "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||
Configurations {
|
||||
ResourceClass PC {
|
||||
}
|
||||
ResourceClass HEADLESS : PC {
|
||||
}
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
class ERR_UnlockPrisonAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
SetCannotPerformReason(string.Empty);
|
||||
if (!mode) { SetCannotPerformReason("Mission is not ready"); return false; }
|
||||
if (mode.Phase() != ERR_Phase.PRISON) { SetCannotPerformReason("Prison lock is not available in this mission phase"); return false; }
|
||||
if (!ERR_Actors.CanFight(user)) { SetCannotPerformReason("You must be alive and conscious"); return false; }
|
||||
return true;
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.UnlockPrison(pUserEntity)) return;
|
||||
BaseDoorComponent door = BaseDoorComponent.Cast(pOwnerEntity.FindComponent(BaseDoorComponent));
|
||||
if (door) door.UseDoorAction(pUserEntity);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_PrisonDoorAction : SCR_DoorUserAction
|
||||
{
|
||||
// Native AI calls PerformAction directly, bypassing the UI availability query.
|
||||
// Recheck the same permission here; preserve vanilla execution/broadcasting
|
||||
// on both server and clients once the replicated prison phase is unlocked.
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
if (!CanBePerformedScript(pUserEntity)) return;
|
||||
super.PerformAction(pOwnerEntity, pUserEntity);
|
||||
}
|
||||
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
SetCannotPerformReason(string.Empty);
|
||||
if (!mode) { SetCannotPerformReason("Mission is not ready"); return false; }
|
||||
if (mode.Phase() <= ERR_Phase.PRISON)
|
||||
{
|
||||
SetCannotPerformReason("Pick the prison lock before opening the gate");
|
||||
return false;
|
||||
}
|
||||
// Vanilla owns any obstruction/door-specific reason after unlocking.
|
||||
return super.CanBePerformedScript(user);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_TuneRadioAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
ERR_LocationComponent location = ERR_LocationComponent.Cast(GetOwner().FindComponent(ERR_LocationComponent));
|
||||
SetCannotPerformReason(string.Empty);
|
||||
if (!mode) { SetCannotPerformReason("Mission is not ready"); return false; }
|
||||
if (mode.Phase() != ERR_Phase.RADIO_SEARCH)
|
||||
{
|
||||
string reason = "Radio is not available in this mission phase";
|
||||
if (mode.Phase() == ERR_Phase.PRISON) reason = "Escape the prison first";
|
||||
if (mode.Phase() == ERR_Phase.RADIO_DEFENCE) reason = "Transmission is already in progress; defend the radio";
|
||||
if (mode.Phase() == ERR_Phase.EXTRACTION) reason = "Transmission is complete; reach the extraction helicopter";
|
||||
SetCannotPerformReason(reason);
|
||||
return false;
|
||||
}
|
||||
if (!location) { SetCannotPerformReason("Radio location is unavailable"); return false; }
|
||||
if (location.Knowledge() != 2) { SetCannotPerformReason("Identify this radio through recovered intel or close reconnaissance first"); return false; }
|
||||
if (!ERR_Actors.CanFight(user)) { SetCannotPerformReason("You must be alive and conscious"); return false; }
|
||||
return true;
|
||||
}
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.TuneRadio(pUserEntity, ERR_LocationComponent.Cast(pOwnerEntity.FindComponent(ERR_LocationComponent)));
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
class ERR_Actors
|
||||
{
|
||||
static bool Alive(IEntity actor)
|
||||
{
|
||||
if (!actor) return false;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
return damage && !damage.IsDestroyed();
|
||||
}
|
||||
|
||||
static bool CanFight(IEntity actor)
|
||||
{
|
||||
if (!Alive(actor)) return false;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
|
||||
return controller && !controller.IsUnconscious();
|
||||
}
|
||||
|
||||
static SCR_AIGroup Group(IEntity actor)
|
||||
{
|
||||
if (!actor) return null;
|
||||
AIControlComponent control = AIControlComponent.Cast(actor.FindComponent(AIControlComponent));
|
||||
if (!control || !control.GetAIAgent()) return null;
|
||||
return SCR_AIGroup.Cast(control.GetAIAgent().GetParentGroup());
|
||||
}
|
||||
|
||||
static bool Player(IEntity actor)
|
||||
{
|
||||
return actor && GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(actor) > 0;
|
||||
}
|
||||
|
||||
static IEntity Spawn(ResourceName prefab, vector position, vector angles = "0 0 0")
|
||||
{
|
||||
if (!Replication.IsServer() || prefab.IsEmpty()) return null;
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return null;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
Math3D.AnglesToMatrix(angles, params.Transform);
|
||||
params.Transform[3] = position;
|
||||
return GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params);
|
||||
}
|
||||
|
||||
static void DeleteSpawned(IEntity entity)
|
||||
{
|
||||
if (!Replication.IsServer() || !entity || Player(entity)) return;
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(entity);
|
||||
if (group)
|
||||
{
|
||||
// Never remove a group that has acquired a player since mission creation.
|
||||
if (group.GetPlayerCount(true) > 0) return;
|
||||
group.DespawnMembers();
|
||||
}
|
||||
SCR_EntityHelper.DeleteEntityAndChildren(entity);
|
||||
}
|
||||
|
||||
static vector Ground(vector position)
|
||||
{
|
||||
position[1] = GetGame().GetWorld().GetSurfaceY(position[0], position[2]) + 0.1;
|
||||
return position;
|
||||
}
|
||||
|
||||
static void Players(out array<IEntity> players, bool conscious = false)
|
||||
{
|
||||
players.Clear();
|
||||
array<int> ids = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (Alive(actor) && (!conscious || CanFight(actor))) players.Insert(actor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One server-owned implementation is installed by the AI addon.
|
||||
class ERR_CommandService
|
||||
{
|
||||
void Tick(float now) {}
|
||||
void Stop() {}
|
||||
void RegisterGroup(SCR_AIGroup group) {}
|
||||
void UnregisterGroup(SCR_AIGroup group) {}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Transport-independent administration. Only the owning controller supplies
|
||||
// authorization, derived on the server from its authenticated player ID.
|
||||
class ERR_CommandArguments
|
||||
{
|
||||
static string CoreSetting(string section, string option)
|
||||
{
|
||||
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
|
||||
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
|
||||
return "";
|
||||
}
|
||||
static bool Integer(string text, out int value)
|
||||
{
|
||||
if (text.IsEmpty() || text.Length() > 6) return false;
|
||||
for (int i = 0; i < text.Length(); i++)
|
||||
if ("0123456789".IndexOf(text.Substring(i, 1)) < 0) return false;
|
||||
value = text.ToInt();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
class ERR_AdminService
|
||||
{
|
||||
protected string Setting(string section, string option)
|
||||
{
|
||||
return ERR_CommandArguments.CoreSetting(section, option);
|
||||
}
|
||||
protected bool Extension(ERR_GameMode mode, string arguments, array<string> replies) { return false; }
|
||||
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
|
||||
{
|
||||
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
|
||||
string key = "startTimeMinutes";
|
||||
if (parts[1] == "speed") key = "timeMultiplier";
|
||||
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.");
|
||||
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);
|
||||
}
|
||||
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.");
|
||||
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 "";
|
||||
}
|
||||
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; }
|
||||
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");
|
||||
else
|
||||
{
|
||||
string failure = "Invalid value or settings could not be saved; previous setting retained.";
|
||||
if (!help.IsEmpty()) failure += " " + help;
|
||||
replies.Insert(failure);
|
||||
}
|
||||
}
|
||||
protected string Help()
|
||||
{
|
||||
return "/esc status | settings | mission abandon | mission radio show|set N | mission date show|set YYYY-MM-DD|authored | mission weather show|set authored|clear|overcast|rain | mission time show|set HH:MM | mission speed show|set N | intel reveal show|set N|help | spectate next|previous";
|
||||
}
|
||||
void Execute(ERR_GameMode mode, bool authorized, string arguments, array<string> replies)
|
||||
{
|
||||
replies.Clear();
|
||||
if (!Replication.IsServer() || !mode) return;
|
||||
if (!authorized) { replies.Insert("Administrator access required."); return; }
|
||||
if (arguments.Length() > 160) { replies.Insert("Command is too long."); return; }
|
||||
arguments = arguments.Trim(); arguments.ToLower();
|
||||
if (arguments == "status") { replies.Insert(mode.Status()); Extension(mode, arguments, replies); return; }
|
||||
if (arguments == "mission abandon")
|
||||
{
|
||||
if (mode.AbandonRound()) replies.Insert("Round abandoned. The normal defeat and restart sequence has begun.");
|
||||
else replies.Insert("Only an active round can be abandoned.");
|
||||
return;
|
||||
}
|
||||
if (!mode.Settings()) { replies.Insert("Mission settings are not ready."); return; }
|
||||
if (arguments == "settings")
|
||||
{
|
||||
foreach (string key : ERR_Settings.Keys()) replies.Insert(key + "=" + mode.Settings().Get(key).ToString());
|
||||
return;
|
||||
}
|
||||
if (Extension(mode, arguments, replies)) return;
|
||||
array<string> parts = {};
|
||||
arguments.Split(" ", parts, true);
|
||||
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
|
||||
if (TimeCommand(mode, parts, replies)) return;
|
||||
if (parts.Count() == 3 || parts.Count() == 4)
|
||||
{
|
||||
string key = Setting(parts[0], parts[1]);
|
||||
if (!key.IsEmpty())
|
||||
{
|
||||
SettingCommand(mode.Settings(), key, parts, replies);
|
||||
return;
|
||||
}
|
||||
}
|
||||
replies.Insert(Help());
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
// Conservative fallback for a sleeping, engine-off Mi8 whose native simulation
|
||||
// has not yet populated contact flags. Reads physical support; never wakes it.
|
||||
class ERR_CasualtyGroundSupport
|
||||
{
|
||||
static bool SupportHit(float fraction, float normalY, bool selfHit, bool dynamicHit)
|
||||
{
|
||||
return fraction >= 0 && fraction < 1 && normalY >= 0.85 && !selfHit && !dynamicHit;
|
||||
}
|
||||
static bool BelongsTo(IEntity entity, IEntity root)
|
||||
{
|
||||
while (entity)
|
||||
{
|
||||
if (entity == root) return true;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool WheelCenter(Vehicle aircraft, string bone, out vector center, bool diagnostics = false)
|
||||
{
|
||||
if (!aircraft) return false;
|
||||
if (aircraft.GetBoneIndex(bone) == -1)
|
||||
{
|
||||
if (diagnostics) Print("ERR GEAR PIVOT MISSING: " + bone);
|
||||
return false;
|
||||
}
|
||||
vector identity[4]; Math3D.MatrixIdentity4(identity);
|
||||
PointInfo pivot = new PointInfo();
|
||||
pivot.Set(aircraft, bone, identity);
|
||||
vector world[4]; pivot.GetWorldTransform(world);
|
||||
center = world[3];
|
||||
if (diagnostics) PrintFormat("ERR GEAR PIVOT: bone=%1 node=%2 center=%3", bone, pivot.GetNodeId(), center);
|
||||
return pivot.GetNodeId() != -1;
|
||||
}
|
||||
static bool WheelSupported(Vehicle aircraft, vector center, float radius, bool diagnostics = false)
|
||||
{
|
||||
vector bottom = center - Vector(0, radius, 0);
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = bottom + "0 0.10 0";
|
||||
trace.End = bottom - "0 0.12 0";
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = aircraft;
|
||||
float fraction = aircraft.GetWorld().TraceMove(trace, null);
|
||||
bool selfHit = trace.TraceEnt && BelongsTo(trace.TraceEnt, aircraft);
|
||||
bool dynamicHit;
|
||||
if (trace.TraceEnt)
|
||||
{
|
||||
Physics surface = trace.TraceEnt.GetPhysics();
|
||||
// Unknown/entity-only support and moving actors/vehicles are not parking.
|
||||
dynamicHit = !surface || surface.IsDynamic();
|
||||
}
|
||||
bool supported = SupportHit(fraction, trace.TraceNorm[1], selfHit, dynamicHit);
|
||||
if (diagnostics)
|
||||
{
|
||||
vector contact = vector.Lerp(trace.Start, trace.End, fraction);
|
||||
float terrainY = aircraft.GetWorld().GetSurfaceY(bottom[0], bottom[2]);
|
||||
PrintFormat("ERR GEAR TERRAIN: bottom=%1 terrainY=%2 tireTerrainGap=%3", bottom, terrainY, bottom[1] - terrainY);
|
||||
PrintFormat("ERR GEAR SUPPORT: center=%1 radius=%2 bottom=%3 fraction=%4 normal=%5 self=%6 dynamic=%7 supported=%8 gap=%9", center, radius, bottom, fraction, trace.TraceNorm, selfHit, dynamicHit, supported, bottom[1] - contact[1]);
|
||||
}
|
||||
return supported;
|
||||
}
|
||||
static bool Parked(Vehicle aircraft, bool diagnostics = false)
|
||||
{
|
||||
if (!aircraft || !aircraft.GetPrefabData() || !aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return false;
|
||||
Physics physics = aircraft.GetPhysics();
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
|
||||
if (!physics || !controller || !controller.GetSimulation()) return false;
|
||||
if (diagnostics) PrintFormat("ERR PARKED SUPPORT: physicsAwake=%1 engineOn=%2 nativeContact=%3", physics.IsActive(), controller.GetSimulation().EngineIsOn(), controller.GetSimulation().HasAnyGroundContact());
|
||||
if (physics.IsActive() || controller.GetSimulation().EngineIsOn()) return false;
|
||||
vector front, left, right;
|
||||
if (!WheelCenter(aircraft, "v_wheel_F", front, diagnostics) || !WheelCenter(aircraft, "v_wheel_bL", left, diagnostics) || !WheelCenter(aircraft, "v_wheel_bR", right, diagnostics)) return false;
|
||||
// Fail closed if native pivot resolution collapses to an owner origin or
|
||||
// otherwise cannot represent the authored three-point landing footprint.
|
||||
if (vector.DistanceXZ(front, left) < 1 || vector.DistanceXZ(front, right) < 1 || vector.DistanceXZ(left, right) < 1) return false;
|
||||
// Radii come from installed Mi8_base.et's authored landing-gear configuration.
|
||||
// Every wheel needs a short, upward-facing static collision directly below.
|
||||
bool a = WheelSupported(aircraft, front, 0.30, diagnostics);
|
||||
bool b = WheelSupported(aircraft, left, 0.45, diagnostics);
|
||||
bool c = WheelSupported(aircraft, right, 0.45, diagnostics);
|
||||
return a && b && c;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
// Round-owned server service; only the aircraft reference is replicated for action visibility.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_CasualtyLoadingService m_ERRCasualtyLoading;
|
||||
[RplProp()]
|
||||
protected RplId m_ERRCasualtyAircraftId = RplId.Invalid();
|
||||
ERR_CasualtyLoadingService CasualtyLoading() { return m_ERRCasualtyLoading; }
|
||||
Vehicle CasualtyLoadingAircraft()
|
||||
{
|
||||
if (Replication.IsServer()) return ExtractionVehicle();
|
||||
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRCasualtyAircraftId));
|
||||
if (!component) return null;
|
||||
return Vehicle.Cast(component.GetEntity());
|
||||
}
|
||||
protected void ERR_UpdateCasualtyAircraft(float now)
|
||||
{
|
||||
Vehicle aircraft = ExtractionVehicle();
|
||||
if (!aircraft)
|
||||
{
|
||||
if (m_ERRCasualtyAircraftId != RplId.Invalid()) { m_ERRCasualtyAircraftId = RplId.Invalid(); Replication.BumpMe(); }
|
||||
return;
|
||||
}
|
||||
RplComponent component = RplComponent.Cast(aircraft.FindComponent(RplComponent));
|
||||
if (!component || m_ERRCasualtyAircraftId == component.Id()) return;
|
||||
m_ERRCasualtyAircraftId = component.Id();
|
||||
Replication.BumpMe();
|
||||
}
|
||||
protected void ERR_StopCasualtyLoading()
|
||||
{
|
||||
if (m_ERRCasualtyLoading)
|
||||
{
|
||||
Players().OnChanged().Remove(m_ERRCasualtyLoading.RosterChanged);
|
||||
OnTick().Remove(m_ERRCasualtyLoading.Tick);
|
||||
m_ERRCasualtyLoading.Stop();
|
||||
}
|
||||
OnTick().Remove(ERR_UpdateCasualtyAircraft);
|
||||
}
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (!Replication.IsServer() || SCR_Global.IsEditMode()) return;
|
||||
m_ERRCasualtyLoading = new ERR_CasualtyLoadingService(this);
|
||||
OnTick().Insert(ERR_UpdateCasualtyAircraft);
|
||||
OnTick().Insert(m_ERRCasualtyLoading.Tick);
|
||||
Players().OnChanged().Insert(m_ERRCasualtyLoading.RosterChanged);
|
||||
OnStop().Insert(ERR_StopCasualtyLoading);
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
// Only successor service methods initiate this controller-owned handshake.
|
||||
// No caller-supplied entity/seat RPC can ask the server to load another player.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected RplId m_ERRPreparedLoadMode = RplId.Invalid();
|
||||
protected int m_ERRPreparedLoadNonce;
|
||||
protected int m_ERRPreparedLoadClock;
|
||||
protected int m_ERRLastLoadNonce;
|
||||
|
||||
void ERR_PrepareCasualtyLoad(RplId modeId, int nonce)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (this == GetGame().GetPlayerController()) ERR_PrepareCasualtyLoadOwner(modeId, nonce);
|
||||
else Rpc(ERR_PrepareCasualtyLoadOwner, modeId, nonce);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
protected void ERR_PrepareCasualtyLoadOwner(RplId modeId, int nonce)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (this != GetGame().GetPlayerController() || !mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !ERR_CasualtyLoadingRules.GroundSettled(GetControlledEntity())) return;
|
||||
if (m_ERRPreparedLoadMode == modeId && nonce <= m_ERRLastLoadNonce) return;
|
||||
if (nonce <= 0) return;
|
||||
m_ERRPreparedLoadMode = modeId;
|
||||
m_ERRPreparedLoadNonce = nonce;
|
||||
m_ERRLastLoadNonce = nonce;
|
||||
m_ERRPreparedLoadClock = System.GetTickCount();
|
||||
if (Replication.IsServer()) ERR_CasualtyLoadReady(modeId, nonce);
|
||||
else Rpc(ERR_CasualtyLoadReady, modeId, nonce);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_CasualtyLoadReady(RplId modeId, int nonce)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !mode.CasualtyLoading()) return;
|
||||
mode.CasualtyLoading().OwnerReady(this, nonce);
|
||||
}
|
||||
void ERR_CommitCasualtyLoad(RplId modeId, int nonce, RplId callerId, RplId actorId, RplId vehicleId, RplId ownerId, int slotId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (this == GetGame().GetPlayerController()) ERR_CommitCasualtyLoadOwner(modeId, nonce, callerId, actorId, vehicleId, ownerId, slotId);
|
||||
else Rpc(ERR_CommitCasualtyLoadOwner, modeId, nonce, callerId, actorId, vehicleId, ownerId, slotId);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
protected void ERR_CommitCasualtyLoadOwner(RplId modeId, int nonce, RplId callerId, RplId actorId, RplId vehicleId, RplId ownerId, int slotId)
|
||||
{
|
||||
bool lease = this == GetGame().GetPlayerController() && m_ERRPreparedLoadMode == modeId && m_ERRPreparedLoadNonce == nonce && ERR_CasualtyLoadingRules.Within(m_ERRPreparedLoadClock, System.GetTickCount(), 3000);
|
||||
if (m_ERRPreparedLoadMode == modeId && m_ERRPreparedLoadNonce == nonce) m_ERRPreparedLoadNonce = 0;
|
||||
// Consume the local lease before the native call so duplicates cannot act.
|
||||
bool accepted;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
IEntity actor = GetControlledEntity();
|
||||
IEntity caller = ERR_CasualtyLoadingRules.Entity(callerId);
|
||||
Vehicle vehicle = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(vehicleId));
|
||||
IEntity owner = ERR_CasualtyLoadingRules.Entity(ownerId);
|
||||
if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && vehicle == mode.CasualtyLoadingAircraft() && ERR_CasualtyLoadingRules.Nearby(mode, caller, actor) && owner)
|
||||
{
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(owner.FindComponent(BaseCompartmentManagerComponent));
|
||||
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
|
||||
BaseCompartmentSlot seat;
|
||||
if (manager) seat = manager.FindCompartment(slotId);
|
||||
if (access && ERR_CasualtyLoadingRules.TransportBench(seat, vehicle) && seat.GetOwner() == owner && !seat.GetOccupant() && seat.IsCompartmentAccessible() && !seat.IsGetInLockedFor(actor) && (!seat.IsReserved() || seat.IsReservedBy(actor)))
|
||||
accepted = access.GetInVehicle(owner, seat, true, -1, ECloseDoorAfterActions.INVALID, false);
|
||||
}
|
||||
// Missing replicated entities or rejected native entry fail once; never retry.
|
||||
if (Replication.IsServer()) ERR_CasualtyLoadResult(modeId, nonce, accepted);
|
||||
else Rpc(ERR_CasualtyLoadResult, modeId, nonce, accepted);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_CasualtyLoadResult(RplId modeId, int nonce, bool accepted)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode || ERR_CasualtyLoadingRules.EntityId(mode) != modeId || !mode.CasualtyLoading()) return;
|
||||
mode.CasualtyLoading().OwnerResult(this, nonce, accepted);
|
||||
}
|
||||
}
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
// First release supports only the registered Mi8 extraction aircraft's physical
|
||||
// child transport benches. Nothing changes health, ownership, or seat classes.
|
||||
class ERR_CasualtyLoadingRules
|
||||
{
|
||||
static int RoundAction(int phase)
|
||||
{
|
||||
if (phase == ERR_Phase.PREPARING) return 0;
|
||||
if (ERR_MissionRules.Terminal(phase)) return 2;
|
||||
return 1;
|
||||
}
|
||||
static bool ObserveTicket(bool committed, bool physical, bool authorized)
|
||||
{
|
||||
return physical && (committed || authorized);
|
||||
}
|
||||
static bool Within(int started, int now, int limit) { return now >= started && now - started < limit; }
|
||||
static bool SettledFor(int started, int observed, int now)
|
||||
{
|
||||
return started >= 0 && now >= started && now - started >= 5000 && Within(observed, now, 2000);
|
||||
}
|
||||
static bool Ranges(float callerToCasualty, float callerToAircraft, float casualtyToAircraft)
|
||||
{
|
||||
return callerToCasualty >= 0 && callerToCasualty <= 3 && callerToAircraft >= 0 && callerToAircraft <= 12 && casualtyToAircraft >= 0 && casualtyToAircraft <= 12;
|
||||
}
|
||||
static bool AircraftMotion(bool groundContact, float up, float speed, float angularSpeed)
|
||||
{
|
||||
return groundContact && up >= 0.85 && speed >= 0 && speed <= 0.5 && angularSpeed >= 0 && angularSpeed <= 0.1;
|
||||
}
|
||||
static RplId EntityId(IEntity entity)
|
||||
{
|
||||
if (!entity) return RplId.Invalid();
|
||||
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
if (!component) return RplId.Invalid();
|
||||
return component.Id();
|
||||
}
|
||||
static IEntity Entity(RplId id)
|
||||
{
|
||||
if (id == RplId.Invalid()) return null;
|
||||
RplComponent component = RplComponent.Cast(Replication.FindItem(id));
|
||||
if (!component) return null;
|
||||
return component.GetEntity();
|
||||
}
|
||||
static bool Down(IEntity actor)
|
||||
{
|
||||
if (!ERR_Actors.Alive(actor)) return false;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
|
||||
return controller && controller.GetLifeState() == ECharacterLifeState.INCAPACITATED;
|
||||
}
|
||||
static bool GroundSettled(IEntity actor)
|
||||
{
|
||||
if (!Down(actor) || CompartmentAccessComponent.GetVehicleIn(actor)) return false;
|
||||
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(actor.FindComponent(CharacterAnimationComponent));
|
||||
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
|
||||
Physics physics = actor.GetPhysics();
|
||||
return animation && !animation.PhysicsIsFalling() && physics && physics.GetVelocity().Length() <= 0.5 && access && !access.IsGettingIn() && !access.IsGettingOut();
|
||||
}
|
||||
static bool AircraftReady(Vehicle aircraft)
|
||||
{
|
||||
if (!ERR_Actors.Alive(aircraft)) return false;
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
|
||||
Physics physics = aircraft.GetPhysics();
|
||||
if (!controller || !controller.GetSimulation() || !physics) return false;
|
||||
vector transform[4]; aircraft.GetWorldTransform(transform);
|
||||
if (!AircraftMotion(true, transform[1][1], physics.GetVelocity().Length(), physics.GetAngularVelocity().Length())) return false;
|
||||
if (controller.GetSimulation().HasAnyGroundContact()) return true;
|
||||
return ERR_CasualtyGroundSupport.Parked(aircraft);
|
||||
}
|
||||
static bool LineOfSight(IEntity caller, IEntity casualty)
|
||||
{
|
||||
ChimeraCharacter rescuer = ChimeraCharacter.Cast(caller);
|
||||
ChimeraCharacter patient = ChimeraCharacter.Cast(casualty);
|
||||
if (!rescuer || !patient || !GetGame()) return false;
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = rescuer.AimingPosition();
|
||||
trace.End = patient.AimingPosition();
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = caller;
|
||||
if (GetGame().GetWorld().TraceMove(trace, null) >= 1) return true;
|
||||
// Hitting the intended body's clothing/attachment is still sight of that body.
|
||||
IEntity hit = trace.TraceEnt;
|
||||
while (hit)
|
||||
{
|
||||
if (hit == casualty) return true;
|
||||
hit = hit.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static bool Nearby(ERR_GameMode mode, IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty) return false;
|
||||
CharacterControllerComponent rescuer = CharacterControllerComponent.Cast(caller.FindComponent(CharacterControllerComponent));
|
||||
if (!ERR_Actors.CanFight(caller) || !rescuer || rescuer.GetLifeState() != ECharacterLifeState.ALIVE || !GroundSettled(casualty) || CompartmentAccessComponent.GetVehicleIn(caller)) return false;
|
||||
Vehicle aircraft = mode.CasualtyLoadingAircraft();
|
||||
if (!aircraft || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
|
||||
Faction faction = mode.PlayerFaction();
|
||||
return faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && LineOfSight(caller, casualty);
|
||||
}
|
||||
static bool TransportBench(BaseCompartmentSlot seat, Vehicle aircraft)
|
||||
{
|
||||
if (!seat || !aircraft || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) return false;
|
||||
IEntity owner = seat.GetOwner();
|
||||
if (!owner || owner == aircraft || !owner.GetPrefabData() || !owner.GetPrefabData().GetPrefabName().Contains("VehPart_MI8_seats")) return false;
|
||||
while (owner)
|
||||
{
|
||||
if (owner == aircraft) return true;
|
||||
owner = owner.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
class ERR_CasualtySettling
|
||||
{
|
||||
IEntity Actor;
|
||||
UUID Account;
|
||||
int Connection;
|
||||
int Since;
|
||||
int Observed;
|
||||
}
|
||||
class ERR_CasualtyLoadTicket
|
||||
{
|
||||
IEntity Caller;
|
||||
IEntity Casualty;
|
||||
Vehicle Aircraft;
|
||||
BaseCompartmentSlot Seat;
|
||||
UUID CallerAccount;
|
||||
UUID CasualtyAccount;
|
||||
int CallerConnection;
|
||||
int CasualtyConnection;
|
||||
int Nonce;
|
||||
int Started;
|
||||
int CommittedAt;
|
||||
bool Committed;
|
||||
bool OwnerReported;
|
||||
}
|
||||
class ERR_CasualtyLoadingService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected bool m_Stopped;
|
||||
protected int m_NextNonce;
|
||||
protected bool m_MaintenanceQueued;
|
||||
protected ref array<ref ERR_CasualtySettling> m_Settling = {};
|
||||
protected ref array<ref ERR_CasualtyLoadTicket> m_Pending = {};
|
||||
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
|
||||
void ERR_CasualtyLoadingService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
ScriptInvoker OnResolved() { return m_OnResolved; } // (IEntity casualty, bool physicallyLoaded)
|
||||
int PendingCount() { return m_Pending.Count(); }
|
||||
protected ERR_PlayerLife CurrentLife(IEntity actor)
|
||||
{
|
||||
if (!m_Mode || !m_Mode.Players()) return null;
|
||||
return m_Mode.Players().CurrentActor(actor);
|
||||
}
|
||||
protected bool SameLife(IEntity actor, UUID account, int connection)
|
||||
{
|
||||
ERR_PlayerLife life = CurrentLife(actor);
|
||||
return life && life.AccountId == account && life.ConnectionId == connection;
|
||||
}
|
||||
protected bool Settled(IEntity actor, int now)
|
||||
{
|
||||
foreach (ERR_CasualtySettling sample : m_Settling)
|
||||
if (sample.Actor == actor && SameLife(actor, sample.Account, sample.Connection)) return ERR_CasualtyLoadingRules.GroundSettled(actor) && ERR_CasualtyLoadingRules.SettledFor(sample.Since, sample.Observed, now);
|
||||
return false;
|
||||
}
|
||||
protected void Observe(int now)
|
||||
{
|
||||
for (int index = m_Settling.Count() - 1; index >= 0; index--)
|
||||
{
|
||||
ERR_CasualtySettling sample = m_Settling[index];
|
||||
if (!SameLife(sample.Actor, sample.Account, sample.Connection) || !ERR_CasualtyLoadingRules.GroundSettled(sample.Actor) || !ERR_CasualtyLoadingRules.Within(sample.Observed, now, 2000)) m_Settling.Remove(index);
|
||||
else sample.Observed = now;
|
||||
}
|
||||
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
{
|
||||
if (!CurrentLife(life.Actor) || !ERR_CasualtyLoadingRules.GroundSettled(life.Actor)) continue;
|
||||
bool found;
|
||||
foreach (ERR_CasualtySettling tracked : m_Settling) if (tracked.Actor == life.Actor) { found = true; break; }
|
||||
if (found) continue;
|
||||
ERR_CasualtySettling added = new ERR_CasualtySettling();
|
||||
added.Actor = life.Actor; added.Account = life.AccountId; added.Connection = life.ConnectionId; added.Since = now; added.Observed = now;
|
||||
m_Settling.Insert(added);
|
||||
}
|
||||
}
|
||||
protected bool PendingActor(IEntity caller, IEntity casualty)
|
||||
{
|
||||
foreach (ERR_CasualtyLoadTicket pending : m_Pending)
|
||||
if (pending.Caller == caller || pending.Casualty == casualty) return true;
|
||||
return false;
|
||||
}
|
||||
protected BaseCompartmentSlot AvailableSeat(IEntity casualty, Vehicle aircraft)
|
||||
{
|
||||
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
{
|
||||
if (!ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) || seat.GetOccupant() || seat.IsReserved() || !seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(casualty)) continue;
|
||||
bool pendingSeat;
|
||||
foreach (ERR_CasualtyLoadTicket pending : m_Pending) if (pending.Seat == seat) { pendingSeat = true; break; }
|
||||
if (!pendingSeat) return seat;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
bool CanLoad(IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !ERR_CasualtyLoadingRules.Nearby(m_Mode, caller, casualty)) return false;
|
||||
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
|
||||
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null;
|
||||
}
|
||||
bool Begin(IEntity caller, IEntity casualty)
|
||||
{
|
||||
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
|
||||
ERR_PlayerLife callerLife = CurrentLife(caller);
|
||||
ERR_PlayerLife casualtyLife = CurrentLife(casualty);
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(casualtyLife.ConnectionId));
|
||||
if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(m_Mode.ExtractionVehicle()) == RplId.Invalid()) return false;
|
||||
ERR_CasualtyLoadTicket ticket = new ERR_CasualtyLoadTicket();
|
||||
ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = m_Mode.ExtractionVehicle();
|
||||
ticket.Seat = AvailableSeat(casualty, ticket.Aircraft);
|
||||
if (!ticket.Seat || ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()) == RplId.Invalid()) return false;
|
||||
ticket.CallerAccount = callerLife.AccountId; ticket.CasualtyAccount = casualtyLife.AccountId;
|
||||
ticket.CallerConnection = callerLife.ConnectionId; ticket.CasualtyConnection = casualtyLife.ConnectionId;
|
||||
ticket.Started = System.GetTickCount(); ticket.Nonce = ++m_NextNonce;
|
||||
m_Pending.Insert(ticket);
|
||||
ticket.Seat.SetReserved(casualty);
|
||||
if (!ticket.Seat.IsReservedBy(casualty)) { Resolve(ticket, false); return false; }
|
||||
ScheduleMaintenance();
|
||||
// Preparation never moves the actor. Its owner starts a local real-time lease.
|
||||
controller.ERR_PrepareCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), ticket.Nonce);
|
||||
return true;
|
||||
}
|
||||
protected ERR_CasualtyLoadTicket Ticket(SCR_PlayerController controller, int nonce)
|
||||
{
|
||||
if (!controller || !GetGame() || !GetGame().GetPlayerManager() || GetGame().GetPlayerManager().GetPlayerController(controller.GetPlayerId()) != controller) return null;
|
||||
foreach (ERR_CasualtyLoadTicket ticket : m_Pending)
|
||||
if (ticket.Nonce == nonce && ticket.CasualtyConnection == controller.GetPlayerId() && ticket.Casualty == controller.GetControlledEntity()) return ticket;
|
||||
return null;
|
||||
}
|
||||
void OwnerReady(SCR_PlayerController controller, int nonce)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) return;
|
||||
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
|
||||
if (!ticket || ticket.Committed) return;
|
||||
int now = System.GetTickCount();
|
||||
if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; }
|
||||
ticket.Committed = true; ticket.CommittedAt = now;
|
||||
// Final authority/physical validation is the commit boundary. No retries.
|
||||
controller.ERR_CommitCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), nonce, ERR_CasualtyLoadingRules.EntityId(ticket.Caller), ERR_CasualtyLoadingRules.EntityId(ticket.Casualty), ERR_CasualtyLoadingRules.EntityId(ticket.Aircraft), ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()), ticket.Seat.GetCompartmentSlotID());
|
||||
}
|
||||
void OwnerResult(SCR_PlayerController controller, int nonce, bool accepted)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) return;
|
||||
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
|
||||
if (!ticket || !ticket.Committed || ticket.OwnerReported) return;
|
||||
ticket.OwnerReported = true;
|
||||
if (!accepted) Print("Rereintroduced [casualty]: owner declined loading; observing physical outcome until expiry.");
|
||||
// Neither positive nor negative client acknowledgements release a reserved
|
||||
// seat while an authorized owner command could still be in flight.
|
||||
}
|
||||
protected bool PhysicalTicket(ERR_CasualtyLoadTicket ticket)
|
||||
{
|
||||
// Death cannot revoke an in-flight commit. Keep the vacant seat reserved
|
||||
// while the captured body exists; its owner may still see the prior state.
|
||||
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
|
||||
}
|
||||
protected bool ValidTicket(ERR_CasualtyLoadTicket ticket)
|
||||
{
|
||||
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
|
||||
}
|
||||
protected void Resolve(ERR_CasualtyLoadTicket ticket, bool success)
|
||||
{
|
||||
int index = m_Pending.Find(ticket); if (index < 0) return;
|
||||
m_Pending.Remove(index);
|
||||
if (ticket.Seat && ticket.Casualty && ticket.Seat.IsReservedBy(ticket.Casualty)) ticket.Seat.SetReserved(null);
|
||||
if (m_Pending.IsEmpty()) CancelMaintenance();
|
||||
m_OnResolved.Invoke(ticket.Casualty, success);
|
||||
}
|
||||
void Tick(float worldNow)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Mode) return;
|
||||
int roundAction = ERR_CasualtyLoadingRules.RoundAction(m_Mode.Phase());
|
||||
if (roundAction == 0) return;
|
||||
if (roundAction == 2) { Stop(); return; }
|
||||
int now = System.GetTickCount();
|
||||
Observe(now);
|
||||
MaintainPending();
|
||||
}
|
||||
protected void ScheduleMaintenance()
|
||||
{
|
||||
if (m_MaintenanceQueued || m_Stopped || m_Pending.IsEmpty() || !GetGame()) return;
|
||||
// Only ticket observation runs independently of player population. No owner
|
||||
// request is retried and neither local nor server leases are refreshed.
|
||||
m_MaintenanceQueued = true;
|
||||
GetGame().GetCallqueue().CallLater(MaintainPending, 250, true);
|
||||
}
|
||||
protected void CancelMaintenance()
|
||||
{
|
||||
if (!m_MaintenanceQueued) return;
|
||||
m_MaintenanceQueued = false;
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(MaintainPending);
|
||||
}
|
||||
protected void MaintainPending()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) { CancelMaintenance(); return; }
|
||||
if (!m_Mode || ERR_MissionRules.Terminal(m_Mode.Phase())) { Stop(); return; }
|
||||
if (m_Pending.IsEmpty()) { CancelMaintenance(); return; }
|
||||
int now = System.GetTickCount();
|
||||
array<ref ERR_CasualtyLoadTicket> snapshot = {}; foreach (ERR_CasualtyLoadTicket item : m_Pending) snapshot.Insert(item);
|
||||
foreach (ERR_CasualtyLoadTicket ticket : snapshot)
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
// Commit authorizes one move of the captured body; a later caller/target
|
||||
// disconnect cannot revoke that command or release its seat prematurely.
|
||||
if (!ERR_CasualtyLoadingRules.ObserveTicket(ticket.Committed, PhysicalTicket(ticket), ValidTicket(ticket))) { Resolve(ticket, false); continue; }
|
||||
bool aboard = CompartmentAccessComponent.GetVehicleIn(ticket.Casualty) == ticket.Aircraft && ticket.Seat.GetOccupant() == ticket.Casualty;
|
||||
if (aboard && ticket.Committed) { Resolve(ticket, ERR_Actors.Alive(ticket.Casualty)); continue; }
|
||||
if (ticket.Committed)
|
||||
{
|
||||
// Owner's prepare-to-commit lease is only 3s; this 8s observation budget
|
||||
// leaves room for replication. Timeouts do not issue compensating teleports.
|
||||
if (!ERR_CasualtyLoadingRules.Within(ticket.CommittedAt, now, 8000)) Resolve(ticket, false);
|
||||
}
|
||||
else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty)) Resolve(ticket, false);
|
||||
}
|
||||
}
|
||||
void RosterChanged(int connection) { if (GetGame()) Tick(GetGame().GetWorld().GetWorldTime()); }
|
||||
void Stop()
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) return;
|
||||
m_Stopped = true; m_Settling.Clear();
|
||||
CancelMaintenance();
|
||||
while (!m_Pending.IsEmpty()) Resolve(m_Pending[m_Pending.Count() - 1], false);
|
||||
}
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
// A first clue identifies a stable search square, not the exact objective.
|
||||
// Stable grid centers prevent repeated updates from triangulating the site.
|
||||
class ERR_DiscoveryMapRules
|
||||
{
|
||||
static int Coordinate(float coordinate, int knowledge)
|
||||
{
|
||||
if (knowledge >= 2) return Math.Round(coordinate);
|
||||
return Math.Floor(coordinate / 500.0) * 500 + 250;
|
||||
}
|
||||
}
|
||||
|
||||
// Discovery markers are authored objective information, not player annotations.
|
||||
// Native local insertion overwrites owner ID before OnCreateMarker; clear it at
|
||||
// that boundary so native edit/drag cannot move a discovered objective.
|
||||
class ERR_DiscoveryMarker : SCR_MapMarkerBase
|
||||
{
|
||||
void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); }
|
||||
void SetDiscoveryPosition(vector position, int knowledge)
|
||||
{
|
||||
SetWorldPos(ERR_DiscoveryMapRules.Coordinate(position[0], knowledge), ERR_DiscoveryMapRules.Coordinate(position[2], knowledge));
|
||||
}
|
||||
override void OnCreateMarker(bool skipProfanityFilter = false)
|
||||
{
|
||||
SetMarkerOwnerID(-1);
|
||||
// Text is scenario-authored, never user input. Avoid delayed UGC callbacks
|
||||
// overwriting a newer discovery label after the location becomes identified.
|
||||
super.OnCreateMarker(true);
|
||||
}
|
||||
override void OnDelete()
|
||||
{
|
||||
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
|
||||
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
|
||||
super.OnDelete();
|
||||
m_wRoot = null;
|
||||
m_MarkerWidgetComp = null;
|
||||
}
|
||||
override void SetCustomText(string text)
|
||||
{
|
||||
super.SetCustomText(text);
|
||||
// Native SetCustomText updates data only; an already-open map needs its
|
||||
// existing widget updated too. Closed maps use the saved text on creation.
|
||||
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
|
||||
if (GetRootWidget() && widget) widget.SetText(text, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Vanilla local deletion removes only its active list. A culled local objective
|
||||
// must also leave the disabled list or map movement can resurrect it later.
|
||||
modded class SCR_MapMarkerManagerComponent
|
||||
{
|
||||
void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker marker)
|
||||
{
|
||||
if (!marker || marker.GetMarkerID() != -1) return;
|
||||
m_aStaticMarkers.RemoveItem(marker);
|
||||
m_aDisabledMarkers.RemoveItem(marker);
|
||||
marker.OnDelete();
|
||||
}
|
||||
}
|
||||
|
||||
// Read replicated discovery locally. A server-wide marker list never leaks
|
||||
// locations which have not been recovered through gameplay.
|
||||
modded class ERR_LocationComponent
|
||||
{
|
||||
protected ref ERR_DiscoveryMarker m_ERRMarker;
|
||||
protected int m_ERRShown;
|
||||
protected string m_ERRShownLabel;
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (RplSession.Mode() != RplMode.Dedicated && !SCR_Global.IsEditMode())
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateMarker, 1500, true);
|
||||
}
|
||||
protected void ERR_UpdateMarker()
|
||||
{
|
||||
if (!GetGame().GetPlayerController() || Knowledge() <= 0 || (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)";
|
||||
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 label = Id();
|
||||
}
|
||||
if (!m_ERRMarker)
|
||||
{
|
||||
m_ERRMarker = new ERR_DiscoveryMarker();
|
||||
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
|
||||
m_ERRMarker.SetMarkerConfigID(0);
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
manager.InsertStaticMarker(m_ERRMarker, true);
|
||||
}
|
||||
m_ERRMarker.SetDiscoveryPosition(Position(), 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.
|
||||
SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance();
|
||||
if (mapEntity && mapEntity.IsOpen()) manager.SetStaticMarkerDisabled(m_ERRMarker, false);
|
||||
m_ERRShown = Knowledge();
|
||||
m_ERRShownLabel = Label();
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateMarker);
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (m_ERRMarker)
|
||||
{
|
||||
if (manager) manager.ERR_RemoveDiscoveryMarker(m_ERRMarker);
|
||||
else m_ERRMarker.OnDelete();
|
||||
}
|
||||
m_ERRMarker = null;
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Base mission discovery: recovering physical documents works without companions.
|
||||
// The Intel addon optionally assigns extra documents to enemy inventories.
|
||||
class ERR_DiscoveryService
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref set<IEntity> m_Spent = new set<IEntity>();
|
||||
void ERR_DiscoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||
static void Folders(InventoryStorageManagerComponent inventory, out array<IEntity> folders)
|
||||
{
|
||||
folders.Clear();
|
||||
SCR_PrefabNamePredicate predicate = new SCR_PrefabNamePredicate();
|
||||
predicate.prefabName = ERR_Resources.IntelFolder();
|
||||
array<IEntity> intel = {};
|
||||
inventory.FindItems(intel, predicate, EStoragePurpose.PURPOSE_ANY);
|
||||
foreach (IEntity folder : intel)
|
||||
if (folder && !folders.Contains(folder)) folders.Insert(folder);
|
||||
array<IEntity> objectives = {};
|
||||
predicate.prefabName = ERR_Resources.ObjectiveDocument();
|
||||
inventory.FindItems(objectives, predicate, EStoragePurpose.PURPOSE_ANY);
|
||||
foreach (IEntity objective : objectives)
|
||||
if (objective && !folders.Contains(objective)) folders.Insert(objective);
|
||||
}
|
||||
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
foreach (IEntity player : players) Recover(player);
|
||||
}
|
||||
protected void Recover(IEntity player)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Mode || !m_Mode.Players() || !m_Mode.Players().CurrentActor(player) || !ERR_Actors.Alive(player)) return;
|
||||
if (!m_Mode.PlayerFaction() || SCR_FactionManager.SGetFaction(player) != m_Mode.PlayerFaction()) return;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) return;
|
||||
array<IEntity> folders = {};
|
||||
Folders(inventory, folders);
|
||||
foreach (IEntity folder : folders)
|
||||
{
|
||||
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
array<ERR_LocationComponent> eligible = {};
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
if (!location || location.Knowledge() >= 2) 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;
|
||||
ERR_LocationComponent chosen = eligible.GetRandomElement();
|
||||
if (!chosen.Discover(Math.RandomInt(0, 100), m_Mode.Settings().Get("intelRevealChancePercent"))) continue;
|
||||
m_Spent.Insert(folder);
|
||||
PrintFormat("Rereintroduced [intel]: player %1 recovered folder; location=%2 knowledge=%3", GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(player), chosen.Id(), chosen.Knowledge());
|
||||
}
|
||||
}
|
||||
|
||||
void Stop() { m_Spent.Clear(); }
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
[ComponentEditorProps(category: "Rereintroduced", description: "Physical document identifying one authored objective")]
|
||||
class ERR_DocumentComponentClass : ScriptComponentClass {}
|
||||
class ERR_DocumentComponent : ScriptComponent
|
||||
{
|
||||
protected string m_TargetId;
|
||||
void SetTarget(string id) { if (Replication.IsServer()) m_TargetId = id; }
|
||||
string TargetId() { return m_TargetId; }
|
||||
}
|
||||
+164
@@ -0,0 +1,164 @@
|
||||
// The server applies optional date/weather choices once at round preparation.
|
||||
// Zero values preserve the world's authored environment and existing defaults.
|
||||
class ERR_EnvironmentService
|
||||
{
|
||||
protected TimeAndWeatherManagerEntity m_Manager;
|
||||
protected bool m_Started;
|
||||
protected bool m_Stopped;
|
||||
protected int m_DateCode;
|
||||
protected int m_Profile;
|
||||
|
||||
static string Pad2(int value)
|
||||
{
|
||||
if (value < 10) return "0" + value.ToString();
|
||||
return value.ToString();
|
||||
}
|
||||
static string Date(int code)
|
||||
{
|
||||
if (code == 0) return "authored";
|
||||
if (!ERR_Settings.Valid("startDateCode", code)) return "unavailable";
|
||||
int year = 1900 + code / 10000;
|
||||
int month = (code % 10000) / 100;
|
||||
int day = code % 100;
|
||||
return year.ToString() + "-" + Pad2(month) + "-" + Pad2(day);
|
||||
}
|
||||
static bool ParseDate(string text, out int code)
|
||||
{
|
||||
if (text == "authored") { code = 0; return true; }
|
||||
if (text.Length() != 10 || text.Substring(4, 1) != "-" || text.Substring(7, 1) != "-") return false;
|
||||
int year, month, day;
|
||||
if (!ERR_CommandArguments.Integer(text.Substring(0, 4), year) || !ERR_CommandArguments.Integer(text.Substring(5, 2), month) || !ERR_CommandArguments.Integer(text.Substring(8, 2), day)) return false;
|
||||
if (year < 1900 || year > 2050 || month < 1 || month > 12 || day < 1 || day > 31) return false;
|
||||
int parsed = (year - 1900) * 10000 + month * 100 + day;
|
||||
if (!ERR_Settings.Valid("startDateCode", parsed)) return false;
|
||||
code = parsed;
|
||||
return true;
|
||||
}
|
||||
static string ProfileName(int profile)
|
||||
{
|
||||
if (profile == 0) return "authored";
|
||||
if (profile == 1) return "clear";
|
||||
if (profile == 2) return "overcast";
|
||||
if (profile == 3) return "rain";
|
||||
return "unavailable";
|
||||
}
|
||||
static string WeatherStateName(int profile)
|
||||
{
|
||||
if (profile == 1) return "Clear";
|
||||
if (profile == 2) return "Overcast";
|
||||
if (profile == 3) return "Rainy";
|
||||
return "";
|
||||
}
|
||||
static bool ParseProfile(string text, out int profile)
|
||||
{
|
||||
if (text == "authored") { profile = 0; return true; }
|
||||
if (text == "clear") { profile = 1; return true; }
|
||||
if (text == "overcast") { profile = 2; return true; }
|
||||
if (text == "rain" || text == "rainy") { profile = 3; return true; }
|
||||
return false;
|
||||
}
|
||||
static bool HasWeatherState(TimeAndWeatherManagerEntity manager, string name)
|
||||
{
|
||||
if (!manager || name.IsEmpty()) return false;
|
||||
array<ref WeatherState> states = {};
|
||||
manager.GetWeatherStatesList(states);
|
||||
foreach (WeatherState state : states)
|
||||
if (state && state.GetStateName() == name) return true;
|
||||
return false;
|
||||
}
|
||||
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"));
|
||||
}
|
||||
protected bool StartOn(TimeAndWeatherManagerEntity manager, int dateCode, int profile)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) return false;
|
||||
if (m_Started) return true;
|
||||
if (!manager || !ERR_Settings.Valid("startDateCode", dateCode) || !ERR_Settings.Valid("weatherProfile", profile)) return false;
|
||||
int year = 1900 + dateCode / 10000;
|
||||
int month = (dateCode % 10000) / 100;
|
||||
int day = dateCode % 100;
|
||||
if (dateCode != 0 && !manager.CheckValidDate(year, month, day)) return false;
|
||||
string weather = WeatherStateName(profile);
|
||||
BaseWeatherStateTransitionManager transitions = manager.GetTransitionManager();
|
||||
if (profile != 0 && (!transitions || !manager.GetCurrentWeatherState() || !HasWeatherState(manager, weather))) return false;
|
||||
int previousYear, previousMonth, previousDay;
|
||||
manager.GetDate(previousYear, previousMonth, previousDay);
|
||||
if (dateCode != 0 && !manager.SetDate(year, month, day, true)) return false;
|
||||
if (profile != 0)
|
||||
{
|
||||
string previousWeather = manager.GetCurrentWeatherState().GetStateName();
|
||||
bool previousLooping = manager.IsWeatherLooping();
|
||||
// Use the native replicated weather route, never local editor previews.
|
||||
// Loop the selected family; native variants/wind still follow vanilla rules.
|
||||
manager.ForceWeatherTo(true, weather, 0, 24);
|
||||
WeatherState current = manager.GetCurrentWeatherState();
|
||||
WeatherState next = transitions.GetNextState();
|
||||
if (!manager.IsWeatherLooping() || ((!current || current.GetStateName() != weather) && (!next || next.GetStateName() != weather)))
|
||||
{
|
||||
manager.ForceWeatherTo(previousLooping, previousWeather, 0, 24);
|
||||
if (dateCode != 0) manager.SetDate(previousYear, previousMonth, previousDay, true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
m_Manager = manager;
|
||||
m_DateCode = dateCode;
|
||||
m_Profile = profile;
|
||||
m_Started = true;
|
||||
PrintFormat("Rereintroduced [environment]: date=%1 weather=%2", Date(dateCode), ProfileName(profile));
|
||||
return true;
|
||||
}
|
||||
bool Started() { return m_Started && !m_Stopped; }
|
||||
int DateCode() { return m_DateCode; }
|
||||
int Profile() { return m_Profile; }
|
||||
int CurrentDateCode()
|
||||
{
|
||||
if (!Started() || !m_Manager) return -1;
|
||||
return (m_Manager.GetYear() - 1900) * 10000 + m_Manager.GetMonth() * 100 + m_Manager.GetDay();
|
||||
}
|
||||
string CurrentWeather()
|
||||
{
|
||||
if (!Started() || !m_Manager) return "unavailable";
|
||||
WeatherState weather = m_Manager.GetCurrentWeatherState();
|
||||
if (!weather) return "unavailable";
|
||||
return weather.GetStateName();
|
||||
}
|
||||
string Status()
|
||||
{
|
||||
if (!Started()) return "Environment not started.";
|
||||
return "Current date " + Date(CurrentDateCode()) + ", weather " + CurrentWeather() + " (round profile " + ProfileName(m_Profile) + ").";
|
||||
}
|
||||
void Stop() { m_Stopped = true; m_Manager = null; }
|
||||
}
|
||||
|
||||
// Called after the server-owned admin authorization gate. Saving cannot change
|
||||
// the active service; accepted changes are consumed by the next mission instance.
|
||||
class ERR_EnvironmentCommands
|
||||
{
|
||||
static bool Execute(ERR_Settings settings, ERR_EnvironmentService environment, array<string> parts, array<string> replies)
|
||||
{
|
||||
if (!Replication.IsServer() || !settings || parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "date" && parts[1] != "weather")) return false;
|
||||
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")) + ".");
|
||||
return true;
|
||||
}
|
||||
int value;
|
||||
bool valid;
|
||||
string key = "startDateCode";
|
||||
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);
|
||||
}
|
||||
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.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
[BaseContainerProps()]
|
||||
class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
{
|
||||
protected ERR_PersonalEscapeResult PersonalResult(int playerId)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller || controller.GetPlayerId() != playerId) return ERR_PersonalEscapeResult.UNKNOWN;
|
||||
return controller.ERR_EscapeResult();
|
||||
}
|
||||
override string GetTitle(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
string title = "The helicopter escaped";
|
||||
if (PersonalResult(playerId) == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon";
|
||||
#ifdef WORKBENCH
|
||||
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
|
||||
#endif
|
||||
return title;
|
||||
}
|
||||
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
ERR_PersonalEscapeResult result = PersonalResult(playerId);
|
||||
if (result == ERR_PersonalEscapeResult.ESCAPED)
|
||||
return "You crossed the extraction boundary aboard the helicopter.";
|
||||
if (result == ERR_PersonalEscapeResult.MISSED) return "You did not escape with the helicopter.";
|
||||
return "Your personal escape result is not available.";
|
||||
}
|
||||
}
|
||||
|
||||
// End data and the controller replicate independently. Refresh an already-open
|
||||
// native result screen when the identity-derived controller result arrives.
|
||||
modded class SCR_GameOverScreenContentUIComponent
|
||||
{
|
||||
protected bool m_ERREscapeContent;
|
||||
override void InitContent(SCR_GameOverScreenUIContentData endScreenUIContent)
|
||||
{
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
|
||||
super.InitContent(endScreenUIContent);
|
||||
m_ERREscapeContent = ERR_EscapeGameOverScreenInfo.Cast(endScreenUIContent.m_GameOverInfo) != null;
|
||||
if (!m_ERREscapeContent) return;
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Insert(ERR_RefreshEscapeContent);
|
||||
ERR_RefreshEscapeContent();
|
||||
}
|
||||
protected void ERR_RefreshEscapeContent()
|
||||
{
|
||||
if (!m_ERREscapeContent || !m_wRoot) return;
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
int playerId = -1;
|
||||
if (controller) playerId = controller.GetPlayerId();
|
||||
ERR_EscapeGameOverScreenInfo info = new ERR_EscapeGameOverScreenInfo();
|
||||
TextWidget title = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sTileName));
|
||||
TextWidget debrief = TextWidget.Cast(m_wRoot.FindAnyWidget(m_sDebriefingName));
|
||||
if (title) title.SetText(info.GetTitle(playerId, null));
|
||||
if (debrief) debrief.SetText(info.GetDebriefing(playerId, null));
|
||||
}
|
||||
override void HandlerDeattached(Widget w)
|
||||
{
|
||||
SCR_PlayerController.ERR_OnEscapeResultChanged().Remove(ERR_RefreshEscapeContent);
|
||||
m_ERREscapeContent = false;
|
||||
super.HandlerDeattached(w);
|
||||
}
|
||||
}
|
||||
|
||||
[BaseContainerProps()]
|
||||
class ERR_FailureGameOverScreenInfo : SCR_BaseGameOverScreenInfo
|
||||
{
|
||||
override string GetDebriefing(int playerId, array<int> otherPlayerIds)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && !mode.EndReason().IsEmpty()) return mode.EndReason();
|
||||
return "No surviving prisoners can complete the escape, or the extraction helicopter has been lost.";
|
||||
}
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
// Initial preparation only. A newly spawned vehicle may start asleep above its
|
||||
// landing gear contact plane. Wake once, then observe native gravity/contact;
|
||||
// never correct its transform, apply an impulse, or replenish its supplies.
|
||||
class ERR_ExtractionPlacement
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected bool m_Stopped;
|
||||
protected int m_WakeCount;
|
||||
protected float m_Deadline;
|
||||
protected float m_StableSince = -1;
|
||||
protected float m_LastObservation = -1;
|
||||
protected float m_NextDiagnostic;
|
||||
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
|
||||
protected string m_Failure;
|
||||
void ERR_ExtractionPlacement(ERR_GameMode mode, Vehicle aircraft)
|
||||
{
|
||||
m_Mode = mode; m_Aircraft = aircraft;
|
||||
m_Deadline = GetGame().GetWorld().GetWorldTime() + 30000;
|
||||
}
|
||||
static bool CanWake(bool initialized, int count, float now, float deadline)
|
||||
{
|
||||
return initialized && count == 0 && now < deadline;
|
||||
}
|
||||
static bool Fresh(float previous, float now)
|
||||
{
|
||||
return previous >= 0 && now >= previous && now - previous <= 1500;
|
||||
}
|
||||
static bool QualificationAllowed(bool previouslyReady, float now, float deadline)
|
||||
{
|
||||
return previouslyReady || now < deadline;
|
||||
}
|
||||
static bool Stable(float since, float previous, float now)
|
||||
{
|
||||
return since >= 0 && previous >= since && now >= previous && now - previous <= 1500 && now - since >= 1000;
|
||||
}
|
||||
int WakeCount() { return m_WakeCount; }
|
||||
bool Ready() { return !m_Stopped && m_Result == ERR_PrepareResult.READY; }
|
||||
string FailureReason() { return m_Failure; }
|
||||
void Stop() { m_Stopped = true; }
|
||||
protected ERR_PrepareResult Fail(string reason)
|
||||
{
|
||||
m_Failure = reason; m_Result = ERR_PrepareResult.FAILED;
|
||||
Print("Rereintroduced [extraction placement]: " + reason, LogLevel.ERROR);
|
||||
return m_Result;
|
||||
}
|
||||
protected void Diagnostic(bool ready)
|
||||
{
|
||||
if (!m_Aircraft) return;
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
vector entity[4]; m_Aircraft.GetWorldTransform(entity);
|
||||
PrintFormat("ERR EXTRACTION PLACEMENT: wakes=%1 alive=%2 ready=%3 origin=%4 up=%5", m_WakeCount, ERR_Actors.Alive(m_Aircraft), ready, entity[3], entity[1][1]);
|
||||
if (physics) PrintFormat("ERR EXTRACTION PHYSICS: dynamic=%1 awake=%2 speed=%3 angularSpeed=%4", physics.IsDynamic(), physics.IsActive(), physics.GetVelocity().Length(), physics.GetAngularVelocity().Length());
|
||||
if (controller && controller.GetSimulation())
|
||||
{
|
||||
vector nativeTransform[4]; controller.GetSimulation().GetWorldTransform(nativeTransform);
|
||||
PrintFormat("ERR EXTRACTION NATIVE: valid=%1 contact=%2 engine=%3 origin=%4 up=%5 entityDifference=%6", controller.GetSimulation().IsValid(), controller.GetSimulation().HasAnyGroundContact(), controller.GetSimulation().EngineIsOn(), nativeTransform[3], nativeTransform[1][1], vector.Distance(nativeTransform[3], entity[3]));
|
||||
}
|
||||
// Diagnostics read every wheel even while physics is awake. The strict parked
|
||||
// fallback itself still requires sleep and never alters the vehicle.
|
||||
vector front, left, right;
|
||||
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_F", front, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, front, 0.30, true);
|
||||
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_bL", left, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, left, 0.45, true);
|
||||
if (ERR_CasualtyGroundSupport.WheelCenter(m_Aircraft, "v_wheel_bR", right, true)) ERR_CasualtyGroundSupport.WheelSupported(m_Aircraft, right, 0.45, true);
|
||||
}
|
||||
ERR_PrepareResult Poll(float now)
|
||||
{
|
||||
if (m_Stopped || m_Result == ERR_PrepareResult.FAILED) return ERR_PrepareResult.FAILED;
|
||||
if (!Replication.IsServer() || !m_Mode || m_Mode.Phase() != ERR_Phase.PREPARING || m_Mode.ExtractionVehicle() != m_Aircraft) return Fail("initial aircraft ownership/phase changed");
|
||||
if (!ERR_Actors.Alive(m_Aircraft) || !m_Aircraft.GetPrefabData() || !m_Aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return Fail("registered initial Mi8 is missing, destroyed or unsupported");
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
bool initialized = physics && physics.IsDynamic() && controller && controller.GetSimulation() && controller.GetSimulation().IsValid();
|
||||
if (CanWake(initialized, m_WakeCount, now, m_Deadline))
|
||||
{
|
||||
// Production preparation owns the empty registered aircraft. The count is
|
||||
// latched BEFORE the native call, preventing repeated wakes on any callback.
|
||||
m_WakeCount = 1;
|
||||
physics.SetActive(ActiveState.ACTIVE);
|
||||
m_LastObservation = now;
|
||||
Diagnostic(false);
|
||||
m_NextDiagnostic = now + 1000;
|
||||
return ERR_PrepareResult.PENDING; // never accept in the mutation frame
|
||||
}
|
||||
FuelManagerComponent fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||
bool ready = initialized && m_WakeCount == 1 && fuel && fuel.HasFuel() && ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
|
||||
if (now >= m_NextDiagnostic)
|
||||
{
|
||||
Diagnostic(ready);
|
||||
PrintFormat("ERR EXTRACTION FUEL: manager=%1 hasFuel=%2", fuel != null, fuel && fuel.HasFuel());
|
||||
m_NextDiagnostic = now + 1000;
|
||||
}
|
||||
if (!ready || !Fresh(m_LastObservation, now)) m_StableSince = -1;
|
||||
if (ready)
|
||||
{
|
||||
if (m_StableSince < 0) m_StableSince = now;
|
||||
if (Stable(m_StableSince, m_LastObservation, now))
|
||||
{
|
||||
if (!QualificationAllowed(m_Result == ERR_PrepareResult.READY, now, m_Deadline)) return Fail("initial aircraft stable qualification missed the 30-second preparation deadline");
|
||||
if (m_Result != ERR_PrepareResult.READY) Print("Rereintroduced [extraction placement]: native supported aircraft stable for one second, fuel present, exactly one initial wake");
|
||||
m_Result = ERR_PrepareResult.READY;
|
||||
m_LastObservation = now;
|
||||
return m_Result;
|
||||
}
|
||||
}
|
||||
m_Result = ERR_PrepareResult.PENDING;
|
||||
m_LastObservation = now;
|
||||
if (now >= m_Deadline) return Fail("initial aircraft did not become alive, fueled and physically supported/stable within 30 seconds; no replacement or repositioning requested");
|
||||
return m_Result;
|
||||
}
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
enum ERR_ExtractionState { WAITING, ESCAPED, LOST }
|
||||
|
||||
// Mission progression consumes the result; this service never spawns a vehicle.
|
||||
class ERR_ExtractionService
|
||||
{
|
||||
static const int REQUIRED_SEATS = 12;
|
||||
protected ref ERR_PlayerRoster m_Roster;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected bool m_Assigned;
|
||||
protected bool m_Begun;
|
||||
protected bool m_Stopped;
|
||||
protected vector m_Center;
|
||||
protected float m_Radius;
|
||||
protected ERR_ExtractionState m_State;
|
||||
protected ref set<UUID> m_Escapees = new set<UUID>();
|
||||
protected int m_ParticipantCount;
|
||||
protected ref ScriptInvoker m_OnResolved = new ScriptInvoker();
|
||||
|
||||
void ERR_ExtractionService(ERR_PlayerRoster roster) { m_Roster = roster; }
|
||||
Vehicle VehicleEntity() { return m_Vehicle; }
|
||||
ERR_ExtractionState State() { return m_State; }
|
||||
ScriptInvoker OnResolved() { return m_OnResolved; }
|
||||
int EscapedCount() { return m_Escapees.Count(); }
|
||||
int LeftBehindCount() { return m_ParticipantCount - m_Escapees.Count(); }
|
||||
bool DidEscape(UUID identity) { return m_Escapees.Contains(identity); }
|
||||
|
||||
bool Assign(Vehicle vehicle)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Assigned || m_Stopped || !ERR_Actors.Alive(vehicle)) return false;
|
||||
m_Vehicle = vehicle;
|
||||
m_Assigned = true;
|
||||
return true;
|
||||
}
|
||||
int Seats(out array<BaseCompartmentSlot> seats)
|
||||
{
|
||||
seats.Clear();
|
||||
if (!m_Vehicle) return 0;
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
if (!manager) return 0;
|
||||
array<BaseCompartmentSlot> all = {};
|
||||
manager.GetCompartments(all);
|
||||
foreach (BaseCompartmentSlot slot : all)
|
||||
{
|
||||
if (!slot) continue;
|
||||
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
|
||||
if (type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) seats.Insert(slot);
|
||||
}
|
||||
return seats.Count();
|
||||
}
|
||||
bool Ready()
|
||||
{
|
||||
if (!m_Assigned || !ERR_Actors.Alive(m_Vehicle) || !m_Vehicle.FindComponent(HelicopterControllerComponent)) return false;
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
if (Seats(seats) < REQUIRED_SEATS) return false;
|
||||
foreach (BaseCompartmentSlot slot : seats)
|
||||
if (SCR_CompartmentAccessComponent.GetCompartmentType(slot) == ECompartmentType.PILOT) return true;
|
||||
return false;
|
||||
}
|
||||
bool Begin(vector center, float radius)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !Ready()) return false;
|
||||
m_Center = center;
|
||||
m_Radius = radius;
|
||||
m_Begun = true;
|
||||
return true;
|
||||
}
|
||||
void Tick(ERR_Phase phase)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Begun || m_Stopped || m_State != ERR_ExtractionState.WAITING) return;
|
||||
if (phase < ERR_Phase.PRISON || ERR_MissionRules.Terminal(phase)) return;
|
||||
if (!ERR_Actors.Alive(m_Vehicle)) { Resolve(ERR_ExtractionState.LOST); return; }
|
||||
if (phase != ERR_Phase.EXTRACTION || vector.DistanceXZ(m_Vehicle.GetOrigin(), m_Center) <= m_Radius) return;
|
||||
array<ref ERR_PlayerLife> lives = {};
|
||||
m_Roster.Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
{
|
||||
if (!ERR_Actors.Alive(life.Actor) || CompartmentAccessComponent.GetVehicleIn(life.Actor) != m_Vehicle) continue;
|
||||
// Reserved disconnected characters and unconscious passengers remain alive.
|
||||
// A recycled connection ID is never an identity for the escape snapshot.
|
||||
m_Escapees.Insert(life.AccountId);
|
||||
}
|
||||
if (m_Escapees.IsEmpty()) return;
|
||||
m_ParticipantCount = lives.Count();
|
||||
Resolve(ERR_ExtractionState.ESCAPED);
|
||||
}
|
||||
protected void Resolve(ERR_ExtractionState result)
|
||||
{
|
||||
m_State = result;
|
||||
m_OnResolved.Invoke(result);
|
||||
}
|
||||
void ConnectedWinners(out array<int> winners)
|
||||
{
|
||||
winners.Clear();
|
||||
if (m_State != ERR_ExtractionState.ESCAPED || !m_Roster) return;
|
||||
array<ref ERR_PlayerLife> lives = {};
|
||||
m_Roster.Participants(lives);
|
||||
foreach (ERR_PlayerLife life : lives)
|
||||
if (life.Connected && m_Roster.Get(life.ConnectionId) == life && m_Escapees.Contains(life.AccountId)) winners.Insert(life.ConnectionId);
|
||||
}
|
||||
void Stop() { m_Stopped = true; }
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// One authored faction/asset contract, supplied by the game mode resource.
|
||||
[BaseContainerProps()]
|
||||
class ERR_FactionCatalog
|
||||
{
|
||||
[Attribute()] string PlayerFactionKey;
|
||||
[Attribute()] string EnemyFactionKey;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Unarmed prisoner character", "et")]
|
||||
ResourceName PrisonerPrefab;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Initial physical guard squad", "et")]
|
||||
ResourceName GuardGroupPrefab;
|
||||
[Attribute("", UIWidgets.ResourceNamePicker, "Extraction helicopter", "et")]
|
||||
ResourceName ExtractionPrefab;
|
||||
|
||||
bool Valid()
|
||||
{
|
||||
return !PlayerFactionKey.IsEmpty() && !EnemyFactionKey.IsEmpty() && PlayerFactionKey != EnemyFactionKey && !PrisonerPrefab.IsEmpty() && !GuardGroupPrefab.IsEmpty() && !ExtractionPrefab.IsEmpty();
|
||||
}
|
||||
Faction PlayerFaction()
|
||||
{
|
||||
if (!GetGame().GetFactionManager()) return null;
|
||||
return GetGame().GetFactionManager().GetFactionByKey(PlayerFactionKey);
|
||||
}
|
||||
Faction EnemyFaction()
|
||||
{
|
||||
if (!GetGame().GetFactionManager()) return null;
|
||||
return GetGame().GetFactionManager().GetFactionByKey(EnemyFactionKey);
|
||||
}
|
||||
bool WorldReady()
|
||||
{
|
||||
if (!Valid()) return false;
|
||||
Faction player = PlayerFaction();
|
||||
Faction enemy = EnemyFaction();
|
||||
if (!player || !enemy || !player.IsFactionEnemy(enemy) || !enemy.IsFactionEnemy(player)) return false;
|
||||
array<ResourceName> required = {PrisonerPrefab, GuardGroupPrefab, ExtractionPrefab};
|
||||
foreach (ResourceName prefab : required)
|
||||
{
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!resource || !resource.IsValid()) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+427
@@ -0,0 +1,427 @@
|
||||
class ERR_GameModeClass : SCR_BaseGameModeClass {}
|
||||
class ERR_GameMode : SCR_BaseGameMode
|
||||
{
|
||||
[RplProp(), Attribute("6090 0 7160", UIWidgets.Coords, "Prison position; configured from authored scenario")]
|
||||
protected vector m_PrisonPosition;
|
||||
[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")]
|
||||
protected float m_EscapeRadius;
|
||||
[RplProp()]
|
||||
protected ERR_Phase m_Phase;
|
||||
[Attribute("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf", UIWidgets.ResourceNamePicker, "Faction and asset catalog", "conf")]
|
||||
protected ResourceName m_FactionDefinition;
|
||||
[RplProp()]
|
||||
protected string m_PreparationError;
|
||||
protected ref ERR_FactionCatalog m_Factions;
|
||||
protected ref ERR_Settings m_Settings;
|
||||
protected ref ERR_TimeService m_Time = new ERR_TimeService();
|
||||
protected ref ERR_EnvironmentService m_Environment = new ERR_EnvironmentService();
|
||||
protected ref ERR_DiscoveryService m_Discovery;
|
||||
protected ref ERR_Objectives m_Objectives;
|
||||
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
|
||||
protected ref ERR_AdminService m_Admin = new ERR_AdminService();
|
||||
protected ref ERR_CommandService m_Command;
|
||||
protected ref array<ERR_LocationComponent> m_Locations = {};
|
||||
protected ref array<IEntity> m_OwnedEntities = {};
|
||||
protected ref ERR_ExtractionService m_Extraction;
|
||||
[RplProp()]
|
||||
protected string m_EndReason;
|
||||
protected ref ERR_RadioDefenceService m_RadioDefence;
|
||||
[RplProp()]
|
||||
protected ERR_RadioFeedback m_RadioFeedback;
|
||||
protected ref ERR_RecoveryService m_Recovery;
|
||||
protected ref ERR_PrisonEscapeService m_PrisonEscape;
|
||||
[RplProp()]
|
||||
protected string m_RecoveryStatus;
|
||||
protected bool m_StartedPlayers;
|
||||
protected bool m_Stopped;
|
||||
protected float m_NoSurvivorsSince = -1;
|
||||
protected ref ScriptInvoker m_OnPhaseChanged = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnTick = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnStop = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnLocationDiscovered = new ScriptInvoker();
|
||||
protected ref ScriptInvoker m_OnGroup = new ScriptInvoker();
|
||||
|
||||
static ERR_GameMode Get()
|
||||
{
|
||||
if (!GetGame()) return null;
|
||||
return ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||
}
|
||||
ERR_Phase Phase() { return m_Phase; }
|
||||
ERR_PlayerRoster Players() { return m_Roster; }
|
||||
ERR_AdminService Admin() { return m_Admin; }
|
||||
ERR_Settings Settings() { return m_Settings; }
|
||||
ERR_TimeService Time() { return m_Time; }
|
||||
ERR_EnvironmentService Environment() { return m_Environment; }
|
||||
ERR_RadioDefenceService RadioDefence() { return m_RadioDefence; }
|
||||
ERR_RecoveryService Recovery() { return m_Recovery; }
|
||||
ERR_PrisonEscapeService PrisonEscape() { return m_PrisonEscape; }
|
||||
string RecoveryStatus() { return m_RecoveryStatus; }
|
||||
ERR_CommandService Command() { return m_Command; }
|
||||
ERR_Objectives Objectives() { return m_Objectives; }
|
||||
ERR_ExtractionService Extraction() { return m_Extraction; }
|
||||
string EndReason() { return m_EndReason; }
|
||||
Vehicle ExtractionVehicle()
|
||||
{
|
||||
if (!m_Extraction) return null;
|
||||
return m_Extraction.VehicleEntity();
|
||||
}
|
||||
float EscapeRadius() { return m_EscapeRadius; }
|
||||
ERR_RadioFeedback RadioFeedback() { return m_RadioFeedback; }
|
||||
// Called only from existing authority transition/tick paths; no extra timer.
|
||||
protected bool RefreshRadioFeedback()
|
||||
{
|
||||
if (!Replication.IsServer()) return false;
|
||||
bool defending = m_Phase == ERR_Phase.RADIO_DEFENCE && m_RadioDefence != null && m_RadioDefence.State() == ERR_RadioDefenceState.DEFENDING;
|
||||
bool occupied = defending && m_RadioDefence.Occupied();
|
||||
ERR_RadioFeedback next = ERR_RadioFeedbackRules.Next(m_RadioFeedback, defending, occupied);
|
||||
if (next == m_RadioFeedback) return false;
|
||||
m_RadioFeedback = next;
|
||||
return true;
|
||||
}
|
||||
bool DefenceOccupied() { return m_RadioDefence && m_RadioDefence.Occupied(); }
|
||||
int DefenceSecondsRemaining()
|
||||
{
|
||||
if (!m_RadioDefence) return 0;
|
||||
return m_RadioDefence.SecondsRemaining(GetWorld().GetWorldTime());
|
||||
}
|
||||
ScriptInvoker OnPhaseChanged() { return m_OnPhaseChanged; }
|
||||
ScriptInvoker OnTick() { return m_OnTick; }
|
||||
ScriptInvoker OnStop() { return m_OnStop; }
|
||||
ScriptInvoker OnGroup() { return m_OnGroup; }
|
||||
ScriptInvoker OnLocationDiscovered() { return m_OnLocationDiscovered; }
|
||||
void LocationDiscovered(ERR_LocationComponent location, int previous, int current)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Locations.Contains(location) || current <= previous) return;
|
||||
m_OnLocationDiscovered.Invoke(location, previous, current);
|
||||
}
|
||||
ERR_FactionCatalog Factions() { return m_Factions; }
|
||||
Faction PlayerFaction()
|
||||
{
|
||||
if (!m_Factions) return null;
|
||||
return m_Factions.PlayerFaction();
|
||||
}
|
||||
Faction EnemyFaction()
|
||||
{
|
||||
if (!m_Factions) return null;
|
||||
return m_Factions.EnemyFaction();
|
||||
}
|
||||
string PreparationError() { return m_PreparationError; }
|
||||
string Status()
|
||||
{
|
||||
if (!m_PreparationError.IsEmpty()) return "Mission startup failed: " + m_PreparationError;
|
||||
if (!m_EndReason.IsEmpty()) return m_EndReason;
|
||||
if (m_Phase == ERR_Phase.PREPARING) return "Preparing mission sites and physical supplies.";
|
||||
string phase;
|
||||
if (m_Phase == ERR_Phase.PRISON) phase = "Escape the prison";
|
||||
else if (m_Phase == ERR_Phase.RADIO_SEARCH) phase = "Locate the radio station";
|
||||
else if (m_Phase == ERR_Phase.RADIO_DEFENCE) phase = "Defend the radio transmission";
|
||||
else if (m_Phase == ERR_Phase.EXTRACTION) phase = "Helicopter extraction";
|
||||
else if (m_Phase == ERR_Phase.VICTORY) phase = "Escape complete";
|
||||
else phase = "Mission lost";
|
||||
if (!m_RecoveryStatus.IsEmpty()) phase += " " + m_RecoveryStatus;
|
||||
return phase;
|
||||
}
|
||||
protected void RecoveryChanged(string message)
|
||||
{
|
||||
if (!Replication.IsServer() || m_RecoveryStatus == message) return;
|
||||
m_RecoveryStatus = message;
|
||||
Replication.BumpMe();
|
||||
}
|
||||
bool AbandonRound()
|
||||
{
|
||||
if (!Replication.IsServer() || !Active() || m_Stopped || !m_PreparationError.IsEmpty()) return false;
|
||||
if (!ERR_MissionRules.CanTransition(m_Phase, ERR_Phase.DEFEAT)) return false;
|
||||
m_EndReason = "An administrator abandoned this round.";
|
||||
return Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
protected void FailPreparation(string detail)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
|
||||
m_PreparationError = detail;
|
||||
Replication.BumpMe();
|
||||
Print("Rereintroduced: scenario preparation failed; spawning remains disabled: " + detail, LogLevel.ERROR);
|
||||
StopServices();
|
||||
ERR_SiteService sites = Sites();
|
||||
if (sites) sites.Rollback();
|
||||
for (int i = m_OwnedEntities.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_OwnedEntities[i]);
|
||||
m_OwnedEntities.Clear();
|
||||
}
|
||||
vector PrisonPosition() { return ERR_Actors.Ground(m_PrisonPosition); }
|
||||
bool Active() { return m_Phase >= ERR_Phase.PRISON && !ERR_MissionRules.Terminal(m_Phase); }
|
||||
protected ERR_CommandService CreateCommandService() { return null; }
|
||||
|
||||
void ConfigureStart(vector position)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Phase != ERR_Phase.PREPARING) return;
|
||||
m_PrisonPosition = position;
|
||||
m_EscapeCenter = position;
|
||||
Replication.BumpMe();
|
||||
}
|
||||
ERR_SiteService Sites()
|
||||
{
|
||||
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
|
||||
if (!scenario) return null;
|
||||
return scenario.Sites();
|
||||
}
|
||||
void RegisterLocation(ERR_LocationComponent location)
|
||||
{
|
||||
if (!location || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
|
||||
foreach (ERR_LocationComponent existing : m_Locations)
|
||||
{
|
||||
if (existing.Id() == location.Id())
|
||||
{
|
||||
Print("Rereintroduced: duplicate location ID " + location.Id(), LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
}
|
||||
m_Locations.Insert(location);
|
||||
}
|
||||
bool HasLocation(ERR_LocationComponent location) { return location && m_Locations.Contains(location); }
|
||||
void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); }
|
||||
void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); }
|
||||
void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); }
|
||||
bool SetExtractionVehicle(Vehicle vehicle)
|
||||
{
|
||||
return Replication.IsServer() && m_Phase == ERR_Phase.PREPARING && m_Extraction && m_Extraction.Assign(vehicle);
|
||||
}
|
||||
void PublishEscapeResults(bool reset = false)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (!controller) continue;
|
||||
if (reset) controller.ERR_ResetEscapeResult();
|
||||
else controller.ERR_PublishEscapeResult(m_Roster, m_Extraction);
|
||||
}
|
||||
}
|
||||
void RegisterGroup(SCR_AIGroup group)
|
||||
{
|
||||
if (!Replication.IsServer() || !group) return;
|
||||
Own(group);
|
||||
if (m_Command) m_Command.RegisterGroup(group);
|
||||
m_OnGroup.Invoke(group);
|
||||
}
|
||||
|
||||
bool Transition(ERR_Phase phase)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
|
||||
ERR_Phase previous = m_Phase;
|
||||
m_Phase = phase;
|
||||
RefreshRadioFeedback();
|
||||
if (ERR_MissionRules.Terminal(phase)) PublishEscapeResults();
|
||||
Replication.BumpMe();
|
||||
PrintFormat("Rereintroduced [mission]: phase %1 -> %2", previous, phase);
|
||||
m_OnPhaseChanged.Invoke(previous, phase);
|
||||
if (ERR_MissionRules.Terminal(phase))
|
||||
{
|
||||
StopServices();
|
||||
array<int> winners = {};
|
||||
if (phase == ERR_Phase.VICTORY && m_Extraction) m_Extraction.ConnectedWinners(winners);
|
||||
EGameOverTypes reason = EGameOverTypes.LOOSE;
|
||||
if (phase == ERR_Phase.VICTORY) reason = EGameOverTypes.VICTORY;
|
||||
EndGameMode(SCR_GameModeEndData.Create(reason, winners));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UnlockPrison(IEntity player)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_Roster || !m_Roster.CurrentActor(player) || !ERR_Actors.CanFight(player)) return false;
|
||||
if (!PlayerFaction() || SCR_FactionManager.SGetFaction(player) != PlayerFaction()) return false;
|
||||
if (vector.DistanceXZ(player.GetOrigin(), PrisonPosition()) > 15) return false;
|
||||
return m_Phase == ERR_Phase.PRISON && Transition(ERR_Phase.RADIO_SEARCH);
|
||||
}
|
||||
|
||||
bool TuneRadio(IEntity player, ERR_LocationComponent location)
|
||||
{
|
||||
return m_RadioDefence && m_RadioDefence.Begin(player, location);
|
||||
}
|
||||
protected void RadioStarted(ERR_LocationComponent location)
|
||||
{
|
||||
if (!Replication.IsServer() || !m_RadioDefence || location != m_RadioDefence.Location()) return;
|
||||
if (!Transition(ERR_Phase.RADIO_DEFENCE)) m_RadioDefence.Stop();
|
||||
}
|
||||
protected void RadioCompleted(ERR_LocationComponent location)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_DEFENCE || !m_RadioDefence || m_RadioDefence.State() != ERR_RadioDefenceState.COMPLETED || location != m_RadioDefence.Location()) return;
|
||||
foreach (ERR_LocationComponent candidate : m_Locations)
|
||||
if (candidate.Kind() == ERR_LocationKind.EXTRACTION) candidate.Discover(0, 100);
|
||||
Transition(ERR_Phase.EXTRACTION);
|
||||
}
|
||||
|
||||
void PlayerSpawned(int id, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
|
||||
m_Roster.Spawned(id, actor);
|
||||
m_StartedPlayers = true;
|
||||
m_NoSurvivorsSince = -1;
|
||||
}
|
||||
|
||||
protected string SettingsPath() { return ERR_Settings.PATH; }
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
if (SCR_Global.IsEditMode()) return;
|
||||
m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition));
|
||||
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
|
||||
if (!Replication.IsServer()) return;
|
||||
m_Settings = new ERR_Settings(SettingsPath());
|
||||
OnStop().Insert(m_Environment.Stop);
|
||||
m_Recovery = new ERR_RecoveryService(this);
|
||||
m_Recovery.OnChanged().Insert(RecoveryChanged);
|
||||
OnStop().Insert(m_Recovery.Stop);
|
||||
m_RadioDefence = new ERR_RadioDefenceService(this);
|
||||
m_RadioDefence.OnStarted().Insert(RadioStarted);
|
||||
m_RadioDefence.OnCompleted().Insert(RadioCompleted);
|
||||
OnPhaseChanged().Insert(m_RadioDefence.PhaseChanged);
|
||||
OnStop().Insert(m_RadioDefence.Stop);
|
||||
m_Extraction = new ERR_ExtractionService(m_Roster);
|
||||
PublishEscapeResults(true);
|
||||
m_Extraction.OnResolved().Insert(ExtractionResolved);
|
||||
OnStop().Insert(m_Extraction.Stop);
|
||||
m_PrisonEscape = new ERR_PrisonEscapeService(this);
|
||||
OnPhaseChanged().Insert(m_PrisonEscape.PhaseChanged);
|
||||
OnTick().Insert(m_PrisonEscape.Tick);
|
||||
OnStop().Insert(m_PrisonEscape.Stop);
|
||||
m_Roster.OnChanged().Insert(m_PrisonEscape.RosterChanged);
|
||||
m_Discovery = new ERR_DiscoveryService(this);
|
||||
OnTick().Insert(m_Discovery.Tick);
|
||||
OnStop().Insert(m_Discovery.Stop);
|
||||
m_Objectives = new ERR_Objectives(this);
|
||||
OnPhaseChanged().Insert(m_Objectives.PhaseChanged);
|
||||
OnTick().Insert(m_Objectives.Tick);
|
||||
OnStop().Insert(m_Objectives.Stop);
|
||||
m_Command = CreateCommandService();
|
||||
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
|
||||
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
|
||||
}
|
||||
|
||||
protected void BindLocalObserver()
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
controller.ERR_StartObserverMonitor();
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
}
|
||||
|
||||
protected void StartScenario()
|
||||
{
|
||||
if (m_Phase != ERR_Phase.PREPARING || !m_PreparationError.IsEmpty()) return;
|
||||
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); 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;
|
||||
if (scenario) prepared = scenario.Prepare();
|
||||
if (prepared == ERR_PrepareResult.PENDING)
|
||||
{
|
||||
GetGame().GetCallqueue().CallLater(StartScenario, 250, false);
|
||||
return;
|
||||
}
|
||||
if (prepared == ERR_PrepareResult.FAILED)
|
||||
{
|
||||
string detail = "Scenario component is missing.";
|
||||
if (scenario) detail = scenario.FailureReason();
|
||||
FailPreparation(detail);
|
||||
return;
|
||||
}
|
||||
if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius))
|
||||
{
|
||||
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary.");
|
||||
return;
|
||||
}
|
||||
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
|
||||
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
|
||||
Transition(ERR_Phase.PRISON);
|
||||
}
|
||||
|
||||
protected void ExtractionResolved(ERR_ExtractionState result)
|
||||
{
|
||||
if (!Replication.IsServer() || !Active()) return;
|
||||
if (result == ERR_ExtractionState.LOST)
|
||||
{
|
||||
m_EndReason = "The extraction helicopter was lost. No replacement is available.";
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
else if (result == ERR_ExtractionState.ESCAPED)
|
||||
{
|
||||
m_EndReason = string.Format("Escape complete: %1 escaped; %2 did not escape.", m_Extraction.EscapedCount(), m_Extraction.LeftBehindCount());
|
||||
Transition(ERR_Phase.VICTORY);
|
||||
}
|
||||
}
|
||||
|
||||
protected void ServerTick()
|
||||
{
|
||||
if (!Replication.IsServer() || !Active()) return;
|
||||
float now = GetWorld().GetWorldTime();
|
||||
m_Extraction.Tick(m_Phase);
|
||||
if (!Active()) return;
|
||||
m_RadioDefence.Tick(now);
|
||||
if (RefreshRadioFeedback()) Replication.BumpMe();
|
||||
m_Recovery.Tick();
|
||||
if (!Active()) return;
|
||||
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
|
||||
{
|
||||
m_NoSurvivorsSince = -1;
|
||||
return;
|
||||
}
|
||||
if (m_StartedPlayers && !m_Roster.HasSurvivor())
|
||||
{
|
||||
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
|
||||
if (now - m_NoSurvivorsSince >= 30000)
|
||||
{
|
||||
m_EndReason = "No surviving prisoners remain.";
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else m_NoSurvivorsSince = -1;
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (controller) controller.ERR_UpdateObserverServer();
|
||||
}
|
||||
if (m_Command) m_Command.Tick(now);
|
||||
m_OnTick.Invoke(now);
|
||||
}
|
||||
|
||||
protected void StopServices()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
GetGame().GetCallqueue().Remove(ServerTick);
|
||||
if (m_Command) m_Command.Stop();
|
||||
m_OnStop.Invoke();
|
||||
if (Replication.IsServer())
|
||||
{
|
||||
array<int> connected = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(connected);
|
||||
foreach (int id : connected)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
|
||||
if (controller) controller.ERR_SetObserver(ERR_ObserverReason.NONE);
|
||||
}
|
||||
foreach (IEntity entity : m_OwnedEntities)
|
||||
if (SCR_AIGroup.Cast(entity)) ERR_Actors.DeleteSpawned(entity);
|
||||
}
|
||||
}
|
||||
|
||||
void ~ERR_GameMode()
|
||||
{
|
||||
if (GetGame())
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(StartScenario);
|
||||
GetGame().GetCallqueue().Remove(BindLocalObserver);
|
||||
StopServices();
|
||||
}
|
||||
m_Locations.Clear();
|
||||
m_OwnedEntities.Clear();
|
||||
}
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
// Initial preparation only. A READY site service never consults this rule again.
|
||||
class ERR_InitialGuardRules
|
||||
{
|
||||
static float TimeoutMilliseconds() { return 90000; }
|
||||
static ERR_PrepareResult State(int requested, int physical, int conscious, bool queueComplete, bool expired)
|
||||
{
|
||||
if (requested <= 0 || physical > requested || conscious > physical || conscious < 0 || physical < 0) return ERR_PrepareResult.FAILED;
|
||||
if (expired) return ERR_PrepareResult.FAILED;
|
||||
if (physical == requested && conscious == requested && queueComplete) return ERR_PrepareResult.READY;
|
||||
return ERR_PrepareResult.PENDING;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// GetItems may enumerate the same physical entity through multiple storages.
|
||||
// Identity is the entity reference, never its prefab: distinct rounds still count.
|
||||
class ERR_Inventory
|
||||
{
|
||||
static int AppendUnique(InventoryStorageManagerComponent inventory, notnull array<IEntity> items)
|
||||
{
|
||||
if (!inventory) return items.Count();
|
||||
array<IEntity> raw = {};
|
||||
inventory.GetItems(raw);
|
||||
foreach (IEntity item : raw)
|
||||
if (item && !items.Contains(item)) items.Insert(item);
|
||||
return items.Count();
|
||||
}
|
||||
static int Items(InventoryStorageManagerComponent inventory, out array<IEntity> items)
|
||||
{
|
||||
items.Clear();
|
||||
return AppendUnique(inventory, items);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// A previously initialized off-world medical kit can outlive its weak catalog.
|
||||
// Runtime/startup validation remains vanilla. Only owned teardown bypasses the
|
||||
// unrelated initialization gate; no catalog is kept alive or fabricated.
|
||||
class ERR_ItemStationTeardownRules
|
||||
{
|
||||
static bool Recover(bool initialized, bool scoped, bool offWorld, bool catalogPresent, bool editMode)
|
||||
{
|
||||
return initialized && scoped && offWorld && !catalogPresent && !editMode;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_ResupplyMedicalGadgetSupportStationComponent
|
||||
{
|
||||
protected bool m_ERRTeardownScope;
|
||||
protected bool m_ERRTeardownInitializing;
|
||||
protected bool m_ERRTeardownInitialized;
|
||||
protected bool m_ERRTeardownDone;
|
||||
protected SCR_ResourceComponent m_ERRTeardownResource;
|
||||
|
||||
protected bool ERR_KnownOffWorldKit(IEntity owner)
|
||||
{
|
||||
if (!GetGame() || !owner || owner.GetWorld() == GetGame().GetWorld() || !owner.GetPrefabData()) return false;
|
||||
ResourceName prefab = owner.GetPrefabData().GetPrefabName();
|
||||
return prefab == "{AE578EEA4244D41F}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_US.et" || prefab == "{21EF98BFC1EB3793}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_USSR.et";
|
||||
}
|
||||
|
||||
protected override void DelayedInit(IEntity owner)
|
||||
{
|
||||
m_ERRTeardownInitializing = ERR_GameMode.Get() && ERR_KnownOffWorldKit(owner);
|
||||
super.DelayedInit(owner);
|
||||
m_ERRTeardownInitializing = false;
|
||||
}
|
||||
|
||||
protected override void AddRemoveOnDamageStateChanged(IEntity owner, bool subscribe)
|
||||
{
|
||||
super.AddRemoveOnDamageStateChanged(owner, subscribe);
|
||||
// Vanilla reaches this only after the complete virtual InitValidSetup passed.
|
||||
if (!subscribe || !m_ERRTeardownInitializing) return;
|
||||
m_ERRTeardownScope = true;
|
||||
m_ERRTeardownInitialized = true;
|
||||
if (m_ResourceConsumer || m_ResourceGenerator) m_ERRTeardownResource = GetResourceComponent();
|
||||
}
|
||||
|
||||
override void OnDelete(IEntity owner)
|
||||
{
|
||||
bool offWorld = owner && (!GetGame() || owner.GetWorld() != GetGame().GetWorld());
|
||||
if (!ERR_ItemStationTeardownRules.Recover(m_ERRTeardownInitialized, m_ERRTeardownScope, offWorld, m_EntityCatalogManager != null, SCR_Global.IsEditMode()))
|
||||
{
|
||||
super.OnDelete(owner);
|
||||
return;
|
||||
}
|
||||
if (m_ERRTeardownDone) return;
|
||||
m_ERRTeardownDone = true;
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(DelayedInit);
|
||||
AddRemoveOnDamageStateChanged(owner, false);
|
||||
// These exact stations reject UsesRange at initialization and were never
|
||||
// registered with the range manager. Native resource subscription is separate
|
||||
// from range, and still needs removing when its component remains alive.
|
||||
if (m_ERRTeardownResource) m_ERRTeardownResource.TEMP_GetOnInteractorReplicated().Remove(TEMP_OnInteractorReplicated);
|
||||
m_ERRTeardownResource = null;
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
// Attach only to a successor-owned prisoner prefab, with a native action duration.
|
||||
class ERR_LoadCasualtyAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBeShownScript(IEntity user)
|
||||
{
|
||||
return ERR_CasualtyLoadingRules.Nearby(ERR_GameMode.Get(), user, GetOwner());
|
||||
}
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!ERR_CasualtyLoadingRules.Nearby(mode, user, GetOwner())) return false;
|
||||
// Clients do not own the authoritative roster. Server always checks again.
|
||||
if (!Replication.IsServer()) return true;
|
||||
return mode.CasualtyLoading() && mode.CasualtyLoading().CanLoad(user, GetOwner());
|
||||
}
|
||||
override bool HasLocalEffectOnlyScript() { return false; }
|
||||
override void PerformAction(IEntity pOwnerEntity, IEntity pUserEntity)
|
||||
{
|
||||
if (!Replication.IsServer() || pOwnerEntity != GetOwner()) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && mode.CasualtyLoading()) mode.CasualtyLoading().Begin(pUserEntity, pOwnerEntity);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
// A round-local roster identity is not a Bohemia/platform identity. Only the
|
||||
// real native admission callback may select it for the sole offline controller.
|
||||
class ERR_LocalAdmissionRules
|
||||
{
|
||||
static bool Context(bool server, int sessionMode, int count, bool connected, bool sameLocal, bool sameManager, int connection, int controllerId)
|
||||
{
|
||||
return server && sessionMode == RplMode.None && count == 1 && connected && sameLocal && sameManager && connection > 0 && controllerId == connection;
|
||||
}
|
||||
}
|
||||
class ERR_LocalAdmission
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected int m_Connection;
|
||||
protected UUID m_Key;
|
||||
protected bool m_Selected;
|
||||
protected bool m_OfflineMode;
|
||||
protected bool m_LocalKey;
|
||||
protected bool m_Failed;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_RefusalLogged;
|
||||
void ERR_LocalAdmission(ERR_GameMode mode) { m_Mode = mode; m_OfflineMode = RplSession.Mode() == RplMode.None; }
|
||||
bool OfflineMode() { return m_OfflineMode; }
|
||||
bool Selected() { return m_Selected; }
|
||||
bool LocalKey() { return m_LocalKey; }
|
||||
protected bool Context(ERR_GameMode mode, int connection, SCR_PlayerController controller)
|
||||
{
|
||||
if (!GetGame()) return false;
|
||||
PlayerManager manager = GetGame().GetPlayerManager();
|
||||
bool ownedMode = mode && mode == m_Mode && mode == ERR_GameMode.Get();
|
||||
int count = -1;
|
||||
bool connected, sameManager;
|
||||
if (manager)
|
||||
{
|
||||
count = manager.GetPlayerCount();
|
||||
connected = manager.IsPlayerConnected(connection);
|
||||
sameManager = controller && manager.GetPlayerController(connection) == controller;
|
||||
}
|
||||
bool sameLocal = controller && controller == GetGame().GetPlayerController();
|
||||
int controllerId;
|
||||
if (controller) controllerId = controller.GetPlayerId();
|
||||
bool valid = ownedMode && manager && controller && ERR_LocalAdmissionRules.Context(Replication.IsServer(), RplSession.Mode(), count, connected, sameLocal, sameManager, connection, controllerId);
|
||||
if (!valid && !m_RefusalLogged)
|
||||
{
|
||||
string project;
|
||||
if (System.GetCLIParam("errOrdinaryScenario", project) && (project == "core" || project == "chat"))
|
||||
{
|
||||
m_RefusalLogged = true;
|
||||
PrintFormat("ERR ORDINARY LOCAL CONTEXT: server=%1 sessionMode=%2 count=%3 connected=%4 sameLocal=%5 sameManager=%6 positiveMatchingId=%7 ownedMode=%8 controllerPresent=%9", Replication.IsServer(), RplSession.Mode(), count, connected, sameLocal, sameManager, connection > 0 && controllerId == connection, ownedMode, controller != null);
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
UUID Resolve(ERR_GameMode mode, int connection, SCR_PlayerController controller, UUID nativeIdentity)
|
||||
{
|
||||
// This helper is inert for modes created as Listen/Dedicated, even after Stop.
|
||||
if (!m_OfflineMode) return nativeIdentity;
|
||||
if (m_Failed || m_Stopped) return UUID.NULL_UUID;
|
||||
if (m_Selected)
|
||||
{
|
||||
// A late backend value or name change never grants another local life.
|
||||
if (mode != m_Mode || connection != m_Connection || controller != m_Controller || !Validate()) { m_Failed = true; return UUID.NULL_UUID; }
|
||||
return m_Key;
|
||||
}
|
||||
if (!Context(mode, connection, controller)) { m_Failed = true; return UUID.NULL_UUID; }
|
||||
m_Controller = controller; m_Connection = connection; m_Key = nativeIdentity;
|
||||
m_Selected = true;
|
||||
if (m_Key.IsNull()) { m_Key = UUID.GenV4(); m_LocalKey = true; }
|
||||
if (m_Key.IsNull()) { m_Failed = true; return UUID.NULL_UUID; }
|
||||
if (m_LocalKey) Print("Rereintroduced [admission]: sole offline controller uses a generated local-round roster key; backend identity and permissions unchanged.");
|
||||
return m_Key;
|
||||
}
|
||||
bool Validate()
|
||||
{
|
||||
if (!m_OfflineMode) return true;
|
||||
if (m_Failed || m_Stopped) return false;
|
||||
if (!m_Selected) return true;
|
||||
if (!Context(m_Mode, m_Connection, m_Controller)) m_Failed = true;
|
||||
return !m_Failed;
|
||||
}
|
||||
bool Disconnected(int connection)
|
||||
{
|
||||
if (!m_Selected || m_Stopped || connection != m_Connection) return false;
|
||||
m_Failed = true;
|
||||
return true;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
m_Controller = null;
|
||||
m_Key = UUID.NULL_UUID;
|
||||
// Keep selection/failure latched: this object cannot admit a replacement.
|
||||
}
|
||||
}
|
||||
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
void ERR_FailLocalAdmission()
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
string reason = "The offline player context changed. Start a new scenario to establish a fresh session.";
|
||||
if (Phase() == ERR_Phase.PREPARING) { FailPreparation(reason); return; }
|
||||
if (!Active()) return;
|
||||
m_EndReason = reason;
|
||||
Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT }
|
||||
|
||||
[ComponentEditorProps(category: "Rereintroduced", description: "Server-owned location with replicated discovery state")]
|
||||
class ERR_LocationComponentClass : ScriptComponentClass {}
|
||||
class ERR_LocationComponent : ScriptComponent
|
||||
{
|
||||
[RplProp(onRplName: "IdentityChanged"), Attribute("", UIWidgets.EditBox, "Stable scenario location ID")]
|
||||
protected string m_Id;
|
||||
[RplProp()]
|
||||
protected string m_Label;
|
||||
[RplProp(), Attribute("0", UIWidgets.ComboBox, "Location kind", enums: ParamEnumArray.FromEnum(ERR_LocationKind))]
|
||||
protected ERR_LocationKind m_Kind;
|
||||
[RplProp()]
|
||||
protected int m_Knowledge;
|
||||
|
||||
string Id() { return m_Id; }
|
||||
string Label() { return m_Label; }
|
||||
ERR_LocationKind Kind() { return m_Kind; }
|
||||
int Knowledge() { return m_Knowledge; }
|
||||
vector Position() { return GetOwner().GetOrigin(); }
|
||||
|
||||
bool Configure(string id, ERR_LocationKind kind, string label = "")
|
||||
{
|
||||
if (!Replication.IsServer() || id.IsEmpty()) return false;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || mode.Phase() != ERR_Phase.PREPARING) return false;
|
||||
mode.UnregisterLocation(this);
|
||||
m_Id = id;
|
||||
m_Label = label;
|
||||
m_Kind = kind;
|
||||
mode.RegisterLocation(this);
|
||||
Replication.BumpMe();
|
||||
return true;
|
||||
}
|
||||
protected void IdentityChanged()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode) return;
|
||||
mode.UnregisterLocation(this);
|
||||
mode.RegisterLocation(this);
|
||||
}
|
||||
bool Discover(int roll, int chance)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Knowledge >= 2) return false;
|
||||
int previous = m_Knowledge;
|
||||
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
|
||||
Replication.BumpMe();
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.LocationDiscovered(this, previous, m_Knowledge);
|
||||
return true;
|
||||
}
|
||||
|
||||
override void OnPostInit(IEntity owner)
|
||||
{
|
||||
super.OnPostInit(owner);
|
||||
if (!SCR_Global.IsEditMode()) SetEventMask(owner, EntityEvent.INIT);
|
||||
}
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.RegisterLocation(this);
|
||||
}
|
||||
protected override void OnDelete(IEntity owner)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode) mode.UnregisterLocation(this);
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
enum ERR_Phase
|
||||
{
|
||||
PREPARING,
|
||||
PRISON,
|
||||
RADIO_SEARCH,
|
||||
RADIO_DEFENCE,
|
||||
EXTRACTION,
|
||||
VICTORY,
|
||||
DEFEAT
|
||||
}
|
||||
|
||||
// Pure mission policy. Entity queries and authority checks belong to the owner.
|
||||
class ERR_MissionRules
|
||||
{
|
||||
static bool Terminal(int phase)
|
||||
{
|
||||
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
|
||||
}
|
||||
|
||||
static bool CanTransition(int from, int to)
|
||||
{
|
||||
if (Terminal(from) || from == to) return false;
|
||||
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
|
||||
if (from == ERR_Phase.PREPARING) return to == ERR_Phase.PRISON;
|
||||
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
|
||||
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
|
||||
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
|
||||
return from == ERR_Phase.EXTRACTION && to == ERR_Phase.VICTORY;
|
||||
}
|
||||
|
||||
static bool DefenceComplete(float now, float since, float duration, bool occupied)
|
||||
{
|
||||
return occupied && since >= 0 && duration > 0 && now >= since && now - since >= duration;
|
||||
}
|
||||
|
||||
static bool ContactFresh(float observed, float now)
|
||||
{
|
||||
return observed >= 0 && now >= observed && now - observed <= 120000;
|
||||
}
|
||||
|
||||
static int Discovery(int knowledge, int roll, int identifyPercent)
|
||||
{
|
||||
if (knowledge >= 1 || roll < identifyPercent) return 2;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
#ifdef WORKBENCH
|
||||
// Test adapter exercises the production setting handler with a disposable file.
|
||||
// Actual mode.Admin below still tests its server authorization and alias dispatch.
|
||||
class ERR_NativeAdminSettingsAdapter : ERR_AdminService
|
||||
{
|
||||
void Probe(ERR_Settings settings, string arguments, array<string> replies)
|
||||
{
|
||||
replies.Clear();
|
||||
arguments = arguments.Trim(); arguments.ToLower();
|
||||
array<string> parts = {};
|
||||
arguments.Split(" ", parts, true);
|
||||
if (parts.Count() != 3 && parts.Count() != 4) return;
|
||||
string key = Setting(parts[0], parts[1]);
|
||||
if (!key.IsEmpty()) SettingCommand(settings, key, parts, replies);
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeAdminSettingsProbe
|
||||
{
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: admin settings " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: admin settings " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected static bool Storage(string path)
|
||||
{
|
||||
ERR_Settings settings = new ERR_Settings(path);
|
||||
ERR_NativeAdminSettingsAdapter adapter = new ERR_NativeAdminSettingsAdapter();
|
||||
array<string> replies = {};
|
||||
map<string, int> initial = new map<string, int>();
|
||||
foreach (string key : ERR_Settings.Keys()) initial.Set(key, settings.Get(key));
|
||||
array<int> values = {0, 100, 15};
|
||||
foreach (int value : values)
|
||||
{
|
||||
adapter.Probe(settings, " INTEL REVEAL SET " + value.ToString() + " ", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0] == "intelRevealChancePercent=" + value.ToString() + " saved" && settings.Get("intelRevealChancePercent") == value, "authorized setting handler accepts normalized value " + value.ToString())) return false;
|
||||
ERR_Settings reloaded = new ERR_Settings(path);
|
||||
if (!Check(reloaded.Get("intelRevealChancePercent") == value, "discovery probability survives native JSON reload")) return false;
|
||||
foreach (string key, int original : initial)
|
||||
if (key != "intelRevealChancePercent" && !Check(reloaded.Get(key) == original, "unrelated setting preserved: " + key)) return false;
|
||||
}
|
||||
array<string> invalid = {"-1", "101", "1.5", "100junk", "999999"};
|
||||
foreach (string argument : invalid)
|
||||
{
|
||||
adapter.Probe(settings, "intel reveal set " + argument, replies);
|
||||
ERR_Settings reloaded = new ERR_Settings(path);
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("Invalid") && replies[0].Contains("0-100") && settings.Get("intelRevealChancePercent") == 15 && reloaded.Get("intelRevealChancePercent") == 15, "invalid input preserves live and stored value: " + argument)) return false;
|
||||
}
|
||||
adapter.Probe(settings, "intel reveal help", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("second document") && replies[0].Contains("newly recovered") && replies[0].Contains("remain unchanged"), "help explains exact versus rough discovery and apply timing")) return false;
|
||||
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");
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
if (!Check(Replication.IsServer() && mode && mode.Admin() && mode.Settings(), "native Core command service and settings ready")) return false;
|
||||
int original = mode.Settings().Get("intelRevealChancePercent");
|
||||
array<string> replies = {};
|
||||
mode.Admin().Execute(mode, false, "intel reveal set 100", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0] == "Administrator access required." && mode.Settings().Get("intelRevealChancePercent") == original, "actual unauthorized command cannot mutate discovery")) return false;
|
||||
mode.Admin().Execute(mode, true, " INTEL REVEAL SHOW ", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0] == "intelRevealChancePercent=" + original.ToString(), "actual Core alias dispatches without Chat")) return false;
|
||||
mode.Admin().Execute(mode, true, "intel reveal set 101", replies);
|
||||
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;
|
||||
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);
|
||||
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");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+851
@@ -0,0 +1,851 @@
|
||||
// Explicit local engine fixture, excluded from game/server builds.
|
||||
#ifdef WORKBENCH
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_NativeEnvironmentRestartProbe m_ERRBaseEnvironmentRestartProbe;
|
||||
protected override string SettingsPath()
|
||||
{
|
||||
string enabled, outcome;
|
||||
if (System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "environment-restart")
|
||||
{
|
||||
m_ERRBaseEnvironmentRestartProbe = new ERR_NativeEnvironmentRestartProbe(this);
|
||||
return ERR_NativeEnvironmentRestartProbe.PATH;
|
||||
}
|
||||
return super.SettingsPath();
|
||||
}
|
||||
|
||||
protected ref ERR_NativeCasualtyServiceProbe m_ERRCasualtyServiceProbe;
|
||||
protected ref ERR_NativePrisonerProbe m_ERRPrisonerProbe;
|
||||
protected ref ERR_NativeGuardCancelProbe m_ERRGuardCancelProbe;
|
||||
protected ref ERR_NativeLocalAdmissionProbe m_ERRLocalAdmissionProbe;
|
||||
protected ref ERR_NativePoweredFlightProbe m_ERRPoweredFlightProbe;
|
||||
protected ref ERR_NativePrisonBreachProbe m_ERRPrisonBreachProbe;
|
||||
protected ref ERR_NativeItemStationTeardownProbe m_ERRItemStationTeardownProbe;
|
||||
protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe;
|
||||
protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe;
|
||||
protected ref ERR_NativeCasualtyLoadProbe m_ERRCasualtyLoadProbe;
|
||||
protected ref ERR_NativeEnvironmentProbe m_ERRBaseEnvironmentProbe;
|
||||
protected bool m_ERRCatalogChecked;
|
||||
protected bool m_ERRDiscoveryMapChecked;
|
||||
protected ref ERR_NativePatrolProbe m_ERRPatrolEast;
|
||||
protected ref ERR_NativePatrolProbe m_ERRPatrolWest;
|
||||
protected int m_ERRBaseStep;
|
||||
protected bool m_ERRObjectiveCloseChecked;
|
||||
protected bool m_ERRStartupFailure;
|
||||
protected bool m_ERRCacheLoss;
|
||||
protected string m_ERRExtractionCase;
|
||||
protected ref ERR_NativeExtractionProbe m_ERRExtractionProbe;
|
||||
protected ref ERR_NativeTransportProbe m_ERRTransportProbe;
|
||||
protected ref ERR_NativeLifecycleProbe m_ERRLifecycleProbe;
|
||||
protected ref ERR_NativeRecoveryProbe m_ERRRecoveryProbe;
|
||||
protected ref ERR_NativeRadioDefenceProbe m_ERRBaseRadioProbe;
|
||||
protected string m_ERRActiveCacheId = "everon-supply-1";
|
||||
protected int m_ERRMedicalDiscoveries;
|
||||
protected IEntity m_ERRSightBlocker;
|
||||
protected float m_ERRBaseDeadline;
|
||||
protected float m_ERRBaseWait;
|
||||
protected IEntity m_ERRBasePlayer;
|
||||
protected ERR_LocationComponent m_ERRBaseRadio;
|
||||
protected ref array<IEntity> m_ERRBaseDocuments = {};
|
||||
|
||||
protected override void EOnInit(IEntity owner)
|
||||
{
|
||||
super.EOnInit(owner);
|
||||
string enabled;
|
||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||
ERR_NativeObjectiveProbe.BeginRound();
|
||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||
string outcome;
|
||||
m_ERRStartupFailure = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "startup-failure";
|
||||
m_ERRExtractionCase = outcome;
|
||||
if (outcome == "extraction-startup-failure" && m_Factions) m_Factions.ExtractionPrefab = "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et";
|
||||
if (outcome == "extraction-boundary-failure") m_EscapeRadius = 0;
|
||||
m_ERRCacheLoss = outcome == "cache-loss";
|
||||
if (m_ERRCacheLoss) m_ERRActiveCacheId = "everon-supply-2";
|
||||
OnLocationDiscovered().Insert(ERR_BaseDiscovered);
|
||||
OnStop().Insert(ERR_NativeObjectiveProbe.Stop);
|
||||
OnStop().Insert(ERR_StopRadioProbe);
|
||||
OnStop().Insert(ERR_StopHealthProbes);
|
||||
if (m_ERRStartupFailure && m_Factions) m_Factions.EnemyFactionKey = m_Factions.PlayerFactionKey;
|
||||
m_ERRBaseDeadline = GetWorld().GetWorldTime() + 210000;
|
||||
if (outcome == "patrols") m_ERRBaseDeadline = GetWorld().GetWorldTime() + 330000;
|
||||
OnPhaseChanged().Insert(ERR_BaseResult);
|
||||
GetGame().GetCallqueue().CallLater(ERR_BaseTick, 500, true);
|
||||
Print("ERR BASE SMOKE: core-only scripted escape; not manual or multiplayer acceptance");
|
||||
}
|
||||
protected bool ERR_BaseCheck(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: base " + detail); return true; }
|
||||
ERR_NativeObjectiveProbe.Stop();
|
||||
ERR_StopRadioProbe();
|
||||
ERR_StopHealthProbes();
|
||||
Print("ERR SMOKE FAIL: base " + detail, LogLevel.ERROR);
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
return false;
|
||||
}
|
||||
protected void ERR_StopHealthProbes()
|
||||
{
|
||||
ERR_NativeDiscoveryMapProbe.Stop();
|
||||
ERR_NativeProgressionIdentityProbe.Stop();
|
||||
if (m_ERRPatrolEast) m_ERRPatrolEast.Stop();
|
||||
if (m_ERRPatrolWest) m_ERRPatrolWest.Stop();
|
||||
if (m_ERRBaseEnvironmentRestartProbe) m_ERRBaseEnvironmentRestartProbe.Stop();
|
||||
if (m_ERRBaseEnvironmentProbe) m_ERRBaseEnvironmentProbe.Stop();
|
||||
if (m_ERRPrisonerProbe) m_ERRPrisonerProbe.Stop();
|
||||
if (m_ERRGuardCancelProbe) m_ERRGuardCancelProbe.Stop();
|
||||
if (m_ERRLocalAdmissionProbe) m_ERRLocalAdmissionProbe.Stop();
|
||||
if (m_ERRPoweredFlightProbe) m_ERRPoweredFlightProbe.Stop();
|
||||
if (m_ERRPrisonBreachProbe) m_ERRPrisonBreachProbe.Stop();
|
||||
if (m_ERRItemStationTeardownProbe) m_ERRItemStationTeardownProbe.Stop();
|
||||
if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop();
|
||||
if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop();
|
||||
if (m_ERRBaseMedicalProbe) m_ERRBaseMedicalProbe.Stop();
|
||||
if (m_ERRCasualtyLoadProbe) m_ERRCasualtyLoadProbe.Cleanup();
|
||||
if (m_ERRRecoveryProbe) m_ERRRecoveryProbe.Stop();
|
||||
}
|
||||
protected void ERR_StopRadioProbe() { if (m_ERRBaseRadioProbe) m_ERRBaseRadioProbe.Stop(); }
|
||||
protected void ERR_BaseMove(vector position)
|
||||
{
|
||||
vector transform[4];
|
||||
m_ERRBasePlayer.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(position);
|
||||
ChimeraCharacter.Cast(m_ERRBasePlayer).Teleport(transform);
|
||||
}
|
||||
protected ERR_UnlockPrisonAction ERR_BaseLock(IEntity entity)
|
||||
{
|
||||
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseUserAction> actions = {};
|
||||
manager.GetActionsList(actions);
|
||||
foreach (BaseUserAction action : actions)
|
||||
{
|
||||
ERR_UnlockPrisonAction lock = ERR_UnlockPrisonAction.Cast(action);
|
||||
if (lock) return lock;
|
||||
}
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
ERR_UnlockPrisonAction lock = ERR_BaseLock(child);
|
||||
if (lock) return lock;
|
||||
child = child.GetSibling();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected bool ERR_BaseCover()
|
||||
{
|
||||
int count;
|
||||
foreach (IEntity entity : m_OwnedEntities)
|
||||
{
|
||||
if (!entity || !entity.GetPrefabData() || entity.GetPrefabData().GetPrefabName() != "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et") continue;
|
||||
vector transform[4];
|
||||
entity.GetWorldTransform(transform);
|
||||
vector up = transform[1];
|
||||
if (!ERR_BaseCheck(up[1] > 0.99, "outpost barrier is upright")) return false;
|
||||
vector low, high;
|
||||
entity.GetWorldBounds(low, high);
|
||||
vector center = (low + high) * 0.5;
|
||||
vector across = "0 0 2";
|
||||
if (high[0] - low[0] < high[2] - low[2]) across = "2 0 0";
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = center + across;
|
||||
trace.End = center - across;
|
||||
trace.Flags = TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
float fraction = GetWorld().TraceMove(trace, null);
|
||||
PrintFormat("ERR COVER GEOMETRY: low=%1 high=%2 trace=%3", low, high, fraction);
|
||||
if (!ERR_BaseCheck(fraction < 1 && trace.TraceEnt == entity, "outpost barrier has physical collision")) return false;
|
||||
count++;
|
||||
}
|
||||
return ERR_BaseCheck(count == 3, "exactly three authored outpost barriers exist");
|
||||
}
|
||||
protected void ERR_BaseDiscovered(ERR_LocationComponent location, int previous, int current)
|
||||
{
|
||||
if (location.Id() == "everon-medical-1" && previous == 0 && current == 2) m_ERRMedicalDiscoveries++;
|
||||
}
|
||||
protected void ERR_BaseResult(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (!ERR_MissionRules.Terminal(current)) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
if (m_ERRExtractionCase == "station-teardown")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2002, "native station teardown fixture finished before explicit reset")) return;
|
||||
ERR_StopHealthProbes();
|
||||
Print("ERR SMOKE COMPLETE: isolated off-world station catalog-expiry injection and native callback teardown; not restart reproduction");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "prison-breach")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1902, "native destroyed-gate crossing finished before explicit reset")) return;
|
||||
ERR_StopHealthProbes();
|
||||
Print("ERR SMOKE COMPLETE: actual gate destruction and sampled prison crossing; repositioned fixture, not walking or multiplayer");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "powered-flight" || m_ERRExtractionCase == "powered-translate")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1802, "native powered-flight fixture finished before explicit test reset")) return;
|
||||
ERR_StopHealthProbes();
|
||||
if (m_ERRExtractionCase == "powered-translate")
|
||||
Print("ERR SMOKE COMPLETE: native powered takeoff, bounded four-metre translation, target hold, contact landing and engine-off; isolated mechanism fixture reset");
|
||||
else Print("ERR SMOKE COMPLETE: native pilot-input powered takeoff, hover, contact landing and shutdown; isolated mechanism fixture reset");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "local-admission")
|
||||
{
|
||||
// Completion is emitted by the calling tick after synchronous teardown and
|
||||
// its post-transition invariants; a terminal event alone is insufficient.
|
||||
ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1700, "local admission fixture received its production fail-closed transition");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "early-helicopter-loss")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 201 && Players().HasSurvivor() && Extraction().State() == ERR_ExtractionState.LOST, "lost helicopter ends prison phase despite a living prisoner")) return;
|
||||
ERR_FailureGameOverScreenInfo result = new ERR_FailureGameOverScreenInfo();
|
||||
if (!ERR_BaseCheck(EndReason().Contains("helicopter was lost") && result.GetDebriefing(0, null) == EndReason(), "early loss has a specific replicated and debriefing reason")) return;
|
||||
Print("ERR SMOKE COMPLETE: early extraction vehicle loss");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "casualty-service")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1402, "casualty service fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native casualty service with helper-only identity adapter; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "radio-document-loss" || m_ERRExtractionCase == "radio-document-loss-partial")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1302, "radio document-loss fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native " + m_ERRExtractionCase + "; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ERRExtractionCase == "guard-cancel")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1502, "guard cancellation fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native guard-cancel; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ERRExtractionCase == "prisoner-kit")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 702, "prisoner-kit fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native prisoner-kit; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "medical")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 602, "medical fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native medical; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if ((m_ERRExtractionCase == "casualty-load" || m_ERRExtractionCase == "casualty-direct" || m_ERRExtractionCase == "casualty-bench" || m_ERRExtractionCase == "casualty-remove"))
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 802, "casualty-load fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native " + m_ERRExtractionCase + "; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "environment-restart")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1202, "environment restart round finished before explicit test reset")) return;
|
||||
PrintFormat("ERR SMOKE COMPLETE: environment restart round %1; reset requested by fixture", m_ERRBaseEnvironmentRestartProbe.Round());
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "environment")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1002, "environment fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native environment; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "patrols")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 1102, "both patrol routes finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native finite patrol routes and cleanup; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "recovery")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("administrator abandoned"), "explicit authorized abandonment uses normal defeat result")) return;
|
||||
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
|
||||
Print("ERR SMOKE COMPLETE: recovery status and explicit administrator abandonment");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "objective-ui")
|
||||
{
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 902, "objective UI fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native objective UI close, reopen and deleted-task cleanup; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "lifecycle")
|
||||
{
|
||||
if (!ERR_BaseCheck(m_ERRBaseStep == 402, "lifecycle fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: native lifecycle admission guards; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "transport-loss")
|
||||
{
|
||||
if (!ERR_BaseCheck(m_ERRBaseStep == 302, "transport fixture finished before explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: finite native ground transport; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "extraction-capacity")
|
||||
{
|
||||
if (!ERR_BaseCheck(m_ERRBaseStep == 202, "capacity fixture finished before its explicit test reset")) return;
|
||||
Print("ERR SMOKE COMPLETE: physical capacity and isolated extraction results; reset requested by fixture");
|
||||
return;
|
||||
}
|
||||
if (!ERR_BaseCheck(current == ERR_Phase.VICTORY && m_ERRBaseStep == 5, "escape and restart reached without companion services")) return;
|
||||
Print("ERR SMOKE COMPLETE: core-only escape");
|
||||
}
|
||||
protected void ERR_BaseTick()
|
||||
{
|
||||
float now = GetWorld().GetWorldTime();
|
||||
if (ERR_NativeProgressionIdentityProbe.Failed()) { ERR_BaseCheck(false, "native progression identity rejection fixture"); return; }
|
||||
if (now > m_ERRBaseDeadline) { ERR_BaseCheck(false, "timeout at " + m_ERRBaseStep.ToString()); return; }
|
||||
if (m_ERRExtractionCase == "extraction-startup-failure" || m_ERRExtractionCase == "extraction-boundary-failure")
|
||||
{
|
||||
if (PreparationError().IsEmpty()) return;
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid extraction setup never starts gameplay")) return;
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
if (!ERR_BaseCheck(players.IsEmpty() && Sites() && Sites().Count() == 0 && m_OwnedEntities.IsEmpty() && !ExtractionVehicle(), "late startup failure rolls back all authored stock, scenery, groups and vehicle")) return;
|
||||
if (!ERR_BaseCheck(PreparationError().Contains("twelve") && !Transition(ERR_Phase.PRISON), "invalid extraction retains actionable diagnosis and blocks progression")) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
Print("ERR SMOKE COMPLETE: expected late extraction startup failure");
|
||||
return;
|
||||
}
|
||||
if (m_ERRStartupFailure)
|
||||
{
|
||||
if (PreparationError().IsEmpty()) return;
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.PREPARING && !Active() && m_Stopped, "invalid faction setup stops services without starting the mission")) return;
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players);
|
||||
if (!ERR_BaseCheck(players.IsEmpty() && !Sites() && m_OwnedEntities.IsEmpty(), "failed setup spawns no players, sites or supplies")) return;
|
||||
array<string> replies = {};
|
||||
Admin().Execute(this, true, "status", replies);
|
||||
if (!ERR_BaseCheck(replies.Count() == 1 && replies[0].Contains("configured player and enemy factions"), "administrator receives retained startup failure cause")) return;
|
||||
if (!ERR_BaseCheck(!Transition(ERR_Phase.PRISON) && !Transition(ERR_Phase.EXTRACTION), "failed preparation cannot be advanced into gameplay")) return;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
Print("ERR SMOKE COMPLETE: expected startup-failure");
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 2000)
|
||||
{
|
||||
int result = m_ERRItemStationTeardownProbe.Tick(now);
|
||||
if (result < 0) { ERR_BaseCheck(false, "native isolated station teardown fixture"); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 2002; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1900)
|
||||
{
|
||||
int result = m_ERRPrisonBreachProbe.Tick(now);
|
||||
if (result < 0) { ERR_BaseCheck(false, "native destroyed-gate progression fixture"); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1902; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1800)
|
||||
{
|
||||
int result = m_ERRPoweredFlightProbe.Tick(now);
|
||||
if (result < 0) { ERR_BaseCheck(false, "native powered-flight feasibility fixture"); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1802; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1700)
|
||||
{
|
||||
int result = m_ERRLocalAdmissionProbe.Tick(now);
|
||||
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1)
|
||||
{
|
||||
m_ERRBaseStep = 1702;
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
Print("ERR SMOKE COMPLETE: native local admission callback replay and fail-closed method injection; not multiplayer evidence");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1400)
|
||||
{
|
||||
int result = m_ERRCasualtyServiceProbe.Tick(now);
|
||||
if (result < 0) { ERR_BaseCheck(false, "casualty service fixture failed"); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1402; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1300)
|
||||
{
|
||||
int result = m_ERRObjectiveLossProbe.Tick(now);
|
||||
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1302; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ERRBaseStep == 1500)
|
||||
{
|
||||
int result = m_ERRGuardCancelProbe.Tick(now);
|
||||
if (result < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1502; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ERRBaseStep == 700)
|
||||
{
|
||||
int result = m_ERRPrisonerProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 702; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 600)
|
||||
{
|
||||
int result = m_ERRBaseMedicalProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 602; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 800)
|
||||
{
|
||||
int result = m_ERRCasualtyLoadProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 802; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1200)
|
||||
{
|
||||
int result = m_ERRBaseEnvironmentRestartProbe.Tick(now);
|
||||
if (result < 0) { ERR_BaseCheck(false, "environment restart fixture failed"); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1202; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1000)
|
||||
{
|
||||
int result = m_ERRBaseEnvironmentProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 1002; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 1100)
|
||||
{
|
||||
int east = m_ERRPatrolEast.Tick(now);
|
||||
int west = m_ERRPatrolWest.Tick(now);
|
||||
if (east < 0 || west < 0) { ERR_StopHealthProbes(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (east == 1 && west == 1)
|
||||
{
|
||||
if (!ERR_NativePatrolProbe.StopChecks(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRBaseStep = 1102; Transition(ERR_Phase.DEFEAT);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 500)
|
||||
{
|
||||
int result = m_ERRRecoveryProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1)
|
||||
{
|
||||
m_ERRBaseStep = 502;
|
||||
array<string> replies = {};
|
||||
Admin().Execute(this, true, "mission abandon", replies);
|
||||
ERR_BaseCheck(Phase() == ERR_Phase.DEFEAT && replies.Count() == 1 && replies[0].Contains("Round abandoned") && RecoveryStatus().IsEmpty() && Recovery().Conscious() == 0 && Recovery().Incapacitated() == 0, "administrator command returns accepted outcome after recovery cleanup");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 400)
|
||||
{
|
||||
int result = m_ERRLifecycleProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 402; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 300)
|
||||
{
|
||||
int result = m_ERRTransportProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 302; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 200)
|
||||
{
|
||||
int result = m_ERRExtractionProbe.Tick(now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 202; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 901)
|
||||
{
|
||||
int result = ERR_NativeObjectiveProbe.TickCloseReopen(this, now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1)
|
||||
{
|
||||
if (m_ERRExtractionCase == "objective-ui")
|
||||
{
|
||||
if (!ERR_NativeObjectiveProbe.BeginDeletedTaskClose(this, now)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRBaseStep = 903;
|
||||
}
|
||||
else { m_ERRObjectiveCloseChecked = true; m_ERRBaseStep = 0; }
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (m_ERRBaseStep == 903)
|
||||
{
|
||||
int result = ERR_NativeObjectiveProbe.TickDeletedTaskClose(this, now);
|
||||
if (result < 0) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 1) { m_ERRBaseStep = 902; Transition(ERR_Phase.DEFEAT); }
|
||||
return;
|
||||
}
|
||||
if (now < m_ERRBaseWait) return;
|
||||
if (m_ERRBaseStep == 0)
|
||||
{
|
||||
array<IEntity> players = {};
|
||||
ERR_Actors.Players(players, true);
|
||||
if (players.Count() != 1 || Phase() != ERR_Phase.PRISON) return;
|
||||
m_ERRBasePlayer = players[0];
|
||||
if (!m_ERRCatalogChecked)
|
||||
{
|
||||
if (!ERR_NativeFactionCatalogProbe.Run(this)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRCatalogChecked = true;
|
||||
}
|
||||
if (!m_ERRDiscoveryMapChecked)
|
||||
{
|
||||
if (!ERR_NativeDiscoveryMapProbe.Run()) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRDiscoveryMapChecked = true;
|
||||
}
|
||||
if (m_ERRExtractionCase == "station-teardown")
|
||||
{
|
||||
m_ERRItemStationTeardownProbe = new ERR_NativeItemStationTeardownProbe();
|
||||
m_ERRBaseStep = 2000;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "prison-breach")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPrisonBreachProbe = new ERR_NativePrisonBreachProbe(this, m_ERRBasePlayer);
|
||||
m_ERRBaseStep = 1900;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "powered-flight" || m_ERRExtractionCase == "powered-translate")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPoweredFlightProbe = new ERR_NativePoweredFlightProbe(this);
|
||||
string autohoverMode;
|
||||
if (System.GetCLIParam("errFlightAutohover", autohoverMode) && autohoverMode == "1")
|
||||
if (!ERR_BaseCheck(m_ERRPoweredFlightProbe.SetAutohoverMode(true), "owned native autohover experiment selected before allocation")) return;
|
||||
m_ERRBaseDeadline = now + 250000;
|
||||
m_ERRBaseStep = 1800;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "local-admission")
|
||||
{
|
||||
m_ERRLocalAdmissionProbe = new ERR_NativeLocalAdmissionProbe(this);
|
||||
m_ERRBaseStep = 1700;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "casualty-service")
|
||||
{
|
||||
m_ERRCasualtyServiceProbe = new ERR_NativeCasualtyServiceProbe(this);
|
||||
m_ERRBaseStep = 1400;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "radio-document-loss" || m_ERRExtractionCase == "radio-document-loss-partial")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRObjectiveLossProbe = new ERR_NativeObjectiveLossProbe(this, m_ERRBasePlayer, m_ERRExtractionCase == "radio-document-loss-partial");
|
||||
m_ERRBaseStep = 1300;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ERRExtractionCase == "guard-cancel")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRGuardCancelProbe = new ERR_NativeGuardCancelProbe(this);
|
||||
m_ERRBaseStep = 1500;
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_ERRExtractionCase == "prisoner-kit")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPrisonerProbe = new ERR_NativePrisonerProbe(this, m_ERRBasePlayer);
|
||||
m_ERRBaseStep = 700;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "medical")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRBaseMedicalProbe = new ERR_NativeMedicalProbe(this);
|
||||
m_ERRBaseStep = 600;
|
||||
return;
|
||||
}
|
||||
if ((m_ERRExtractionCase == "casualty-load" || m_ERRExtractionCase == "casualty-direct" || m_ERRExtractionCase == "casualty-bench" || m_ERRExtractionCase == "casualty-remove"))
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (m_ERRExtractionCase == "casualty-remove") m_ERRCasualtyLoadProbe = new ERR_NativeCasualtyRemoveProbe(this);
|
||||
else m_ERRCasualtyLoadProbe = new ERR_NativeCasualtyLoadProbe(this, m_ERRExtractionCase != "casualty-load", m_ERRExtractionCase == "casualty-bench");
|
||||
m_ERRBaseStep = 800;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "environment-restart")
|
||||
{
|
||||
if (!ERR_BaseCheck(m_ERRBaseEnvironmentRestartProbe != null, "environment restart selected isolated settings before mode initialization")) return;
|
||||
m_ERRBaseStep = 1200;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "environment")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRBaseEnvironmentProbe = new ERR_NativeEnvironmentProbe(this);
|
||||
m_ERRBaseStep = 1000;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "patrols")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRPatrolEast = new ERR_NativePatrolProbe(this, "everon-outpost-1");
|
||||
m_ERRPatrolWest = new ERR_NativePatrolProbe(this, "everon-supply-2");
|
||||
m_ERRBaseStep = 1100;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "recovery")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRRecoveryProbe = new ERR_NativeRecoveryProbe(this, m_ERRBasePlayer);
|
||||
m_ERRBaseStep = 500;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "lifecycle")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRLifecycleProbe = new ERR_NativeLifecycleProbe(this, m_ERRBasePlayer);
|
||||
m_ERRBaseStep = 400;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "transport-survey")
|
||||
{
|
||||
ERR_NativeTransportSurvey.Run(this);
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "transport-loss")
|
||||
{
|
||||
m_ERRTransportProbe = new ERR_NativeTransportProbe(this);
|
||||
m_ERRBaseStep = 300;
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "early-helicopter-loss")
|
||||
{
|
||||
if (!ERR_BaseCheck(Extraction().Ready() && Players().HasSurvivor(), "ready helicopter and living prisoner before deliberate loss")) return;
|
||||
m_ERRBaseStep = 201;
|
||||
ERR_Actors.DeleteSpawned(ExtractionVehicle());
|
||||
return;
|
||||
}
|
||||
if (m_ERRExtractionCase == "extraction-capacity")
|
||||
{
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
m_ERRExtractionProbe = new ERR_NativeExtractionProbe(this);
|
||||
m_ERRBaseStep = 200;
|
||||
return;
|
||||
}
|
||||
string survey;
|
||||
if (System.GetCLIParam("errSmokeOutcome", survey) && survey == "site-survey")
|
||||
{
|
||||
ERR_NativeSiteSurvey probe = new ERR_NativeSiteSurvey();
|
||||
probe.Run();
|
||||
GetGame().GetCallqueue().Remove(ERR_BaseTick);
|
||||
return;
|
||||
}
|
||||
if (!m_ERRObjectiveCloseChecked)
|
||||
{
|
||||
if (!ERR_NativeObjectiveProbe.BeginCloseReopen(this, now)) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
m_ERRBaseStep = 901;
|
||||
return;
|
||||
}
|
||||
if (!ERR_NativeAdminSettingsProbe.Run(this) || !ERR_NativeDefinitionProbe.Run() || !ERR_NativeContractsProbe.Run(this, m_ERRBasePlayer) || !ERR_BaseCover() || !ERR_NativeTimeProbe.Run(this)) return;
|
||||
DamageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
if (!ERR_BaseCheck(Objectives() && Objectives().Current() && Objectives().Current().GetTaskName() == "Escape the prison", "native prison objective exists")) return;
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
|
||||
if (!ERR_BaseCheck(Sites().Count() == 7 && cache && cache.Container && vector.DistanceXZ(cache.Container.GetOrigin(), PrisonPosition()) > 100, "authored supply cache is a separate scavenging location")) return;
|
||||
array<IEntity> stock = {};
|
||||
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
|
||||
PrintFormat("ERR BASE STOCK: cache items=%1", stock.Count());
|
||||
int expectedStock;
|
||||
foreach (ERR_SiteStock entry : cache.Definition.Stock) expectedStock += entry.Count;
|
||||
if (!ERR_BaseCheck(stock.Count() >= expectedStock, "cache holds finite physical equipment and objective documents")) return;
|
||||
if (!ERR_BaseCheck(Command() == null, "no companion HQ or support controller loaded")) return;
|
||||
if (!ERR_BaseCheck(vector.DistanceXZ(m_ERRBasePlayer.GetOrigin(), PrisonPosition()) < 5, "prisoner spawned in authored prison")) return;
|
||||
foreach (ERR_LocationComponent location : m_Locations)
|
||||
if (location.Kind() == ERR_LocationKind.RADIO) m_ERRBaseRadio = location;
|
||||
ERR_UnlockPrisonAction lock;
|
||||
foreach (IEntity entity : m_OwnedEntities)
|
||||
{
|
||||
if (!entity) continue;
|
||||
if (!lock) lock = ERR_BaseLock(entity);
|
||||
}
|
||||
foreach (IEntity item : stock)
|
||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_Resources.ObjectiveDocument()) m_ERRBaseDocuments.Insert(item);
|
||||
if (m_ERRCacheLoss)
|
||||
{
|
||||
ERR_Actors.DeleteSpawned(Sites().Find("everon-supply-1").Container);
|
||||
if (!ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "primary cache lost before any clues are recovered")) return;
|
||||
}
|
||||
if (!ERR_BaseCheck(lock && m_ERRBaseRadio && m_ERRBaseDocuments.Count() == 2 && ERR_Actors.Alive(ExtractionVehicle()), "gate, clues, radio and helicopter created by core")) return;
|
||||
if (!ERR_NativeProgressionIdentityProbe.Begin(this, m_ERRBasePlayer)) { ERR_BaseCheck(false, "native progression identity gate baseline"); return; }
|
||||
ERR_BaseMove(lock.GetOwner().GetOrigin() + "1 0 1");
|
||||
lock.PerformAction(lock.GetOwner(), m_ERRBasePlayer);
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.RADIO_SEARCH, "gate action begins escape")) return;
|
||||
if (!ERR_BaseCheck(m_Settings.Set("radioDefenceSeconds", 30), "test defence duration accepted")) return;
|
||||
SCR_Task prisonTask = SCR_TaskSystem.GetTaskFromTaskID(ERR_Objectives.Id(ERR_Phase.PRISON), false);
|
||||
if (!ERR_BaseCheck(prisonTask && prisonTask.GetTaskState() == SCR_ETaskState.COMPLETED, "previous native objective completes on phase change")) return;
|
||||
if (!ERR_BaseCheck(Objectives().Current().GetTaskUIVisibility() == SCR_ETaskUIVisibility.LIST_ONLY, "search objective does not expose a hidden site")) return;
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
if (!ERR_BaseCheck(Sites().Find("everon-supply-1").Location.Knowledge() == 2 && Sites().Find("everon-supply-2").Location.Knowledge() == 2, "both named supply routes revealed after escape")) return;
|
||||
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
||||
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && Sites().Find("everon-outpost-1").Location.Knowledge() == 0, "optional sites remain hidden until approached")) return;
|
||||
ERR_BaseMove(medical.Location.Position() + "0 0 8");
|
||||
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||
m_ERRBaseStep = 10;
|
||||
m_ERRBaseWait = now + 6000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 10)
|
||||
{
|
||||
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
||||
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && !Sites().CanObserve(m_ERRBasePlayer, medical), "unconscious player cannot discover optional sites")) return;
|
||||
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(false);
|
||||
ERR_BaseMove(medical.Location.Position() + "0 0 65");
|
||||
m_ERRBaseStep = 11;
|
||||
m_ERRBaseWait = now + 6000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 11)
|
||||
{
|
||||
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
||||
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && !Sites().CanObserve(m_ERRBasePlayer, medical), "distant player cannot discover optional sites")) return;
|
||||
ERR_BaseMove(medical.Location.Position() + "0 0 8");
|
||||
m_ERRSightBlocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", ERR_Actors.Ground(medical.Location.Position() + "-1.5 0 4"));
|
||||
if (!ERR_BaseCheck(m_ERRSightBlocker != null, "isolated visibility obstacle created")) return;
|
||||
Own(m_ERRSightBlocker);
|
||||
vector low, high;
|
||||
m_ERRSightBlocker.GetWorldBounds(low, high);
|
||||
PrintFormat("ERR VISIBILITY OBSTACLE: low=%1 high=%2 observer=%3 target=%4", low, high, m_ERRBasePlayer.GetOrigin(), medical.Location.Position());
|
||||
m_ERRBaseStep = 115;
|
||||
m_ERRBaseWait = now + 6000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 115)
|
||||
{
|
||||
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
||||
if (!ERR_BaseCheck(medical.Location.Knowledge() == 0 && !Sites().CanObserve(m_ERRBasePlayer, medical), "physical wall prevents nearby site discovery")) return;
|
||||
ERR_Actors.DeleteSpawned(m_ERRSightBlocker);
|
||||
m_ERRBaseStep = 12;
|
||||
m_ERRBaseWait = now + 6000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 12)
|
||||
{
|
||||
ERR_SiteRuntime medical = Sites().Find("everon-medical-1");
|
||||
if (!ERR_BaseCheck(medical.Location.Knowledge() == 2 && medical.Location.Label() == "Medical stash" && m_ERRMedicalDiscoveries == 1, "conscious nearby player visually discovers named medical stash once")) return;
|
||||
if (!ERR_BaseCheck(!Sites().CanObserve(m_ERRBasePlayer, Sites().Find("everon-radio-1")), "distant medical-site discovery does not reveal the radio")) return;
|
||||
m_ERRBaseStep = 1;
|
||||
}
|
||||
else if (m_ERRBaseStep == 1 || m_ERRBaseStep == 2)
|
||||
{
|
||||
ERR_BaseMove(Sites().Find(m_ERRActiveCacheId).Container.GetOrigin() + "0 0 2");
|
||||
SCR_InventoryStorageManagerComponent inventory = SCR_InventoryStorageManagerComponent.Cast(m_ERRBasePlayer.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(Sites().Find(m_ERRActiveCacheId).Container.FindComponent(InventoryStorageManagerComponent));
|
||||
BaseInventoryStorageComponent destination = inventory.FindStorageForItem(m_ERRBaseDocuments[m_ERRBaseStep - 1]);
|
||||
if (!ERR_BaseCheck(destination && source.TryMoveItemToStorage(m_ERRBaseDocuments[m_ERRBaseStep - 1], destination), "physical clue recovered")) return;
|
||||
m_ERRBaseStep++;
|
||||
m_ERRBaseWait = now + 2500;
|
||||
}
|
||||
else if (m_ERRBaseStep == 3)
|
||||
{
|
||||
if (!ERR_BaseCheck(Objectives().Current().GetTaskDescription().Contains("The radio station is identified on your map. Reach"), "objective guidance reacts to discovered radio")) return;
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
|
||||
array<IEntity> stock = {};
|
||||
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
|
||||
foreach (IEntity item : stock) if (item) SCR_EntityHelper.DeleteEntityAndChildren(item);
|
||||
if (!ERR_BaseCheck(m_ERRMedicalDiscoveries == 1, "discovery ticks do not repeat the location event")) return;
|
||||
if (m_ERRCacheLoss && !ERR_BaseCheck(!Sites().Find("everon-supply-1").Container, "lost primary cache stays absent while alternate documents identify the radio")) return;
|
||||
if (!ERR_BaseCheck(ERR_NativeProgressionIdentityProbe.DocumentPassed(), "actual held-document rejection and restored recovery were exercised this round")) return;
|
||||
if (!ERR_BaseCheck(m_ERRBaseRadio.Knowledge() == 2, "documents identify radio without Intel addon")) return;
|
||||
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
|
||||
m_ERRBaseStep = 30;
|
||||
m_ERRBaseWait = now + 1000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 30)
|
||||
{
|
||||
m_ERRBaseRadioProbe = new ERR_NativeRadioDefenceProbe(this, m_ERRBasePlayer, m_ERRBaseRadio);
|
||||
m_ERRBaseStep = 301;
|
||||
}
|
||||
else if (m_ERRBaseStep == 301)
|
||||
{
|
||||
int result = m_ERRBaseRadioProbe.Tick(now);
|
||||
if (result < 0) { ERR_StopRadioProbe(); GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
|
||||
if (result == 0) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INACTIVE, "radio feedback remains inactive before tuning")) return;
|
||||
if (!ERR_BaseCheck(TuneRadio(m_ERRBasePlayer, m_ERRBaseRadio), "identified radio accepts transmission")) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.WAITING && ERR_ObjectiveText.Description(this, false).Contains("Waiting for"), "radio feedback publishes waiting before first occupancy sample")) return;
|
||||
m_ERRBaseStep = 31;
|
||||
m_ERRBaseWait = now + 5000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 31)
|
||||
{
|
||||
ERR_SiteRuntime cache = Sites().Find(m_ERRActiveCacheId);
|
||||
array<IEntity> stock = {};
|
||||
ERR_Inventory.Items(InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent)), stock);
|
||||
if (!ERR_BaseCheck(stock.IsEmpty() && Sites().Prepare() == ERR_PrepareResult.READY, "depleted stock stays empty when preparation is queried again")) return;
|
||||
ERR_Actors.DeleteSpawned(cache.Container);
|
||||
if (!ERR_BaseCheck(DefenceOccupied() && DefenceSecondsRemaining() > 0 && DefenceSecondsRemaining() < 30 && Objectives().Current().GetTaskDescription().Contains("seconds remaining"), "native objective reports advancing radio progress")) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.TRANSMITTING && ERR_ObjectiveText.Description(this, false).Contains("Transmission in progress"), "radio guide reports transmitting without querying a countdown")) return;
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||
m_ERRBaseStep = 310;
|
||||
m_ERRBaseWait = now + 35000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 310)
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent));
|
||||
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor does not trigger defeat after the full grace period")) return;
|
||||
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30, "unconscious radio defender cannot advance transmission")) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Progress has reset"), "radio guide reports interruption after unconsciousness")) return;
|
||||
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!ERR_BaseCheck(playerController.GetControlledEntity() == m_ERRBasePlayer && playerController.ERR_ObserverState() == ERR_ObserverReason.NONE && !playerController.ERR_HasObserverCamera(), "unconscious player retains their body and camera")) return;
|
||||
controller.SetUnconscious(false);
|
||||
m_ERRBaseStep = 311;
|
||||
m_ERRBaseWait = now + 6000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 311)
|
||||
{
|
||||
if (!ERR_BaseCheck(ERR_Actors.CanFight(m_ERRBasePlayer) && DefenceOccupied() && DefenceSecondsRemaining() < 30, "regaining consciousness resumes transmission from zero")) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.TRANSMITTING && ERR_ObjectiveText.Description(this, false).Contains("Transmission in progress"), "radio guide returns to transmitting after recovery")) return;
|
||||
int playerId = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_ERRBasePlayer);
|
||||
if (!ERR_BaseCheck(!Players().CanEnter(playerId, Phase()), "regaining consciousness does not grant another life")) return;
|
||||
ERR_BaseMove(m_ERRBaseRadio.Position() + "90 0 0");
|
||||
m_ERRBaseStep = 32;
|
||||
m_ERRBaseWait = now + 2500;
|
||||
}
|
||||
else if (m_ERRBaseStep == 32)
|
||||
{
|
||||
if (!ERR_BaseCheck(!Sites().Find(m_ERRActiveCacheId).Container, "lost supply cache is not replaced")) return;
|
||||
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30 && Objectives().Current().GetTaskDescription().Contains("interrupted"), "objective reports interruption and reset")) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Return a conscious survivor"), "radio guide reports interruption after leaving the area")) return;
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
ERR_BaseMove(m_ERRBaseRadio.Position() + "2 0 0");
|
||||
m_ERRBaseStep = 4;
|
||||
m_ERRBaseWait = now + 33000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 4)
|
||||
{
|
||||
if (!ERR_BaseCheck(Phase() == ERR_Phase.EXTRACTION, "radio defence reveals extraction") || !ERR_NativeTimeProbe.VerifyProgression(this)) return;
|
||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INACTIVE, "radio feedback clears after completion")) return;
|
||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_ERRBasePlayer.FindComponent(CompartmentAccessComponent));
|
||||
BaseCompartmentSlot slot = access.FindFreeCompartment(ExtractionVehicle(), ECompartmentType.PILOT);
|
||||
if (!ERR_BaseCheck(slot && access.GetInVehicle(ExtractionVehicle(), slot, true, -1, ECloseDoorAfterActions.INVALID, false), "player physically boards extraction helicopter")) return;
|
||||
m_ERRBaseStep = 5;
|
||||
m_ERRBaseWait = now + 3000;
|
||||
}
|
||||
else if (m_ERRBaseStep == 5)
|
||||
{
|
||||
if (!ERR_BaseCheck(CompartmentAccessComponent.GetVehicleIn(m_ERRBasePlayer) == ExtractionVehicle(), "extraction requires real occupancy")) return;
|
||||
vector transform[4];
|
||||
ExtractionVehicle().GetWorldTransform(transform);
|
||||
transform[3] = m_EscapeCenter + Vector(m_EscapeRadius + 100, 150, 0);
|
||||
ExtractionVehicle().SetWorldTransform(transform);
|
||||
m_ERRBaseWait = now + 4000;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
#ifdef WORKBENCH
|
||||
// Positive check observes production initial placement; the fixture never wakes
|
||||
// the mission helicopter. Negative transforms
|
||||
// and velocity are applied only to an independent, disposable empty aircraft.
|
||||
class ERR_NativeCasualtyGroundProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Negative;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
protected int m_Step;
|
||||
protected bool m_Stopped;
|
||||
void ERR_NativeCasualtyGroundProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
ERR_Actors.DeleteSpawned(m_Negative); m_Negative = null;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: casualty ground " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: casualty ground " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (m_Deadline > 0 && now > m_Deadline) { Check(false, "bounded ground-support probe deadline"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
Vehicle parked = m_Mode.ExtractionVehicle();
|
||||
ERR_SiteService sites = m_Mode.Sites();
|
||||
ERR_ExtractionPlacement placement;
|
||||
if (sites) placement = sites.ExtractionPlacement();
|
||||
if (!Check(placement && placement.Ready() && placement.WakeCount() == 1, "production initial placement completed with exactly one wake before player spawn")) return -1;
|
||||
bool support = ERR_CasualtyGroundSupport.Parked(parked, true);
|
||||
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(parked.FindComponent(HelicopterControllerComponent));
|
||||
bool nativeContact = controller && controller.GetSimulation() && controller.GetSimulation().HasAnyGroundContact();
|
||||
if (!Check((nativeContact || support) && ERR_CasualtyLoadingRules.AircraftReady(parked), "naturally settled mission aircraft has native landing contact or three strict wheel supports")) return -1;
|
||||
vector raised = parked.GetOrigin() + "0 20 0";
|
||||
m_Negative = Vehicle.Cast(ERR_Actors.Spawn(m_Mode.Factions().ExtractionPrefab, raised));
|
||||
if (!Check(m_Negative != null, "separate empty airborne aircraft created for rejection checks")) return -1;
|
||||
m_Mode.Own(m_Negative);
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Negative.FindComponent(DamageManagerComponent));
|
||||
if (!Check(damage != null, "negative aircraft damage manager available")) return -1;
|
||||
damage.EnableDamageHandling(false);
|
||||
m_Deadline = now + 10000; m_Wait = now + 500; m_Step = 1; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
vector position = m_Negative.GetOrigin();
|
||||
float height = position[1] - GetGame().GetWorld().GetSurfaceY(position[0], position[2]);
|
||||
bool support = ERR_CasualtyGroundSupport.Parked(m_Negative, true);
|
||||
PrintFormat("ERR GROUND NEGATIVE: raisedPosition=%1 terrainClearance=%2", position, height);
|
||||
if (!Check(ERR_Actors.Alive(m_Negative) && height > 5 && !support && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "living aircraft above terrain is rejected even if stationary or sleeping")) return -1;
|
||||
vector tilted[4]; Math3D.AnglesToMatrix("0 0 90", tilted); tilted[3] = position;
|
||||
m_Negative.SetWorldTransform(tilted);
|
||||
m_Wait = now + 500; m_Step = 2; return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
vector actual[4]; m_Negative.GetWorldTransform(actual);
|
||||
Physics physics = m_Negative.GetPhysics();
|
||||
if (!Check(physics != null, "negative aircraft has actual native physics")) return -1;
|
||||
PrintFormat("ERR GROUND NEGATIVE: tiltedUp=%1 position=%2", actual[1][1], actual[3]);
|
||||
if (!Check(ERR_Actors.Alive(m_Negative) && actual[1][1] < 0.85 && !ERR_CasualtyLoadingRules.AircraftMotion(true, actual[1][1], 0, 0) && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "actual tilted aircraft is rejected independently by upright policy")) return -1;
|
||||
// Read back an actual native velocity on the disposable clone. No mission
|
||||
// aircraft activation, simulation state, engine or velocity is changed.
|
||||
physics.SetVelocity("2 0 0");
|
||||
float speed = physics.GetVelocity().Length();
|
||||
PrintFormat("ERR GROUND NEGATIVE: nativeSpeed=%1", speed);
|
||||
if (!Check(speed > 0.5 && !ERR_CasualtyLoadingRules.AircraftMotion(true, 1, speed, 0) && !ERR_CasualtyLoadingRules.AircraftReady(m_Negative), "actual native velocity is rejected independently by movement policy")) return -1;
|
||||
if (!Check(ERR_Actors.Alive(m_Mode.ExtractionVehicle()) && ERR_CasualtyLoadingRules.AircraftReady(m_Mode.ExtractionVehicle()), "mission aircraft remains untouched and eligible after negative checks")) return -1;
|
||||
Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
#ifdef WORKBENCH
|
||||
// Native mechanism experiment, NOT proof of an available player load/drag action.
|
||||
// Caller must explicitly opt in and tick this independently of medical fixtures.
|
||||
class ERR_NativeCasualtyLoadProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Caller;
|
||||
protected IEntity m_Casualty;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected SCR_CompartmentAccessComponent m_Access;
|
||||
protected SCR_CharacterDamageManagerComponent m_Damage;
|
||||
protected BaseCompartmentSlot m_Seat;
|
||||
protected vector m_CallerTransform[4];
|
||||
protected vector m_GroundPosition;
|
||||
protected bool m_CallerMoved;
|
||||
protected bool m_Direct;
|
||||
protected bool m_Bench;
|
||||
protected float m_DownSettledAt;
|
||||
protected float m_NextDiagnostic;
|
||||
protected int m_Entered;
|
||||
protected int m_Left;
|
||||
protected int m_Step;
|
||||
protected float m_Deadline;
|
||||
protected float m_Wait;
|
||||
|
||||
void ERR_NativeCasualtyLoadProbe(ERR_GameMode mode, bool direct = false, bool bench = false) { m_Mode = mode; m_Direct = direct; m_Bench = bench; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: casualty-load " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: casualty-load " + detail, LogLevel.ERROR);
|
||||
Cleanup();
|
||||
return false;
|
||||
}
|
||||
void Cleanup()
|
||||
{
|
||||
if (m_Access)
|
||||
{
|
||||
if (m_Access.GetOnCompartmentEntered(false)) m_Access.GetOnCompartmentEntered(false).Remove(Entered);
|
||||
if (m_Access.GetOnCompartmentLeft(false)) m_Access.GetOnCompartmentLeft(false).Remove(Left);
|
||||
}
|
||||
// Restore only the same living local body, never a replacement/reconnected actor.
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (m_CallerMoved && m_Caller && controller && controller.GetControlledEntity() == m_Caller && ERR_Actors.Alive(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller))
|
||||
ChimeraCharacter.Cast(m_Caller).Teleport(m_CallerTransform);
|
||||
m_CallerMoved = false;
|
||||
ERR_Actors.DeleteSpawned(m_Casualty);
|
||||
m_Casualty = null;
|
||||
}
|
||||
protected void Entered(IEntity target, BaseCompartmentManagerComponent manager, int managerId, int slotId, bool move)
|
||||
{
|
||||
m_Entered++;
|
||||
PrintFormat("ERR CASUALTY ENTER EVENT: manager=%1 slot=%2 move=%3 targetRoot=%4 count=%5", managerId, slotId, move, target == m_Vehicle, m_Entered);
|
||||
}
|
||||
protected void Left(IEntity target, BaseCompartmentManagerComponent manager, int managerId, int slotId, bool move)
|
||||
{
|
||||
m_Left++;
|
||||
PrintFormat("ERR CASUALTY EXIT EVENT: manager=%1 slot=%2 move=%3 targetRoot=%4 count=%5", managerId, slotId, move, target == m_Vehicle, m_Left);
|
||||
}
|
||||
protected bool BelongsToAircraft(IEntity entity)
|
||||
{
|
||||
while (entity)
|
||||
{
|
||||
if (entity == m_Vehicle) return true;
|
||||
entity = entity.GetParent();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected BaseCompartmentSlot FindTransportBench()
|
||||
{
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
m_Mode.Extraction().Seats(seats);
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
{
|
||||
if (!seat || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) continue;
|
||||
IEntity owner = seat.GetOwner();
|
||||
if (!owner || owner == m_Vehicle || !BelongsToAircraft(owner) || !owner.GetPrefabData()) continue;
|
||||
ResourceName prefab = owner.GetPrefabData().GetPrefabName();
|
||||
// These installed Mi8 seat-part prefabs own the real transport benches.
|
||||
// No compartment class, animation, accessibility or health setting is changed.
|
||||
if (!prefab.Contains("VehPart_MI8_seats")) continue;
|
||||
if (!seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(m_Casualty) || seat.GetOccupant()) continue;
|
||||
return seat;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
protected void Trace(string stage)
|
||||
{
|
||||
if (!m_Casualty || !m_Access) return;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
|
||||
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(m_Casualty.FindComponent(CharacterAnimationComponent));
|
||||
if (controller && animation)
|
||||
PrintFormat("ERR CASUALTY STATE: stage=%1 life=%2 ragdoll=%3 falling=%4 physicsLinked=%5 gettingIn=%6 gettingOut=%7", stage, controller.GetLifeState(), animation.IsRagdollActive(), animation.PhysicsIsFalling(), animation.PhysicsIsLinked(), m_Access.IsGettingIn(), m_Access.IsGettingOut());
|
||||
if (m_Seat)
|
||||
{
|
||||
IEntity owner = m_Seat.GetOwner();
|
||||
string prefab;
|
||||
if (owner && owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
|
||||
PrintFormat("ERR CASUALTY SEAT: stage=%1 name=%2 ownerPrefab=%3 rootOwner=%4 hierarchyValid=%5 slotId=%6 managerId=%7", stage, m_Seat.GetCompartmentUniqueName(), prefab, owner == m_Vehicle, BelongsToAircraft(owner), m_Seat.GetCompartmentSlotID(), m_Seat.GetCompartmentMgrID());
|
||||
PrintFormat("ERR CASUALTY OCCUPANCY: stage=%1 inCompartment=%2 actualSlot=%3 targetOccupant=%4 vehicleRoot=%5 enterEvents=%6 exitEvents=%7", stage, m_Access.IsInCompartment(), m_Access.GetCompartment() == m_Seat, m_Seat.GetOccupant() == m_Casualty, CompartmentAccessComponent.GetVehicleIn(m_Casualty) == m_Vehicle, m_Entered, m_Left);
|
||||
}
|
||||
}
|
||||
protected bool DownOnGround()
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
|
||||
return ERR_Actors.Alive(m_Casualty) && controller && controller.GetLifeState() == ECharacterLifeState.INCAPACITATED && !CompartmentAccessComponent.GetVehicleIn(m_Casualty);
|
||||
}
|
||||
protected bool IncapacitateGroundCasualty()
|
||||
{
|
||||
if (!Check(m_Damage && ERR_Actors.Alive(m_Casualty), "living fixture casualty has native damage manager before incapacitation")) return false;
|
||||
HitZone resilience = m_Damage.GetResilienceHitZone();
|
||||
if (!Check(resilience != null, "fixture casualty has actual native resilience hit zone")) return false;
|
||||
float beforeResilience = resilience.GetHealth();
|
||||
bool damageWasEnabled = m_Damage.IsDamageHandlingEnabled();
|
||||
// ForceUnconsciousness uses HandleDamage(TRUE), which damage protection can
|
||||
// suppress. Permit only this synchronous operation, then restore the flag
|
||||
// before any assertion/cleanup path. Never restore health or life state.
|
||||
m_Damage.EnableDamageHandling(true);
|
||||
m_Damage.ForceUnconsciousness();
|
||||
float afterResilience = resilience.GetHealth();
|
||||
m_Damage.EnableDamageHandling(damageWasEnabled);
|
||||
PrintFormat("ERR CASUALTY INCAPACITATION: resilienceBefore=%1 resilienceAfter=%2 damageOriginallyEnabled=%3 restored=%4", beforeResilience, afterResilience, damageWasEnabled, m_Damage.IsDamageHandlingEnabled() == damageWasEnabled);
|
||||
return Check(afterResilience < beforeResilience && ERR_Actors.Alive(m_Casualty) && m_Damage.IsDamageHandlingEnabled() == damageWasEnabled, "single native incapacitation reduces actual resilience, preserves life and restores damage-protection flag");
|
||||
}
|
||||
protected bool BeginExit()
|
||||
{
|
||||
// Native unloading is a separate mechanism check, not a user-action claim.
|
||||
vector exitTransform[4];
|
||||
m_Casualty.GetWorldTransform(exitTransform);
|
||||
exitTransform[3] = m_GroundPosition;
|
||||
return Check(m_Access.GetOutVehicle_NoDoor(exitTransform, true, false), "unconscious cargo occupant accepts native ground exit");
|
||||
}
|
||||
protected bool ExitSettled()
|
||||
{
|
||||
return ERR_Actors.Alive(m_Casualty) && vector.Distance(m_Casualty.GetOrigin(), m_GroundPosition) < 4 && m_Seat.IsCompartmentAccessible();
|
||||
}
|
||||
protected vector ExpectedExit() { return m_GroundPosition; }
|
||||
protected string ExitResultDetail() { return "unloaded survivor on ground and seat accessible"; }
|
||||
// now is world milliseconds, matching the other native probes. Returns -1/0/1.
|
||||
int Tick(float now)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (!Check(Replication.IsServer() && controller && m_Mode, "server and local real caller available")) return -1;
|
||||
m_Caller = controller.GetControlledEntity();
|
||||
m_Vehicle = m_Mode.ExtractionVehicle();
|
||||
if (!Check(ERR_Actors.CanFight(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller) && ERR_Actors.Alive(m_Vehicle), "caller on foot and authored helicopter intact")) return -1;
|
||||
m_GroundPosition = ERR_Actors.Ground(m_Vehicle.GetOrigin() + "6 0 6");
|
||||
m_Casualty = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, m_GroundPosition);
|
||||
if (!Check(m_Casualty != null, "independent casualty spawned on ground")) return -1;
|
||||
m_Mode.Own(m_Casualty);
|
||||
m_Access = SCR_CompartmentAccessComponent.Cast(m_Casualty.FindComponent(SCR_CompartmentAccessComponent));
|
||||
m_Damage = SCR_CharacterDamageManagerComponent.Cast(m_Casualty.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!Check(m_Access && m_Damage, "native casualty components present")) return -1;
|
||||
m_Access.GetOnCompartmentEntered().Insert(Entered);
|
||||
m_Access.GetOnCompartmentLeft().Insert(Left);
|
||||
m_Caller.GetWorldTransform(m_CallerTransform);
|
||||
vector helperTransform[4];
|
||||
m_Caller.GetWorldTransform(helperTransform);
|
||||
helperTransform[3] = ERR_Actors.Ground(m_GroundPosition + "2 0 0");
|
||||
ChimeraCharacter.Cast(m_Caller).Teleport(helperTransform);
|
||||
m_CallerMoved = true;
|
||||
m_Step = 1;
|
||||
m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!IncapacitateGroundCasualty()) return -1;
|
||||
m_Deadline = now + 8000;
|
||||
m_Step = 2;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!DownOnGround())
|
||||
{
|
||||
if (now >= m_Deadline) { Check(false, "native health-driven ground incapacitation before boarding deadline"); return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (m_Bench)
|
||||
{
|
||||
if (m_DownSettledAt == 0)
|
||||
{
|
||||
m_DownSettledAt = now + 5000;
|
||||
m_Deadline = now + 13000;
|
||||
Trace("incapacitated; begin five-second bench settling interval");
|
||||
return 0;
|
||||
}
|
||||
if (now < m_DownSettledAt) return 0;
|
||||
}
|
||||
PrintFormat("ERR CASUALTY POSITIONS: caller=%1 casualty=%2 helicopter=%3", m_Caller.GetOrigin(), m_Casualty.GetOrigin(), m_Vehicle.GetOrigin());
|
||||
if (!Check(ERR_Actors.CanFight(m_Caller) && vector.Distance(m_Caller.GetOrigin(), m_Casualty.GetOrigin()) < 4, "conscious real caller physically beside ground casualty")) return -1;
|
||||
if (m_Bench) m_Seat = FindTransportBench();
|
||||
else m_Seat = m_Access.FindFreeAndAccessibleCompartment(m_Vehicle, ECompartmentType.CARGO);
|
||||
if (!Check(m_Seat && !SCR_PatientCompartmentSlot.Cast(m_Seat) && !m_Seat.GetOccupant(), "ordinary free cargo seat selected without patient-seat substitution")) return -1;
|
||||
if (m_Bench) Print("ERR CASUALTY BENCH SOURCE: VehPart_MI8_seats_base.et sha256=1b9a2e55a9a1cb884886488b6e170f437b6c1a615b584449819b96a6c78ad973");
|
||||
Trace("before selected boarding request");
|
||||
PrintFormat("ERR CASUALTY LOAD: CanGetInVehicle=%1 state=%2 targetType=%3", m_Access.CanGetInVehicle(m_Seat.GetOwner()), CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent)).GetLifeState(), m_Seat.GetType());
|
||||
// Exactly the public server-to-owner primitive used by vanilla ambulance loading.
|
||||
bool accepted;
|
||||
if (m_Direct) accepted = m_Access.GetInVehicle(m_Seat.GetOwner(), m_Seat, true, -1, ECloseDoorAfterActions.INVALID, false);
|
||||
else accepted = m_Access.MoveInVehicle(m_Vehicle, ECompartmentType.CARGO, false, m_Seat);
|
||||
PrintFormat("ERR CASUALTY MECHANISM: direct=%1 bench=%2 accepted=%3", m_Direct, m_Bench, accepted);
|
||||
Trace("immediately after selected boarding request");
|
||||
m_NextDiagnostic = now + 1000;
|
||||
if (!Check(accepted, "selected loading request accepted while already unconscious")) return -1;
|
||||
m_Deadline = now + 8000;
|
||||
m_Step = 3;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (now >= m_NextDiagnostic) { Trace("awaiting physical occupancy"); m_NextDiagnostic = now + 1000; }
|
||||
if (CompartmentAccessComponent.GetVehicleIn(m_Casualty) != m_Vehicle || m_Seat.GetOccupant() != m_Casualty)
|
||||
{
|
||||
if (now >= m_Deadline) { Check(false, "physical cargo occupancy after selected mechanism before deadline"); return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (!Check(ERR_Actors.Alive(m_Casualty) && !ERR_Actors.CanFight(m_Casualty), "living unconscious ground casualty now physically aboard")) return -1;
|
||||
if (!Check(!m_Access.MoveInVehicle(m_Vehicle, ECompartmentType.CARGO, false, m_Seat), "occupied target seat rejects repeated request")) return -1;
|
||||
ERR_PlayerRoster roster = new ERR_PlayerRoster();
|
||||
UUID account = "00e77200-0000-0000-0000-000000000201";
|
||||
if (!Check(roster.Bind(10201, account), "isolated casualty identity registered")) return -1;
|
||||
roster.Spawned(10201, m_Casualty);
|
||||
ERR_ExtractionService service = new ERR_ExtractionService(roster);
|
||||
if (!Check(service.Assign(m_Vehicle) && service.Begin(m_Vehicle.GetOrigin() + "100 0 0", 50), "isolated boundary service initialized without moving mission helicopter")) return -1;
|
||||
service.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(service.DidEscape(account) && service.EscapedCount() == 1, "loaded unconscious identity satisfies physical extraction rule")) return -1;
|
||||
if (!BeginExit()) return -1;
|
||||
m_Deadline = now + 8000;
|
||||
m_Step = 4;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (now >= m_NextDiagnostic) { Trace("awaiting ground exit"); m_NextDiagnostic = now + 1000; }
|
||||
if (CompartmentAccessComponent.GetVehicleIn(m_Casualty) || m_Seat.GetOccupant())
|
||||
{
|
||||
if (now >= m_Deadline) { Check(false, "native exit clears physical vehicle and seat before deadline"); return -1; }
|
||||
return 0;
|
||||
}
|
||||
bool settled = ExitSettled();
|
||||
if (!settled && now < m_Deadline) return 0;
|
||||
PrintFormat("ERR CASUALTY EXIT POSITION: actual=%1 requested=%2 alive=%3 accessible=%4", m_Casualty.GetOrigin(), ExpectedExit(), ERR_Actors.Alive(m_Casualty), m_Seat.IsCompartmentAccessible());
|
||||
if (!Check(settled, ExitResultDetail())) return -1;
|
||||
Cleanup();
|
||||
m_Step = 5;
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
#ifdef WORKBENCH
|
||||
// Opt-in completion-boundary test of the REAL inherited stock remove action.
|
||||
// It does not simulate held input, replace the action, or prove remote ownership.
|
||||
class ERR_NativeCasualtyRemoveProbe : ERR_NativeCasualtyLoadProbe
|
||||
{
|
||||
protected SCR_RemoveCasualtyUserAction m_Remove;
|
||||
protected UserActionContext m_Context;
|
||||
protected int m_SavedDoor;
|
||||
protected int m_SelectedDoor;
|
||||
protected bool m_DoorChanged;
|
||||
protected bool m_PendingAction;
|
||||
protected bool m_TestBodyProtected;
|
||||
protected bool m_RemoveStopped;
|
||||
protected bool m_RemoveComplete;
|
||||
protected vector m_AuthoredExit;
|
||||
protected vector m_SeatedPosition;
|
||||
protected vector m_HelperPosition;
|
||||
protected float m_RemoveDeadline;
|
||||
protected float m_RemoveDiagnostic;
|
||||
void ERR_NativeCasualtyRemoveProbe(ERR_GameMode mode, bool direct = false, bool bench = false)
|
||||
{
|
||||
m_Mode = mode; m_Direct = true; m_Bench = true;
|
||||
}
|
||||
override void Cleanup()
|
||||
{
|
||||
if (m_Remove && m_DoorChanged) m_Remove.SetCompartmentDoorInfoIndex(m_SavedDoor);
|
||||
m_DoorChanged = false; m_PendingAction = false; m_RemoveStopped = true;
|
||||
super.Cleanup();
|
||||
}
|
||||
protected void FindAction(IEntity entity)
|
||||
{
|
||||
if (!entity || m_Remove) return;
|
||||
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
array<BaseUserAction> actions = {}; manager.GetActionsList(actions);
|
||||
foreach (BaseUserAction action : actions)
|
||||
{
|
||||
SCR_RemoveCasualtyUserAction remove = SCR_RemoveCasualtyUserAction.Cast(action);
|
||||
if (remove && remove.GetCompartmentSlot() == m_Seat && remove.GetContextIndex("door_l01_door") >= 0)
|
||||
{
|
||||
m_Remove = remove; return;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (IEntity child = entity.GetChildren(); child; child = child.GetSibling()) FindAction(child);
|
||||
}
|
||||
protected void FindContext(IEntity entity)
|
||||
{
|
||||
if (!entity || m_Context) return;
|
||||
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(entity.FindComponent(BaseActionsManagerComponent));
|
||||
if (manager)
|
||||
{
|
||||
UserActionContext context = manager.GetContext("door_l01_door");
|
||||
if (context)
|
||||
{
|
||||
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
|
||||
if (actions.Contains(m_Remove)) { m_Context = context; return; }
|
||||
}
|
||||
}
|
||||
for (IEntity child = entity.GetChildren(); child; child = child.GetSibling()) FindContext(child);
|
||||
}
|
||||
protected CompartmentDoorInfo ResolveDoor()
|
||||
{
|
||||
BaseCompartmentManagerComponent manager = m_Seat.GetManager();
|
||||
if (!manager) return null;
|
||||
CompartmentDoorInfo info = manager.GetDoorInfo(m_SelectedDoor);
|
||||
if (info) return info;
|
||||
CompartmentDoorReference doorReference = manager.GetDoorReference(m_SelectedDoor);
|
||||
if (!doorReference) return null;
|
||||
BaseCompartmentManagerComponent referenced = doorReference.GetReferencedDoorCompartmentManager();
|
||||
int referencedIndex = doorReference.GetAnimDoorIndex();
|
||||
PrintFormat("ERR CASUALTY REMOVE DOOR REFERENCE: selected=%1 referencedIndex=%2 manager=%3", m_SelectedDoor, referencedIndex, referenced != null);
|
||||
if (!referenced || referencedIndex < 0) return null;
|
||||
return referenced.GetDoorInfo(referencedIndex);
|
||||
}
|
||||
protected override bool BeginExit()
|
||||
{
|
||||
FindAction(m_Vehicle);
|
||||
if (!Check(m_Remove && BelongsToAircraft(m_Remove.GetOwner()) && m_Remove.GetActionDuration() == 1 && !m_Remove.HasLocalEffectOnly(), "stock bench RemoveCasualty action exists, duration one second, nonlocal effect")) return false;
|
||||
FindContext(m_Vehicle);
|
||||
if (!Check(m_Context && m_Context.GetContextName() == "door_l01_door", "actual exterior context contains selected stock seat action")) return false;
|
||||
// Same native point-to-door selection used by SCR_CompartmentUserAction.
|
||||
// Explicit cached index substitutes only for selecting a UI context, not input.
|
||||
m_SelectedDoor = m_Seat.PickDoorIndexForPoint(m_Context.GetOrigin());
|
||||
if (!Check(m_SelectedDoor >= 0, "actual action context resolves a relevant native compartment door")) return false;
|
||||
CompartmentDoorInfo door = ResolveDoor();
|
||||
if (!Check(door && door.GetExitTeleportPointInfo(), "selected native door exposes its authored teleport exit")) return false;
|
||||
vector exitTransform[4]; door.GetExitTeleportPointInfo().GetWorldTransform(exitTransform);
|
||||
m_AuthoredExit = exitTransform[3]; m_SeatedPosition = m_Casualty.GetOrigin();
|
||||
if (!Check(vector.Distance(m_AuthoredExit, m_Vehicle.GetOrigin()) < 15, "authored exit resolves beside this physical helicopter")) return false;
|
||||
m_SavedDoor = m_Remove.GetCompartmentDoorInfoIndex();
|
||||
m_Remove.SetCompartmentDoorInfoIndex(m_SelectedDoor); m_DoorChanged = true;
|
||||
vector outside = m_Context.GetOrigin() - m_Vehicle.GetOrigin(); outside[1] = 0;
|
||||
if (!Check(outside.Length() > 0.1, "exterior context has a distinct outward direction")) return false;
|
||||
outside.Normalize();
|
||||
m_HelperPosition = ERR_Actors.Ground(m_Context.GetOrigin() + outside * 0.75);
|
||||
vector helper[4]; m_Caller.GetWorldTransform(helper); helper[3] = m_HelperPosition;
|
||||
ChimeraCharacter.Cast(m_Caller).Teleport(helper);
|
||||
PrintFormat("ERR CASUALTY REMOVE SETUP: action=%1 context=%2 door=%3 contextOrigin=%4 exit=%5 seatedOrigin=%6 helperRequested=%7", m_Remove.GetActionID(), m_Context.GetContextName(), m_SelectedDoor, m_Context.GetOrigin(), m_AuthoredExit, m_SeatedPosition, m_HelperPosition);
|
||||
m_PendingAction = true;
|
||||
m_Wait = GetGame().GetWorld().GetWorldTime() + 1000;
|
||||
return true;
|
||||
}
|
||||
static bool SafeExit(float horizontalDistance, float terrainClearance)
|
||||
{
|
||||
return horizontalDistance >= 0 && horizontalDistance <= 1.25 && terrainClearance >= -0.25 && terrainClearance <= 1.5;
|
||||
}
|
||||
protected override vector ExpectedExit() { return m_AuthoredExit; }
|
||||
protected override string ExitResultDetail() { return "stock removal leaves a living incapacitated body beside authored door exit, outside compartment, on terrain, and seat accessible"; }
|
||||
protected override bool ExitSettled()
|
||||
{
|
||||
if (!m_Casualty || !m_Access || !m_Seat) return false;
|
||||
vector actual = m_Casualty.GetOrigin();
|
||||
float height = actual[1] - GetGame().GetWorld().GetSurfaceY(actual[0], actual[2]);
|
||||
CharacterAnimationComponent animation = CharacterAnimationComponent.Cast(m_Casualty.FindComponent(CharacterAnimationComponent));
|
||||
bool safe = DownOnGround() && !m_Access.IsGettingOut() && !m_Access.IsInCompartment() && !m_Seat.GetOccupant() && m_Seat.IsCompartmentAccessible() && animation && !animation.PhysicsIsFalling() && SafeExit(vector.DistanceXZ(actual, m_AuthoredExit), height);
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now >= m_RemoveDiagnostic || safe)
|
||||
{
|
||||
PrintFormat("ERR CASUALTY REMOVE POSITION: actual=%1 aiming=%2 authored=%3 seated=%4 horizontalError=%5 terrainClearance=%6 safe=%7", actual, ChimeraCharacter.Cast(m_Casualty).AimingPosition(), m_AuthoredExit, m_SeatedPosition, vector.DistanceXZ(actual, m_AuthoredExit), height, safe);
|
||||
Trace("stock door removal observation"); m_RemoveDiagnostic = now + 1000;
|
||||
}
|
||||
return safe;
|
||||
}
|
||||
override int Tick(float now)
|
||||
{
|
||||
if (m_RemoveComplete) return 1;
|
||||
if (m_RemoveStopped) return -1;
|
||||
if (m_RemoveDeadline == 0)
|
||||
{
|
||||
if (!Check(m_Mode && ERR_CasualtyLoadingRules.AircraftReady(m_Mode.ExtractionVehicle()), "production mission helicopter is physically grounded and stable before fixture")) return -1;
|
||||
m_RemoveDeadline = now + 45000;
|
||||
}
|
||||
if (now > m_RemoveDeadline) { Check(false, "bounded 45-second stock removal fixture deadline"); return -1; }
|
||||
if (m_Step == 4 && m_PendingAction)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (!Check(ERR_Actors.CanFight(m_Caller) && !CompartmentAccessComponent.GetVehicleIn(m_Caller) && vector.Distance(m_Caller.GetOrigin(), m_HelperPosition) < 1.5, "real conscious rescuer settled outside selected native door")) return -1;
|
||||
bool shown = m_Remove.CanBeShown(m_Caller);
|
||||
bool permitted = m_Remove.CanBePerformed(m_Caller);
|
||||
PrintFormat("ERR CASUALTY REMOVE ACTION: shown=%1 performedAllowed=%2 reason=%3 cachedDoor=%4 caller=%5", shown, permitted, m_Remove.GetCannotPerformReason(), m_Remove.GetCompartmentDoorInfoIndex(), m_Caller.GetOrigin());
|
||||
if (!Check(shown && permitted && m_Seat.GetOccupant() == m_Casualty && m_Remove.GetCompartmentDoorInfoIndex() == m_SelectedDoor, "stock native action visibility and permission accept actual rescuer and occupied casualty bench")) return -1;
|
||||
m_PendingAction = false; // exactly one completion invocation, no native retry
|
||||
m_Remove.PerformAction(m_Remove.GetOwner(), m_Caller);
|
||||
Print("ERR CASUALTY REMOVE: invoked existing stock action completion once; held input and network dispatch not exercised");
|
||||
m_Deadline = now + 8000;
|
||||
}
|
||||
int result = super.Tick(now);
|
||||
if (!m_TestBodyProtected && m_Casualty && m_Damage)
|
||||
{
|
||||
// Only the fixture-owned casualty is protected from nearby initial guards.
|
||||
// ForceUnconsciousness still uses its actual native health/life transition.
|
||||
m_Damage.EnableDamageHandling(false); m_TestBodyProtected = true;
|
||||
}
|
||||
if (result == 1) m_RemoveComplete = true;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
#ifdef WORKBENCH
|
||||
// Only the helper's membership is synthetic. The casualty keeps its real native
|
||||
// player controller/account/body and exercises the ordinary owner handshake.
|
||||
class ERR_NativeCasualtyHelperAdapter : ERR_CasualtyLoadingService
|
||||
{
|
||||
protected ref ERR_PlayerLife m_HelperLife;
|
||||
protected bool m_HelperEnabled;
|
||||
void ERR_NativeCasualtyHelperAdapter(ERR_GameMode mode) { m_Mode = mode; }
|
||||
void Helper(IEntity actor)
|
||||
{
|
||||
m_HelperLife = new ERR_PlayerLife();
|
||||
m_HelperLife.Actor = actor; m_HelperLife.Connected = true; m_HelperLife.UsedLife = true;
|
||||
m_HelperLife.ConnectionId = 15401;
|
||||
m_HelperLife.AccountId = "00e77200-0000-0000-0000-000000001401";
|
||||
}
|
||||
void EnableHelper() { m_HelperEnabled = true; }
|
||||
bool StrictParticipant(IEntity actor) { return super.CurrentLife(actor) != null; }
|
||||
bool StoppedForProbe() { return m_Stopped; }
|
||||
protected override ERR_PlayerLife CurrentLife(IEntity actor)
|
||||
{
|
||||
if (m_HelperEnabled && m_HelperLife && actor && actor == m_HelperLife.Actor) return m_HelperLife;
|
||||
return super.CurrentLife(actor);
|
||||
}
|
||||
}
|
||||
class ERR_NativeCasualtyServiceProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected IEntity m_Helper;
|
||||
protected IEntity m_Blocker;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected ERR_LoadCasualtyAction m_Action;
|
||||
protected ref ERR_NativeCasualtyHelperAdapter m_Service;
|
||||
protected ref ERR_NativeCasualtyGroundProbe m_GroundProbe;
|
||||
protected bool m_GroundChecked;
|
||||
protected ref array<BaseCompartmentSlot> m_Reserved = {};
|
||||
protected BaseCompartmentSlot m_LoadedSeat;
|
||||
protected float m_Deadline;
|
||||
protected float m_Wait;
|
||||
protected float m_PositionDeadline;
|
||||
protected float m_NextDiagnostic;
|
||||
protected vector m_RequestedPlayer;
|
||||
protected vector m_RequestedHelper;
|
||||
protected bool m_PlayerDamageSnapshot;
|
||||
protected bool m_PlayerDamageOriginallyEnabled;
|
||||
protected int m_SettleStart;
|
||||
protected int m_Step;
|
||||
protected int m_Resolutions;
|
||||
protected bool m_Loaded;
|
||||
protected bool m_Observing;
|
||||
protected bool m_Stopped;
|
||||
void ERR_NativeCasualtyServiceProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: casualty service " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: casualty service " + detail, LogLevel.ERROR);
|
||||
Stop(); return false;
|
||||
}
|
||||
protected void Resolved(IEntity actor, bool loaded)
|
||||
{
|
||||
m_Resolutions++;
|
||||
m_Loaded = actor == m_Player && loaded;
|
||||
}
|
||||
protected void ReleaseFixtureReservations()
|
||||
{
|
||||
foreach (BaseCompartmentSlot seat : m_Reserved)
|
||||
if (seat && m_Helper && seat.IsReservedBy(m_Helper)) seat.SetReserved(null);
|
||||
m_Reserved.Clear();
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_GroundProbe) m_GroundProbe.Stop();
|
||||
if (m_Service) { m_Service.Stop(); m_Service.OnResolved().Remove(Resolved); }
|
||||
if (m_Mode) m_Mode.ERR_UseCasualtyFixture(null);
|
||||
ReleaseFixtureReservations();
|
||||
// Restore the original damage-handling flag only on this exact still-owned
|
||||
// living player body. This never restores health or changes another actor.
|
||||
if (m_PlayerDamageSnapshot && GetGame() && GetGame().GetPlayerController() && GetGame().GetPlayerController().GetControlledEntity() == m_Player && ERR_Actors.Alive(m_Player))
|
||||
{
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
|
||||
if (damage) damage.EnableDamageHandling(m_PlayerDamageOriginallyEnabled);
|
||||
}
|
||||
m_PlayerDamageSnapshot = false;
|
||||
ERR_Actors.DeleteSpawned(m_Blocker); m_Blocker = null;
|
||||
ERR_Actors.DeleteSpawned(m_Helper); m_Helper = null;
|
||||
// This opt-in case ends by ordinary round reset. Never restore arbitrary
|
||||
// health, force recovery, teleport the seated player, or delete its body.
|
||||
}
|
||||
protected ERR_LoadCasualtyAction FindAction()
|
||||
{
|
||||
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(m_Player.FindComponent(BaseActionsManagerComponent));
|
||||
if (!manager) return null;
|
||||
array<BaseUserAction> actions = {}; manager.GetActionsList(actions);
|
||||
bool medical;
|
||||
ERR_LoadCasualtyAction load;
|
||||
foreach (BaseUserAction action : actions)
|
||||
{
|
||||
if (SCR_BandageUserAction.Cast(action)) medical = true;
|
||||
ERR_LoadCasualtyAction candidate = ERR_LoadCasualtyAction.Cast(action);
|
||||
if (candidate) load = candidate;
|
||||
}
|
||||
if (!Check(medical, "successor prisoner retains inherited native medicine actions")) return null;
|
||||
return load;
|
||||
}
|
||||
protected void Diagnose()
|
||||
{
|
||||
if (!m_Player || !m_Helper || !m_Aircraft || !m_Service) return;
|
||||
HelicopterControllerComponent aircraftController = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
bool groundContact;
|
||||
bool simulationAvailable;
|
||||
float speed = -1;
|
||||
float angularSpeed = -1;
|
||||
if (aircraftController && aircraftController.GetSimulation())
|
||||
{
|
||||
simulationAvailable = true;
|
||||
groundContact = aircraftController.GetSimulation().HasAnyGroundContact();
|
||||
}
|
||||
if (physics) { speed = physics.GetVelocity().Length(); angularSpeed = physics.GetAngularVelocity().Length(); }
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
PrintFormat("ERR CASUALTY AIRCRAFT READINESS: alive=%1 controller=%2 simulation=%3 physics=%4 groundContact=%5 up=%6 speed=%7 angularSpeed=%8 position=%9", ERR_Actors.Alive(m_Aircraft), aircraftController != null, simulationAvailable, physics != null, groundContact, transform[1][1], speed, angularSpeed, m_Aircraft.GetOrigin());
|
||||
CharacterControllerComponent playerController = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||
CharacterControllerComponent helperController = CharacterControllerComponent.Cast(m_Helper.FindComponent(CharacterControllerComponent));
|
||||
int playerLife = -1;
|
||||
int helperLife = -1;
|
||||
if (playerController) playerLife = playerController.GetLifeState();
|
||||
if (helperController) helperLife = helperController.GetLifeState();
|
||||
PrintFormat("ERR CASUALTY ACTOR READINESS: playerAlive=%1 playerConscious=%2 playerLife=%3 helperAlive=%4 helperConscious=%5 helperLife=%6 pairRange=%7 playerAircraftRange=%8 helperAircraftRange=%9", ERR_Actors.Alive(m_Player), ERR_Actors.CanFight(m_Player), playerLife, ERR_Actors.Alive(m_Helper), ERR_Actors.CanFight(m_Helper), helperLife, vector.Distance(m_Player.GetOrigin(), m_Helper.GetOrigin()), vector.Distance(m_Player.GetOrigin(), m_Aircraft.GetOrigin()), vector.Distance(m_Helper.GetOrigin(), m_Aircraft.GetOrigin()));
|
||||
PrintFormat("ERR CASUALTY POSITION READINESS: playerRequested=%1 playerActual=%2 playerError=%3 helperRequested=%4 helperActual=%5 helperError=%6", m_RequestedPlayer, m_Player.GetOrigin(), vector.Distance(m_RequestedPlayer, m_Player.GetOrigin()), m_RequestedHelper, m_Helper.GetOrigin(), vector.Distance(m_RequestedHelper, m_Helper.GetOrigin()));
|
||||
ERR_CasualtyGroundSupport.Parked(m_Aircraft, true);
|
||||
PrintFormat("ERR CASUALTY SERVICE STATE: step=%1 player=%2 helper=%3 groundedTarget=%4 aircraftReady=%5 sight=%6 pending=%7 resolutions=%8 loaded=%9", m_Step, m_Player.GetOrigin(), m_Helper.GetOrigin(), ERR_CasualtyLoadingRules.GroundSettled(m_Player), ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft), ERR_CasualtyLoadingRules.LineOfSight(m_Helper, m_Player), m_Service.PendingCount(), m_Resolutions, m_Loaded);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (m_Deadline > 0 && now > m_Deadline) { if (m_Player && m_Helper && m_Service) Diagnose(); Check(false, "bounded native fixture deadline"); return -1; }
|
||||
if (m_Service && m_Observing) m_Service.Tick(now);
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (!Check(Replication.IsServer() && controller && m_Mode.Phase() == ERR_Phase.PRISON, "explicit local server fixture starts with real prisoner")) return -1;
|
||||
m_Player = controller.GetControlledEntity(); m_Aircraft = m_Mode.ExtractionVehicle();
|
||||
if (!Check(ERR_Actors.CanFight(m_Player) && m_Aircraft && !CompartmentAccessComponent.GetVehicleIn(m_Player), "real controlled casualty starts conscious on foot")) return -1;
|
||||
m_Action = FindAction();
|
||||
if (!Check(m_Action && m_Action.GetOwner() == m_Player && m_Action.GetActionDuration() == 5 && !m_Action.HasLocalEffectOnly(), "real successor prisoner owns five-second server action")) return -1;
|
||||
DamageManagerComponent playerDamage = DamageManagerComponent.Cast(m_Player.FindComponent(DamageManagerComponent));
|
||||
if (!Check(playerDamage != null, "fixture player damage manager available")) return -1;
|
||||
m_PlayerDamageOriginallyEnabled = playerDamage.IsDamageHandlingEnabled();
|
||||
m_PlayerDamageSnapshot = true;
|
||||
playerDamage.EnableDamageHandling(false);
|
||||
m_RequestedPlayer = ERR_Actors.Ground(m_Aircraft.GetOrigin() + "6 0 6");
|
||||
m_RequestedHelper = ERR_Actors.Ground(m_RequestedPlayer + "0 0 2");
|
||||
m_Helper = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, m_RequestedHelper);
|
||||
if (!Check(m_Helper != null, "separate physical helper spawned")) return -1;
|
||||
m_Mode.Own(m_Helper);
|
||||
DamageManagerComponent helperDamage = DamageManagerComponent.Cast(m_Helper.FindComponent(DamageManagerComponent));
|
||||
if (!Check(helperDamage != null, "fixture helper damage manager available")) return -1;
|
||||
helperDamage.EnableDamageHandling(false);
|
||||
vector transform[4]; m_Player.GetWorldTransform(transform); transform[3] = m_RequestedPlayer;
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
m_Service = new ERR_NativeCasualtyHelperAdapter(m_Mode); m_Service.Helper(m_Helper);
|
||||
m_Service.OnResolved().Insert(Resolved);
|
||||
if (!Check(m_Service.StrictParticipant(m_Player) && !m_Service.StrictParticipant(m_Helper), "actual target passes strict native membership; synthetic helper does not")) return -1;
|
||||
if (!Check(!m_Mode.CasualtyLoading().Begin(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player), "production and unadapted sample reject out-of-phase helper request")) return -1;
|
||||
// Deliberate fixture phase setup, not evidence of a completed radio objective.
|
||||
if (!Check(m_Mode.Transition(ERR_Phase.RADIO_SEARCH) && m_Mode.Transition(ERR_Phase.RADIO_DEFENCE) && m_Mode.Transition(ERR_Phase.EXTRACTION), "fixture enters extraction through explicit legal phase transitions")) return -1;
|
||||
m_Deadline = now + 45000;
|
||||
m_PositionDeadline = now + 10000;
|
||||
m_NextDiagnostic = now;
|
||||
m_Wait = now + 500; m_Step = 1; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (now >= m_NextDiagnostic) { Diagnose(); m_NextDiagnostic = now + 1000; }
|
||||
bool actorsReady = ERR_Actors.CanFight(m_Player) && ERR_Actors.CanFight(m_Helper) && vector.Distance(m_RequestedPlayer, m_Player.GetOrigin()) <= 1.5 && vector.Distance(m_RequestedHelper, m_Helper.GetOrigin()) <= 1.5 && vector.Distance(m_Player.GetOrigin(), m_Helper.GetOrigin()) <= 3;
|
||||
bool aircraftReady = ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
|
||||
if ((!actorsReady || !aircraftReady) && now < m_PositionDeadline) return 0;
|
||||
if (!Check(actorsReady, "actual living conscious actors reached requested nearby ground positions before deadline")) return -1;
|
||||
if (!Check(aircraftReady, "actual aircraft satisfies unchanged production grounded-motion safety before deadline")) return -1;
|
||||
if (!m_GroundChecked)
|
||||
{
|
||||
if (!m_GroundProbe) m_GroundProbe = new ERR_NativeCasualtyGroundProbe(m_Mode);
|
||||
int groundResult = m_GroundProbe.Tick(now);
|
||||
if (groundResult < 0) { Check(false, "native ground-support qualification failed"); return -1; }
|
||||
if (groundResult == 0) return 0;
|
||||
m_GroundChecked = true;
|
||||
}
|
||||
if (!Check(!m_Service.Begin(m_Helper, m_Player), "conscious target is rejected before native incapacitation")) return -1;
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Player.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
if (!Check(damage != null, "real casualty has native health manager")) return -1;
|
||||
HitZone resilience = damage.GetResilienceHitZone();
|
||||
if (!Check(resilience && damage.GetPermitUnconsciousness(), "native resilience and unconsciousness policy available")) return -1;
|
||||
float beforeResilience = resilience.GetHealth();
|
||||
// Protection must not swallow the fixture's own true-damage request. Keep
|
||||
// this synchronous interval limited to that one native health operation.
|
||||
damage.EnableDamageHandling(true);
|
||||
damage.ForceUnconsciousness();
|
||||
float afterResilience = resilience.GetHealth();
|
||||
damage.EnableDamageHandling(false);
|
||||
PrintFormat("ERR CASUALTY INCAPACITATION: resilienceBefore=%1 resilienceAfter=%2", beforeResilience, afterResilience);
|
||||
if (!Check(afterResilience < beforeResilience && ERR_Actors.Alive(m_Player), "single native resilience damage applied before restoring fixture protection")) return -1;
|
||||
m_Step = 2; m_Wait = now + 500; return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!ERR_CasualtyLoadingRules.GroundSettled(m_Player))
|
||||
{
|
||||
if (now >= m_NextDiagnostic) { Diagnose(); m_NextDiagnostic = now + 1000; }
|
||||
return 0;
|
||||
}
|
||||
if (!Check(ERR_CasualtyLoadingRules.Nearby(m_Mode, m_Helper, m_Player), "actual incapacitated target is settled with clear line of sight")) return -1;
|
||||
if (!Check(!m_Mode.CasualtyLoading().CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player) && m_Service.PendingCount() == 0, "strict production authorization rejects nonparticipant helper even in valid physical state")) return -1;
|
||||
m_Service.EnableHelper();
|
||||
if (!Check(m_Mode.ERR_UseCasualtyFixture(m_Service), "explicit fixture installs helper-only membership adapter")) return -1;
|
||||
m_Observing = true; m_Service.Tick(now); m_SettleStart = System.GetTickCount();
|
||||
if (!Check(!m_Service.CanLoad(m_Helper, m_Player), "new observation cannot bypass five-second settling window")) return -1;
|
||||
m_Step = 3; return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (System.GetTickCount() - m_SettleStart < 5500) return 0;
|
||||
Diagnose();
|
||||
if (!Check(m_Service.CanLoad(m_Helper, m_Player), "settled real casualty is eligible with only helper membership adapted")) return -1;
|
||||
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
{
|
||||
if (!ERR_CasualtyLoadingRules.TransportBench(seat, m_Aircraft) || seat.GetOccupant() || seat.IsReserved()) continue;
|
||||
seat.SetReserved(m_Helper); m_Reserved.Insert(seat);
|
||||
}
|
||||
if (!Check(!m_Reserved.IsEmpty() && !m_Service.CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player), "native external reservations prevent loading when all child benches are reserved")) return -1;
|
||||
ReleaseFixtureReservations();
|
||||
vector middle = (ChimeraCharacter.Cast(m_Helper).AimingPosition() + ChimeraCharacter.Cast(m_Player).AimingPosition()) * 0.5;
|
||||
m_Blocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", middle);
|
||||
if (!Check(m_Blocker != null, "successor-owned physical line-of-sight obstacle spawned")) return -1;
|
||||
m_Mode.Own(m_Blocker);
|
||||
vector low, high; m_Blocker.GetWorldBounds(low, high);
|
||||
vector wall[4]; m_Blocker.GetWorldTransform(wall); wall[3] = wall[3] + middle - (low + high) * 0.5;
|
||||
m_Blocker.SetWorldTransform(wall);
|
||||
m_Step = 4; m_Wait = now + 500; return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (!Check(!ERR_CasualtyLoadingRules.LineOfSight(m_Helper, m_Player) && !m_Service.CanLoad(m_Helper, m_Player) && !m_Service.Begin(m_Helper, m_Player) && m_Service.PendingCount() == 0, "physical wall blocks completion-time rescue authorization")) return -1;
|
||||
ERR_Actors.DeleteSpawned(m_Blocker); m_Blocker = null;
|
||||
m_Step = 5; m_Wait = now + 500; return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(m_Action.CanBePerformedScript(m_Helper) && m_Service.PendingCount() == 0, "real action becomes performable after obstruction removal")) return -1;
|
||||
// Invoke the real completion boundary. This does not simulate holding input
|
||||
// for five seconds or claim a second authenticated network player.
|
||||
m_Action.PerformAction(m_Player, m_Helper);
|
||||
if (!Check(m_Service.PendingCount() == 1 && m_Resolutions == 0, "real owner handshake commits one pending physical load")) return -1;
|
||||
array<BaseCompartmentSlot> seats = {}; m_Mode.Extraction().Seats(seats);
|
||||
foreach (BaseCompartmentSlot seat : seats)
|
||||
if (ERR_CasualtyLoadingRules.TransportBench(seat, m_Aircraft) && seat.IsReservedBy(m_Player)) m_LoadedSeat = seat;
|
||||
if (!Check(m_LoadedSeat != null, "service reserves the actual selected child bench for the casualty")) return -1;
|
||||
m_Action.PerformAction(m_Player, m_Helper);
|
||||
if (!Check(m_Service.PendingCount() == 1 && m_Resolutions == 0, "duplicate completion cannot issue a second ticket")) return -1;
|
||||
m_Step = 6; return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
if (m_Service.PendingCount() > 0) return 0;
|
||||
Diagnose();
|
||||
if (!Check(m_Loaded && m_Resolutions == 1 && ERR_Actors.Alive(m_Player) && ERR_CasualtyLoadingRules.Down(m_Player) && CompartmentAccessComponent.GetVehicleIn(m_Player) == m_Aircraft && m_LoadedSeat.GetOccupant() == m_Player, "production owner handshake physically loads real living incapacitated player exactly once")) return -1;
|
||||
if (!Check(!m_LoadedSeat.IsReserved() && m_Service.StrictParticipant(m_Player), "successful loading releases reservation and preserves actual player identity")) return -1;
|
||||
m_Service.Stop(); m_Service.Stop(); m_Service.Tick(now);
|
||||
if (!Check(m_Service.StoppedForProbe() && m_Service.PendingCount() == 0 && m_Resolutions == 1 && !m_Service.Begin(m_Helper, m_Player), "Stop is latched, idempotent and cannot repeat completion or restart loading")) return -1;
|
||||
Stop(); return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
protected ref ERR_NativeCasualtyHelperAdapter m_ERRNativeCasualtyAdapter;
|
||||
bool ERR_UseCasualtyFixture(ERR_NativeCasualtyHelperAdapter service)
|
||||
{
|
||||
string enabled, outcome;
|
||||
if (!Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1" || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "casualty-service") return false;
|
||||
m_ERRNativeCasualtyAdapter = service; return true;
|
||||
}
|
||||
override ERR_CasualtyLoadingService CasualtyLoading()
|
||||
{
|
||||
if (m_ERRNativeCasualtyAdapter) return m_ERRNativeCasualtyAdapter;
|
||||
return super.CasualtyLoading();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
#ifdef WORKBENCH
|
||||
// Exercises native service dispatch, not remote RPC ownership or rendered UI.
|
||||
class ERR_NativeContractsProbe
|
||||
{
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: base contracts " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: base contracts " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
static bool Run(ERR_GameMode mode, IEntity player)
|
||||
{
|
||||
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;
|
||||
ERR_FactionCatalog isolated = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BE50B97B8E2B36E6}Configs/ERR/VanillaFactions.conf"));
|
||||
if (!isolated) return false;
|
||||
string enemyKey = isolated.EnemyFactionKey;
|
||||
isolated.EnemyFactionKey = isolated.PlayerFactionKey;
|
||||
if (!Check(!isolated.Valid() && !isolated.WorldReady(), "same-side factions rejected")) return false;
|
||||
isolated.EnemyFactionKey = "ERR_MISSING_FACTION";
|
||||
if (!Check(!isolated.WorldReady(), "unknown world faction rejected")) return false;
|
||||
isolated.EnemyFactionKey = enemyKey;
|
||||
ResourceName prisoner = isolated.PrisonerPrefab;
|
||||
isolated.PrisonerPrefab = "";
|
||||
if (!Check(!isolated.Valid(), "missing prisoner resource rejected")) return false;
|
||||
isolated.PrisonerPrefab = prisoner;
|
||||
if (!Check(isolated.WorldReady(), "isolated catalog restored without mutating live configuration")) return false;
|
||||
|
||||
array<string> replies = {};
|
||||
int original = mode.Settings().Get("radioDefenceSeconds");
|
||||
mode.Admin().Execute(mode, false, "mission radio set 60", replies);
|
||||
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies.Count() == 1 && replies[0] == "Administrator access required.", "unauthorized settings request rejected")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission radio set 60junk", replies);
|
||||
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies[0].Contains("Invalid"), "partially numeric input rejected without mutation")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission radio set 0", replies);
|
||||
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original && replies[0].Contains("Invalid"), "out-of-range duration rejected without mutation")) return false;
|
||||
mode.Admin().Execute(mode, true, " MISSION RADIO SET 60 ", replies);
|
||||
if (!Check(mode.Settings().Get("radioDefenceSeconds") == 60 && replies[0] == "radioDefenceSeconds=60 saved", "normalized base command saves validated duration")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission radio show", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0] == "radioDefenceSeconds=60", "base command reads saved duration")) return false;
|
||||
mode.Admin().Execute(mode, true, "settings", replies);
|
||||
if (!Check(replies.Count() == ERR_Settings.Keys().Count(), "base settings listing available")) return false;
|
||||
mode.Admin().Execute(mode, true, "status", replies);
|
||||
if (!Check(!replies.IsEmpty() && replies[0] == "Escape the prison", "base mission status available before companions")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission radio set " + original.ToString(), replies);
|
||||
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original, "test setting restored")) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeDefinitionProbe
|
||||
{
|
||||
protected static bool Check(bool condition, string name)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: definition " + name); return true; }
|
||||
Print("ERR SMOKE FAIL: definition " + name, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
static bool Run()
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
if (!Check(definition && definition.Valid(), "authored configuration resolves")) return false;
|
||||
ERR_SiteDefinition cache = definition.Find("everon-supply-1");
|
||||
if (!cache) return false;
|
||||
cache.Id = "everon-prison-1";
|
||||
if (!Check(!definition.Valid(), "duplicate site identity rejected")) return false;
|
||||
cache.Id = "everon-supply-1";
|
||||
ERR_SiteStock documents = cache.Stock[3];
|
||||
documents.TargetId = "missing-radio";
|
||||
if (!Check(!definition.Valid(), "dangling document target rejected")) return false;
|
||||
documents.TargetId = "everon-prison-1";
|
||||
if (!Check(!definition.Valid(), "non-radio document target rejected")) return false;
|
||||
documents.TargetId = "everon-radio-1";
|
||||
documents.Count = 1;
|
||||
if (!Check(!definition.Valid(), "insufficient guaranteed radio clues rejected")) return false;
|
||||
documents.Count = 65;
|
||||
if (!Check(!definition.Valid(), "unbounded stock rejected")) return false;
|
||||
documents.Count = 2;
|
||||
cache.RevealOnEscape = false;
|
||||
if (!Check(!definition.Valid(), "unguaranteed clue route rejected")) return false;
|
||||
cache.RevealOnEscape = true;
|
||||
ERR_SiteDefinition alternate = definition.Find("everon-supply-2");
|
||||
vector alternatePosition = alternate.Position;
|
||||
alternate.Position = cache.Position + "10 0 0";
|
||||
if (!Check(!definition.Valid(), "radio sources sharing one footprint rejected")) return false;
|
||||
alternate.Position = alternatePosition;
|
||||
string label = alternate.Label;
|
||||
alternate.Label = "";
|
||||
if (!Check(!definition.Valid(), "unnamed location rejected")) return false;
|
||||
alternate.Label = label;
|
||||
ERR_SiteDefinition radio = definition.Find("everon-radio-1");
|
||||
float radioRadius = radio.DiscoverRadius;
|
||||
radio.DiscoverRadius = 9;
|
||||
if (!Check(!definition.Valid(), "radio identification beyond eight metres rejected")) return false;
|
||||
radio.DiscoverRadius = 8;
|
||||
if (!Check(definition.Valid(), "close physical radio identification allowed")) return false;
|
||||
radio.DiscoverRadius = radioRadius;
|
||||
radio.RevealOnEscape = true;
|
||||
if (!Check(!definition.Valid(), "escape cannot reveal the radio directly")) return false;
|
||||
radio.RevealOnEscape = false;
|
||||
ERR_SiteProp prop = definition.Find("everon-outpost-1").Props[0];
|
||||
vector offset = prop.Offset;
|
||||
prop.Offset = "100 0 0";
|
||||
if (!Check(!definition.Valid(), "unbounded scenery offset rejected")) return false;
|
||||
prop.Offset = offset;
|
||||
return Check(definition.Valid(), "valid configuration restored after isolated mutations");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
#ifdef WORKBENCH
|
||||
// Opt-in hidden native-widget comparison; no discovery, inventory or mission
|
||||
// state is changed, and no map menu or player input is fabricated.
|
||||
// Separate owned subclasses preserve the vanilla base RPC serializer identity.
|
||||
// Their small widget adapters are repeated to compare the unchanged native
|
||||
// setter against the real successor subclass without modding a replicated type.
|
||||
class ERR_NativePlainMapMarker : SCR_MapMarkerBase
|
||||
{
|
||||
bool ERR_ProbeAttachMapWidget()
|
||||
{
|
||||
ERR_ProbeDetachMapWidget();
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!manager || !manager.GetMarkerConfig()) return false;
|
||||
m_ConfigEntry = manager.GetMarkerConfig().GetMarkerEntryConfigByType(GetType());
|
||||
if (!m_ConfigEntry) return false;
|
||||
m_wRoot = GetGame().GetWorkspace().CreateWidgets(m_ConfigEntry.GetMarkerLayout());
|
||||
if (!m_wRoot) return false;
|
||||
m_wRoot.SetVisible(false);
|
||||
m_MarkerWidgetComp = SCR_MapMarkerWidgetComponent.Cast(m_wRoot.FindHandler(SCR_MapMarkerWidgetComponent));
|
||||
if (!m_MarkerWidgetComp) return false;
|
||||
m_MarkerWidgetComp.SetMarkerObject(this);
|
||||
m_ConfigEntry.InitClientSettings(this, m_MarkerWidgetComp, true);
|
||||
return m_wRoot.FindAnyWidget("MarkerText") != null;
|
||||
}
|
||||
string ERR_ProbeMapText()
|
||||
{
|
||||
if (!m_wRoot) return "";
|
||||
TextWidget text = TextWidget.Cast(m_wRoot.FindAnyWidget("MarkerText"));
|
||||
if (!text) return "";
|
||||
return text.GetText();
|
||||
}
|
||||
void ERR_ProbeDetachMapWidget()
|
||||
{
|
||||
// Remove only this probe object's native subscriptions/widgets.
|
||||
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
|
||||
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
|
||||
if (m_wRoot) m_wRoot.RemoveFromHierarchy();
|
||||
m_wRoot = null;
|
||||
m_MarkerWidgetComp = null;
|
||||
}
|
||||
}
|
||||
class ERR_NativeObjectiveMapMarker : ERR_DiscoveryMarker
|
||||
{
|
||||
bool ERR_ProbeAttachMapWidget()
|
||||
{
|
||||
ERR_ProbeDetachMapWidget();
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!manager || !manager.GetMarkerConfig()) return false;
|
||||
m_ConfigEntry = manager.GetMarkerConfig().GetMarkerEntryConfigByType(GetType());
|
||||
if (!m_ConfigEntry) return false;
|
||||
m_wRoot = GetGame().GetWorkspace().CreateWidgets(m_ConfigEntry.GetMarkerLayout());
|
||||
if (!m_wRoot) return false;
|
||||
m_wRoot.SetVisible(false);
|
||||
m_MarkerWidgetComp = SCR_MapMarkerWidgetComponent.Cast(m_wRoot.FindHandler(SCR_MapMarkerWidgetComponent));
|
||||
if (!m_MarkerWidgetComp) return false;
|
||||
m_MarkerWidgetComp.SetMarkerObject(this);
|
||||
m_ConfigEntry.InitClientSettings(this, m_MarkerWidgetComp, true);
|
||||
return m_wRoot.FindAnyWidget("MarkerText") != null;
|
||||
}
|
||||
string ERR_ProbeMapText()
|
||||
{
|
||||
if (!m_wRoot) return "";
|
||||
TextWidget text = TextWidget.Cast(m_wRoot.FindAnyWidget("MarkerText"));
|
||||
if (!text) return "";
|
||||
return text.GetText();
|
||||
}
|
||||
void ERR_ProbeDetachMapWidget()
|
||||
{
|
||||
// Remove only this probe object's native subscriptions/widgets.
|
||||
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
|
||||
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
|
||||
if (m_wRoot) m_wRoot.RemoveFromHierarchy();
|
||||
m_wRoot = null;
|
||||
m_MarkerWidgetComp = null;
|
||||
}
|
||||
}
|
||||
modded class SCR_MapMarkerManagerComponent
|
||||
{
|
||||
bool ERR_ProbeRetainsDiscoveryMarker(ERR_DiscoveryMarker marker)
|
||||
{
|
||||
return m_aStaticMarkers.Contains(marker) || m_aDisabledMarkers.Contains(marker);
|
||||
}
|
||||
}
|
||||
class ERR_NativeDiscoveryMapProbe
|
||||
{
|
||||
protected static ref ERR_NativePlainMapMarker s_Plain;
|
||||
protected static ref ERR_NativeObjectiveMapMarker s_Objective;
|
||||
static void Stop()
|
||||
{
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (s_Plain)
|
||||
{
|
||||
s_Plain.ERR_ProbeDetachMapWidget();
|
||||
if (manager) manager.RemoveStaticMarker(s_Plain);
|
||||
}
|
||||
if (s_Objective)
|
||||
{
|
||||
s_Objective.ERR_ProbeDetachMapWidget();
|
||||
if (manager) manager.ERR_RemoveDiscoveryMarker(s_Objective);
|
||||
}
|
||||
s_Plain = null;
|
||||
s_Objective = null;
|
||||
}
|
||||
protected static bool Check(bool success, string detail)
|
||||
{
|
||||
if (success) { Print("ERR SMOKE PASS: discovery map " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: discovery map " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
// Invoke once after ordinary fixture admission and native map manager init.
|
||||
static bool Run()
|
||||
{
|
||||
Stop();
|
||||
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
|
||||
if (!Check(manager && manager.GetMarkerConfig() && GetGame().GetPlayerController(), "native local controller and marker manager available")) return false;
|
||||
int originalCount = manager.GetStaticMarkers().Count();
|
||||
s_Plain = new ERR_NativePlainMapMarker();
|
||||
s_Plain.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
s_Plain.SetMarkerConfigID(0);
|
||||
s_Plain.SetCustomText("Unknown location");
|
||||
manager.InsertStaticMarker(s_Plain, true);
|
||||
if (!Check(s_Plain.ERR_ProbeAttachMapWidget() && s_Plain.ERR_ProbeMapText() == "Unknown location", "native baseline label rendered in actual hidden marker layout")) return false;
|
||||
s_Plain.SetCustomText("Radio station");
|
||||
if (!Check(s_Plain.GetCustomText() == "Radio station" && s_Plain.ERR_ProbeMapText() == "Unknown location", "native data-only setter reproduces stale already-open label")) return false;
|
||||
if (!Check(s_Plain.CanBeRemovedByOwner() && SCR_MapMarkersUI.IsOwnedMarker(s_Plain), "plain local marker exposes native delete/edit/drag eligibility")) return false;
|
||||
|
||||
s_Objective = new ERR_NativeObjectiveMapMarker();
|
||||
s_Objective.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
s_Objective.SetMarkerConfigID(0);
|
||||
s_Objective.SetCustomText("Unknown location");
|
||||
manager.InsertStaticMarker(s_Objective, true);
|
||||
if (!Check(s_Objective.GetMarkerOwnerID() == -1 && !SCR_MapMarkersUI.IsOwnedMarker(s_Objective) && !s_Objective.CanBeRemovedByOwner(), "local insertion retains objective identity and disables native edit/drag/delete eligibility")) return false;
|
||||
if (!Check(s_Objective.ERR_ProbeAttachMapWidget() && s_Objective.ERR_ProbeMapText() == "Unknown location", "objective first label rendered through actual native layout")) return false;
|
||||
int coordinates[2];
|
||||
s_Objective.SetDiscoveryPosition("6123.25 0 7345.75", 1);
|
||||
s_Objective.GetWorldPos(coordinates);
|
||||
if (!Check(coordinates[0] == 6250 && coordinates[1] == 7250, "first clue stores only stable 500m search-square center")) return false;
|
||||
s_Objective.SetDiscoveryPosition("6123.25 0 7345.75", 2);
|
||||
s_Objective.GetWorldPos(coordinates);
|
||||
if (!Check(coordinates[0] == 6123 && coordinates[1] == 7346, "identification moves same native marker to exact rounded coordinates")) return false;
|
||||
Widget originalWidget = s_Objective.GetRootWidget();
|
||||
s_Objective.SetCustomText("Radio station");
|
||||
if (!Check(s_Objective.GetRootWidget() == originalWidget && s_Objective.ERR_ProbeMapText() == "Radio station", "identification refreshes existing native widget without recreating marker")) return false;
|
||||
if (!Check(manager.GetStaticMarkers().Count() == originalCount + 2, "label change creates no additional marker")) return false;
|
||||
s_Objective.ERR_ProbeDetachMapWidget();
|
||||
s_Objective.SetCustomText("Initial helicopter pickup site");
|
||||
if (!Check(!s_Objective.GetRootWidget() && s_Objective.GetCustomText() == "Initial helicopter pickup site", "closed widget accepts data-only update safely")) return false;
|
||||
if (!Check(s_Objective.ERR_ProbeAttachMapWidget() && s_Objective.ERR_ProbeMapText() == "Initial helicopter pickup site", "new native widget renders latest saved label")) return false;
|
||||
manager.SetStaticMarkerDisabled(s_Objective, true);
|
||||
if (!Check(manager.ERR_ProbeRetainsDiscoveryMarker(s_Objective) && !manager.GetStaticMarkers().Contains(s_Objective), "native culling retains objective only in disabled list")) return false;
|
||||
manager.ERR_RemoveDiscoveryMarker(s_Objective);
|
||||
if (!Check(!manager.ERR_ProbeRetainsDiscoveryMarker(s_Objective) && !s_Objective.GetRootWidget(), "deleting culled objective removes both memberships and its widget")) return false;
|
||||
manager.ERR_RemoveDiscoveryMarker(s_Objective);
|
||||
if (!Check(!manager.ERR_ProbeRetainsDiscoveryMarker(s_Objective), "duplicate owned cleanup cannot reactivate objective")) return false;
|
||||
Stop();
|
||||
return Check(manager.GetStaticMarkers().Count() == originalCount, "owned probe widgets and markers removed; mission markers preserved");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeEnvironmentSample : ERR_EnvironmentService
|
||||
{
|
||||
bool Apply(TimeAndWeatherManagerEntity manager, int dateCode, int profile) { return StartOn(manager, dateCode, profile); }
|
||||
}
|
||||
// Explicit isolated environment smoke outcome. It temporarily changes the private
|
||||
// native world, restores date/weather family/looping, then requests a round reset.
|
||||
class ERR_NativeEnvironmentProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected TimeAndWeatherManagerEntity m_Manager;
|
||||
protected ref ERR_NativeEnvironmentSample m_Sample;
|
||||
protected ref ERR_Settings m_TestSettings;
|
||||
protected bool m_Snapshot;
|
||||
protected bool m_Restored;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_OwnsSettings;
|
||||
protected bool m_RestoreDateAccepted;
|
||||
protected int m_Year;
|
||||
protected int m_Month;
|
||||
protected int m_Day;
|
||||
protected string m_Weather;
|
||||
protected bool m_Looping;
|
||||
protected float m_Clock;
|
||||
protected float m_DayDuration;
|
||||
protected bool m_DayAdvances;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
protected int m_Step;
|
||||
protected int m_Result;
|
||||
void ERR_NativeEnvironmentProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: environment " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: environment " + detail, LogLevel.ERROR);
|
||||
m_Result = -1;
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected string CurrentWeather()
|
||||
{
|
||||
if (!m_Manager || !m_Manager.GetCurrentWeatherState()) return "";
|
||||
return m_Manager.GetCurrentWeatherState().GetStateName();
|
||||
}
|
||||
protected void Restore()
|
||||
{
|
||||
if (!m_Snapshot || m_Restored || !m_Manager) return;
|
||||
m_Restored = true;
|
||||
if (m_Sample) m_Sample.Stop();
|
||||
m_RestoreDateAccepted = m_Manager.SetDate(m_Year, m_Month, m_Day, true);
|
||||
m_Manager.ForceWeatherTo(m_Looping, m_Weather, 0, 24);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
Restore();
|
||||
m_Stopped = true;
|
||||
if (m_OwnsSettings)
|
||||
{
|
||||
string path = "$profile:erri-native-environment-probe.json";
|
||||
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
|
||||
m_OwnsSettings = false;
|
||||
}
|
||||
}
|
||||
protected bool SettingsChecks()
|
||||
{
|
||||
string path = "$profile:erri-native-environment-probe.json";
|
||||
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "environment fixture filenames are unused")) return false;
|
||||
m_OwnsSettings = true;
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
foreach (string key : ERR_Settings.Keys())
|
||||
if (key != "startDateCode" && key != "weatherProfile") json.WriteValue(key, m_Mode.Settings().Get(key));
|
||||
if (!Check(json.SaveToFile(path), "owned legacy settings fixture written")) return false;
|
||||
m_TestSettings = new ERR_Settings(path);
|
||||
JsonLoadContext unchanged = new JsonLoadContext();
|
||||
if (!Check(m_TestSettings.Get("startDateCode") == 0 && m_TestSettings.Get("weatherProfile") == 0 && unchanged.LoadFromFile(path) && !unchanged.DoesKeyExist("startDateCode") && !unchanged.DoesKeyExist("weatherProfile"), "missing environment settings preserve authored defaults without rewriting legacy file")) return false;
|
||||
if (!Check(m_TestSettings.Set("startDateCode", 1501231) && m_TestSettings.Set("weatherProfile", 2), "date and weather persist in owned fixture settings")) return false;
|
||||
ERR_Settings reloaded = new ERR_Settings(path);
|
||||
if (!Check(reloaded.Get("startDateCode") == 1501231 && reloaded.Get("weatherProfile") == 2 && reloaded.Get("radioDefenceSeconds") == m_Mode.Settings().Get("radioDefenceSeconds"), "odd maximum date code round-trips exactly and old settings survive")) return false;
|
||||
if (!Check(!m_TestSettings.Set("startDateCode", 229) && !m_TestSettings.Set("weatherProfile", 4), "non-leap-century date and unknown profile cannot be saved")) return false;
|
||||
int originalDate = m_Mode.Settings().Get("startDateCode");
|
||||
int originalProfile = m_Mode.Settings().Get("weatherProfile");
|
||||
array<string> replies = {};
|
||||
m_Mode.Admin().Execute(m_Mode, false, "mission weather set rain", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("Administrator") && m_Mode.Settings().Get("startDateCode") == originalDate && m_Mode.Settings().Get("weatherProfile") == originalProfile, "unauthorized environment command preserves real settings")) return false;
|
||||
replies.Clear();
|
||||
ERR_EnvironmentCommands.Execute(m_TestSettings, m_Mode.Environment(), {"mission", "date", "set", "2000-02-29"}, replies);
|
||||
if (!Check(m_TestSettings.Get("startDateCode") == 1000229 && replies.Count() == 1 && replies[0].Contains("next round"), "natural leap-date command saves only next-round settings")) return false;
|
||||
replies.Clear();
|
||||
ERR_EnvironmentCommands.Execute(m_TestSettings, m_Mode.Environment(), {"mission", "weather", "set", "clear"}, replies);
|
||||
if (!Check(m_TestSettings.Get("weatherProfile") == 1 && replies.Count() == 1 && replies[0].Contains("Current round unchanged"), "named weather command saves only next-round settings")) return false;
|
||||
replies.Clear();
|
||||
ERR_EnvironmentCommands.Execute(m_TestSettings, m_Mode.Environment(), {"mission", "date", "set", "1900-00-00"}, replies);
|
||||
if (!Check(m_TestSettings.Get("startDateCode") == 1000229 && replies[0].Contains("Invalid"), "zero month/day cannot masquerade as authored date")) return false;
|
||||
return true;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Result != 0) return m_Result;
|
||||
if (m_Stopped) return -1;
|
||||
if (!Replication.IsServer() || !m_Mode || !m_Mode.Active()) { Check(false, "requires active server authority"); return -1; }
|
||||
if (m_Deadline > 0 && now > m_Deadline) { Check(false, "native environment request timed out"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(m_Mode.GetWorld());
|
||||
if (world) m_Manager = world.GetTimeAndWeatherManager();
|
||||
if (!Check(m_Manager && m_Manager.GetCurrentWeatherState() && m_Mode.Environment().Started(), "native environment manager and round service available")) return -1;
|
||||
m_Manager.GetDate(m_Year, m_Month, m_Day);
|
||||
m_Weather = CurrentWeather(); m_Looping = m_Manager.IsWeatherLooping();
|
||||
m_Clock = m_Manager.GetTimeOfTheDay(); m_DayDuration = m_Manager.GetDayDuration(); m_DayAdvances = m_Manager.GetIsDayAutoAdvanced();
|
||||
m_Snapshot = true;
|
||||
m_Deadline = now + 15000;
|
||||
if (!SettingsChecks()) return -1;
|
||||
if (!Check(CurrentWeather() == m_Weather && m_Manager.GetYear() == m_Year && m_Manager.GetMonth() == m_Month && m_Manager.GetDay() == m_Day, "saving fixture environment settings does not mutate native world")) return -1;
|
||||
for (int profile = 1; profile <= 3; profile++)
|
||||
if (!Check(ERR_EnvironmentService.HasWeatherState(m_Manager, ERR_EnvironmentService.WeatherStateName(profile)), "selected vanilla weather family exists: " + ERR_EnvironmentService.ProfileName(profile))) return -1;
|
||||
m_Sample = new ERR_NativeEnvironmentSample();
|
||||
if (!Check(!m_Sample.Apply(m_Manager, 229, 1) && !m_Sample.Apply(m_Manager, 1000229, 4) && !m_Sample.Started(), "invalid date/profile rejected before native changes")) return -1;
|
||||
if (!Check(m_Sample.Apply(m_Manager, 890601, 1), "clear profile and specific date requested once")) return -1;
|
||||
m_Step = 1; m_Wait = now + 500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!Check(CurrentWeather() == "Clear" && m_Manager.IsWeatherLooping() && m_Manager.GetYear() == 1989 && m_Manager.GetMonth() == 6 && m_Manager.GetDay() == 1, "native clear weather and requested date applied after frame")) return -1;
|
||||
if (!Check(m_Sample.Apply(m_Manager, 1000229, 3) && m_Sample.DateCode() == 890601 && m_Sample.Profile() == 1 && CurrentWeather() == "Clear", "repeated start cannot apply another date or profile")) return -1;
|
||||
m_Sample.Stop();
|
||||
if (!Check(!m_Sample.Apply(m_Manager, 1000229, 3), "stopped environment cannot restart")) return -1;
|
||||
m_Sample = new ERR_NativeEnvironmentSample();
|
||||
if (!Check(m_Sample.Apply(m_Manager, 1000229, 2), "overcast and leap date requested on fresh service")) return -1;
|
||||
m_Step = 2; m_Wait = now + 500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!Check(CurrentWeather() == "Overcast" && m_Manager.GetYear() == 2000 && m_Manager.GetMonth() == 2 && m_Manager.GetDay() == 29, "native overcast and leap date accepted")) return -1;
|
||||
m_Sample.Stop(); m_Sample = new ERR_NativeEnvironmentSample();
|
||||
if (!Check(m_Sample.Apply(m_Manager, 0, 3), "rain profile preserves authored/current date when date override is disabled")) return -1;
|
||||
m_Step = 3; m_Wait = now + 500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!Check(CurrentWeather() == "Rainy" && m_Manager.GetYear() == 2000 && m_Manager.GetMonth() == 2 && m_Manager.GetDay() == 29, "native rainy family applied without changing date")) return -1;
|
||||
float clockAdvance = m_Manager.GetTimeOfTheDay() - m_Clock;
|
||||
if (clockAdvance < 0) clockAdvance += 24;
|
||||
if (!Check(m_Manager.GetDayDuration() == m_DayDuration && m_Manager.GetIsDayAutoAdvanced() == m_DayAdvances && clockAdvance < 0.1, "environment does not reset clock or progression")) return -1;
|
||||
Restore();
|
||||
m_Step = 4; m_Wait = now + 500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (!Check(m_RestoreDateAccepted && m_Manager.GetYear() == m_Year && m_Manager.GetMonth() == m_Month && m_Manager.GetDay() == m_Day && CurrentWeather() == m_Weather && m_Manager.IsWeatherLooping() == m_Looping, "fixture restores original native date, weather family and looping")) return -1;
|
||||
m_Result = 1; Stop();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
#ifdef WORKBENCH
|
||||
// Only environment-restart opts into this fixed private path. The runner must
|
||||
// refuse all preexisting fixture files and require exactly two real rounds.
|
||||
class ERR_NativeEnvironmentRestartProbe
|
||||
{
|
||||
static const string PATH = "$profile:erri-environment-restart.json";
|
||||
static const string MARKER = "$profile:erri-environment-restart-round.json";
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected TimeAndWeatherManagerEntity m_Manager;
|
||||
protected bool m_Failed;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
protected int m_Round = 1;
|
||||
protected int m_InitialDate;
|
||||
protected string m_InitialWeather;
|
||||
protected bool m_InitialLoop;
|
||||
protected float m_Wait;
|
||||
protected float m_Hour;
|
||||
protected float m_Duration;
|
||||
|
||||
void ERR_NativeEnvironmentRestartProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode;
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
if (world) m_Manager = world.GetTimeAndWeatherManager();
|
||||
if (!Check(m_Manager && m_Manager.GetCurrentWeatherState(), "world environment exists before production settings and service initialization")) return;
|
||||
m_InitialDate = DateCode();
|
||||
m_InitialWeather = Weather();
|
||||
m_InitialLoop = m_Manager.IsWeatherLooping();
|
||||
if (FileIO.FileExists(MARKER))
|
||||
{
|
||||
JsonLoadContext marker = new JsonLoadContext();
|
||||
int stage;
|
||||
if (!Check(marker.LoadFromFile(MARKER) && marker.ReadValue("completedRound", stage) && stage == 1 && FileIO.FileExists(PATH), "second instance sees first round's completed marker and saved settings")) return;
|
||||
m_Round = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Check(!FileIO.FileExists(PATH) && !FileIO.FileExists(PATH + ".bak") && !FileIO.FileExists(PATH + ".tmp"), "first instance owns previously unused fixture settings paths")) return;
|
||||
}
|
||||
PrintFormat("ERR ENVIRONMENT RESTART INSTANCE: round=%1 authoredDate=%2 authoredWeather=%3", m_Round, m_InitialDate, m_InitialWeather);
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: environment restart " + detail); return true; }
|
||||
m_Failed = true;
|
||||
Print("ERR SMOKE FAIL: environment restart " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected int DateCode() { return (m_Manager.GetYear() - 1900) * 10000 + m_Manager.GetMonth() * 100 + m_Manager.GetDay(); }
|
||||
protected string Weather()
|
||||
{
|
||||
WeatherState state = m_Manager.GetCurrentWeatherState();
|
||||
if (!state) return "unavailable";
|
||||
return state.GetStateName();
|
||||
}
|
||||
int Round() { return m_Round; }
|
||||
void Stop() { m_Stopped = true; }
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Failed || m_Stopped) return -1;
|
||||
if (m_Complete) return 1;
|
||||
if (m_Wait == 0)
|
||||
{
|
||||
if (!Check(m_Mode.Environment().Started() && m_Mode.Time().Started(), "production round services started")) return -1;
|
||||
if (!Check(m_Mode.Settings().Get("startTimeMinutes") == 840 && m_Mode.Settings().Get("timeMultiplier") == 1 && m_Mode.Time().StartMinutes() == 840 && m_Mode.Time().Multiplier() == 1, "both rounds retain original clock defaults and active progression settings")) return -1;
|
||||
m_Hour = m_Manager.GetTimeOfTheDay();
|
||||
m_Duration = m_Manager.GetDayDuration();
|
||||
if (m_Round == 1)
|
||||
{
|
||||
if (!Check(m_Mode.Settings().Get("startDateCode") == 0 && m_Mode.Settings().Get("weatherProfile") == 0 && m_Mode.Environment().DateCode() == 0 && m_Mode.Environment().Profile() == 0, "first instance uses authored environment defaults")) return -1;
|
||||
if (!Check(DateCode() == m_InitialDate && Weather() == m_InitialWeather && m_Manager.IsWeatherLooping() == m_InitialLoop, "default production startup preserves authored date and weather")) return -1;
|
||||
array<string> replies = {};
|
||||
if (!Check(ERR_EnvironmentCommands.Execute(m_Mode.Settings(), m_Mode.Environment(), {"mission", "date", "set", "2000-02-29"}, replies), "production date command handled")) return -1;
|
||||
if (!Check(ERR_EnvironmentCommands.Execute(m_Mode.Settings(), m_Mode.Environment(), {"mission", "weather", "set", "overcast"}, replies), "production weather command handled")) return -1;
|
||||
if (!Check(replies.Count() == 2 && replies[0].Contains("Saved for next round") && replies[1].Contains("Saved for next round"), "both commands report durable next-round saves")) return -1;
|
||||
// This is the actual mode's settings owner, not a detached sample service.
|
||||
ERR_Settings disk = new ERR_Settings(PATH);
|
||||
if (!Check(disk.Get("startDateCode") == 1000229 && disk.Get("weatherProfile") == 2 && disk.Get("radioDefenceSeconds") == 180 && disk.Get("timeMultiplier") == 1, "production saves persist environment and preserve unrelated defaults")) return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Check(m_Mode.Settings().Get("startDateCode") == 1000229 && m_Mode.Settings().Get("weatherProfile") == 2 && m_Mode.Environment().DateCode() == 1000229 && m_Mode.Environment().Profile() == 2, "second real mode loads saved date and weather into production services")) return -1;
|
||||
}
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (now < m_Wait) return 0;
|
||||
if (!Check(m_Mode.Settings().Get("startDateCode") == 1000229 && m_Mode.Settings().Get("weatherProfile") == 2, "saved next-round settings remain intact")) return -1;
|
||||
if (m_Round == 1)
|
||||
{
|
||||
if (!Check(m_Mode.Environment().DateCode() == 0 && m_Mode.Environment().Profile() == 0 && DateCode() == m_InitialDate && Weather() == m_InitialWeather && m_Manager.IsWeatherLooping() == m_InitialLoop, "saving real mode settings leaves current environment unchanged across native ticks")) return -1;
|
||||
}
|
||||
else if (!Check(DateCode() == 1000229 && Weather() == "Overcast" && m_Manager.IsWeatherLooping(), "second round applies exact leap date and selected native weather family")) return -1;
|
||||
float advance = m_Manager.GetTimeOfTheDay() - m_Hour;
|
||||
if (advance < 0) advance += 24;
|
||||
if (!Check(advance > 0 && advance < 0.02 && m_Manager.GetIsDayAutoAdvanced() && m_Duration == ERR_TimeService.DayDuration(1) && m_Manager.GetDayDuration() == m_Duration, "native clock continues advancing without an environment-induced reset or rate change")) return -1;
|
||||
if (m_Round == 1)
|
||||
{
|
||||
JsonSaveContext marker = new JsonSaveContext();
|
||||
marker.EnableTypeDiscriminator(false);
|
||||
marker.WriteValue("completedRound", 1);
|
||||
if (!Check(marker.SaveToFile(MARKER), "first completed round leaves restart handoff marker")) return -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FileIO.FileExists(PATH)) FileIO.DeleteFile(PATH);
|
||||
if (FileIO.FileExists(PATH + ".bak")) FileIO.DeleteFile(PATH + ".bak");
|
||||
if (FileIO.FileExists(PATH + ".tmp")) FileIO.DeleteFile(PATH + ".tmp");
|
||||
if (FileIO.FileExists(MARKER)) FileIO.DeleteFile(MARKER);
|
||||
if (!Check(!FileIO.FileExists(PATH) && !FileIO.FileExists(PATH + ".bak") && !FileIO.FileExists(PATH + ".tmp") && !FileIO.FileExists(MARKER), "successful second round removes only fixture settings and marker files")) return -1;
|
||||
}
|
||||
m_Complete = true;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
#ifdef WORKBENCH
|
||||
// Real vehicle/character compartments with an isolated synthetic identity roster.
|
||||
// This is not twelve network clients, manual boarding, medical treatment or flight.
|
||||
class ERR_NativeExtractionProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
|
||||
protected ref ERR_ExtractionService m_All;
|
||||
protected ref ERR_ExtractionService m_Partial;
|
||||
protected ref array<IEntity> m_Actors = {};
|
||||
protected ref array<UUID> m_Accounts = {};
|
||||
protected ref array<BaseCompartmentSlot> m_Seats = {};
|
||||
protected vector m_Original[4];
|
||||
protected int m_Step;
|
||||
protected int m_Boarding;
|
||||
protected int m_Resolutions;
|
||||
protected float m_Wait;
|
||||
protected UUID m_NewAccount = "00e77200-0000-0000-0000-000000000099";
|
||||
|
||||
void ERR_NativeExtractionProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: extraction " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: extraction " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected void Resolved(ERR_ExtractionState state) { m_Resolutions++; }
|
||||
protected IEntity Character(int index)
|
||||
{
|
||||
IEntity actor = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, ERR_Actors.Ground(m_Original[3] + Vector(20 + index * 2, 0, 15)));
|
||||
if (!actor) return null;
|
||||
m_Mode.Own(actor);
|
||||
m_Actors.Insert(actor);
|
||||
DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
return actor;
|
||||
}
|
||||
protected void MoveHelicopter(bool outside)
|
||||
{
|
||||
vector transform[4];
|
||||
m_Vehicle.GetWorldTransform(transform);
|
||||
transform[3] = m_Original[3];
|
||||
if (outside) transform[3] = m_Original[3] + "650 150 0";
|
||||
m_Vehicle.SetWorldTransform(transform);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!ERR_NativeTimeProbe.Run(m_Mode)) return -1;
|
||||
m_Vehicle = m_Mode.ExtractionVehicle();
|
||||
if (!Check(m_Vehicle && m_Mode.Extraction().Ready(), "authored transport is ready for twelve prisoners")) return -1;
|
||||
int count = m_Mode.Extraction().Seats(m_Seats);
|
||||
PrintFormat("ERR EXTRACTION CAPACITY: usable pilot/passenger positions=%1", count);
|
||||
if (!Check(count >= 12 && !m_Mode.SetExtractionVehicle(m_Vehicle), "running mission cannot rebind its extraction vehicle")) return -1;
|
||||
m_Vehicle.GetWorldTransform(m_Original);
|
||||
DamageManagerComponent.Cast(m_Vehicle.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
IEntity actor = Character(i);
|
||||
UUID account = "00e77200-0000-0000-0000-" + (i + 1).ToString(12, true);
|
||||
if (!Check(actor && m_Roster.Bind(10001 + i, account), "isolated participant created")) return -1;
|
||||
m_Accounts.Insert(account);
|
||||
m_Roster.Spawned(10001 + i, actor);
|
||||
}
|
||||
m_All = new ERR_ExtractionService(m_Roster);
|
||||
if (!Check(m_All.Assign(m_Vehicle) && !m_All.Begin(m_Original[3], 0) && m_All.Begin(m_Original[3], 500), "invalid boundary rejected without preventing a valid service start")) return -1;
|
||||
if (!Check(!m_All.Assign(m_Vehicle) && !m_All.Begin(m_Original[3], 500), "duplicate binding and start rejected")) return -1;
|
||||
m_All.OnResolved().Insert(Resolved);
|
||||
m_Step = 1;
|
||||
m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_Actors[m_Boarding].FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(access && access.GetInVehicle(m_Vehicle, m_Seats[m_Boarding], true, -1, ECloseDoorAfterActions.INVALID, false), "native boarding request accepted")) return -1;
|
||||
m_Boarding++;
|
||||
m_Wait = now + 500;
|
||||
if (m_Boarding == 12) { m_Step = 2; m_Wait = now + 3000; }
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
for (int i = 0; i < 12; i++)
|
||||
if (!Check(CompartmentAccessComponent.GetVehicleIn(m_Actors[i]) == m_Vehicle && m_Seats[i].GetOccupant() == m_Actors[i], "participant occupies a distinct physical seat")) return -1;
|
||||
m_All.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(m_All.State() == ERR_ExtractionState.WAITING && m_All.EscapedCount() == 0, "boarding inside boundary does not win")) return -1;
|
||||
MoveHelicopter(true);
|
||||
m_Step = 3;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
m_All.Tick(ERR_Phase.RADIO_SEARCH);
|
||||
if (!Check(m_All.State() == ERR_ExtractionState.WAITING, "leaving before extraction phase does not win")) return -1;
|
||||
m_All.Tick(ERR_Phase.EXTRACTION);
|
||||
array<int> winners = {};
|
||||
m_All.ConnectedWinners(winners);
|
||||
if (!Check(m_All.EscapedCount() == 12 && m_All.LeftBehindCount() == 0 && winners.Count() == 12 && m_Resolutions == 1, "all twelve physical occupants escape exactly once")) return -1;
|
||||
m_All.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(m_Resolutions == 1 && !m_All.Assign(m_Vehicle), "resolved extraction neither repeats nor replaces its vehicle")) return -1;
|
||||
MoveHelicopter(false);
|
||||
m_Step = 4;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Actors[0].FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
damage.EnableDamageHandling(true);
|
||||
damage.Kill(Instigator.CreateInstigator(m_Actors[0]));
|
||||
vector exitTransform[4];
|
||||
m_Actors[1].GetWorldTransform(exitTransform);
|
||||
exitTransform[3] = ERR_Actors.Ground(m_Original[3] + "30 0 0");
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_Actors[1].FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(access.GetOutVehicle_NoDoor(exitTransform, false, false), "one survivor leaves the helicopter")) return -1;
|
||||
CharacterControllerComponent.Cast(m_Actors[2].FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||
m_Roster.Disconnect(10004);
|
||||
IEntity newcomer = Character(12);
|
||||
if (!Check(newcomer && m_Roster.Bind(10004, m_NewAccount), "departed connection ID reused by a distinct account")) return -1;
|
||||
m_Roster.Spawned(10004, newcomer);
|
||||
m_Partial = new ERR_ExtractionService(m_Roster);
|
||||
if (!Check(m_Partial.Assign(m_Vehicle) && m_Partial.Begin(m_Original[3], 500), "isolated partial escape case starts")) return -1;
|
||||
m_Step = 5;
|
||||
m_Wait = now + 3000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(!ERR_Actors.Alive(m_Actors[0]) && !CompartmentAccessComponent.GetVehicleIn(m_Actors[1]), "dead occupant and left-behind survivor are physically distinct cases")) return -1;
|
||||
if (!Check(ERR_Actors.Alive(m_Actors[2]) && !ERR_Actors.CanFight(m_Actors[2]) && CompartmentAccessComponent.GetVehicleIn(m_Actors[2]) == m_Vehicle, "unconscious passenger remains physically aboard")) return -1;
|
||||
MoveHelicopter(true);
|
||||
m_Step = 6;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
m_Partial.Tick(ERR_Phase.EXTRACTION);
|
||||
array<int> winners = {};
|
||||
m_Partial.ConnectedWinners(winners);
|
||||
if (!Check(m_Partial.EscapedCount() == 10 && m_Partial.LeftBehindCount() == 3 && winners.Count() == 9, "partial escape counts only living aboard participants")) return -1;
|
||||
if (!Check(m_Partial.DidEscape(m_Accounts[2]) && m_Partial.DidEscape(m_Accounts[3]) && !m_Partial.DidEscape(m_Accounts[0]) && !m_Partial.DidEscape(m_Accounts[1]) && !m_Partial.DidEscape(m_NewAccount), "unconscious and reserved occupants escape; dead and absent people do not")) return -1;
|
||||
if (!Check(winners.Contains(10003) && !winners.Contains(10004), "recycled connection ID does not inherit another account's win")) return -1;
|
||||
if (!Check(m_Roster.Bind(10040, m_Accounts[3]), "escaped reserved account can rebind to a new connection")) return -1;
|
||||
m_Partial.ConnectedWinners(winners);
|
||||
if (!Check(winners.Count() == 10 && winners.Contains(10040) && !winners.Contains(10004), "result follows escaped identity after rebinding")) return -1;
|
||||
m_Partial.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(m_Partial.EscapedCount() == 10 && m_Partial.LeftBehindCount() == 3, "terminal result snapshot remains stable")) return -1;
|
||||
ERR_PlayerRoster emptyRoster = new ERR_PlayerRoster();
|
||||
ERR_ExtractionService empty = new ERR_ExtractionService(emptyRoster);
|
||||
if (!Check(empty.Assign(m_Vehicle) && empty.Begin(m_Original[3], 500), "empty-roster control starts")) return -1;
|
||||
empty.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(empty.State() == ERR_ExtractionState.WAITING, "unregistered occupants cannot win the mission")) return -1;
|
||||
ERR_ExtractionService stopped = new ERR_ExtractionService(m_Roster);
|
||||
if (!Check(stopped.Assign(m_Vehicle) && stopped.Begin(m_Original[3], 500), "stopped-service control starts")) return -1;
|
||||
stopped.Stop();
|
||||
stopped.Tick(ERR_Phase.EXTRACTION);
|
||||
if (!Check(stopped.State() == ERR_ExtractionState.WAITING, "stopped extraction cannot resolve")) return -1;
|
||||
if (!ERR_NativeResultProbe.Run(m_Partial, m_Accounts[3], m_Accounts[0]) || !ERR_NativeTimeProbe.VerifyProgression(m_Mode)) return -1;
|
||||
MoveHelicopter(false);
|
||||
for (int i = m_Actors.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Actors[i]);
|
||||
m_Step = 7;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
#ifdef WORKBENCH
|
||||
// Read-only native assertions. Run after site preparation and faction init,
|
||||
// before destructive vehicle fixtures. Does not load legacy full faction configs.
|
||||
class ERR_NativeFactionCatalogProbe
|
||||
{
|
||||
protected static ResourceName UAZ() { return "{259EE7B78C51B624}Prefabs/Vehicles/Wheeled/UAZ469/UAZ469.et"; }
|
||||
protected static ResourceName Mi8() { return "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et"; }
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: faction-catalog " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: faction-catalog " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected static bool Catalogs(SCR_Faction faction)
|
||||
{
|
||||
array<SCR_EntityCatalog> catalogs = {};
|
||||
if (!Check(faction.GetAllFactionEntityCatalogs(catalogs) == 6, faction.GetFactionKey() + " retains all six catalog types")) return false;
|
||||
SCR_EntityCatalog vehicles = faction.GetFactionEntityCatalogOfType(EEntityCatalogType.VEHICLE);
|
||||
array<SCR_EntityCatalogEntry> entries = {};
|
||||
if (!Check(vehicles && vehicles.GetEntityList(entries) == 2, faction.GetFactionKey() + " has exactly two supported vehicle entries")) return false;
|
||||
bool uaz, mi8;
|
||||
foreach (SCR_EntityCatalogEntry entry : entries)
|
||||
{
|
||||
if (!Check(entry != null, "vehicle entry exists")) return false;
|
||||
ResourceName prefab = entry.GetPrefab();
|
||||
PrintFormat("ERR FACTION VEHICLE: faction=%1 prefab=%2", faction.GetFactionKey(), prefab);
|
||||
if (prefab == UAZ()) uaz = true;
|
||||
else if (prefab == Mi8()) mi8 = true;
|
||||
else return Check(false, "unexpected vehicle catalog entry " + prefab);
|
||||
}
|
||||
if (!Check(uaz && mi8 && vehicles.GetEntryWithPrefab(UAZ()) && vehicles.GetEntryWithPrefab(Mi8()), "catalog lookup resolves both unique mission vehicles")) return false;
|
||||
array<EEntityCatalogType> retained = {EEntityCatalogType.CHARACTER, EEntityCatalogType.GROUP, EEntityCatalogType.WEAPONS_TRIPOD, EEntityCatalogType.ITEM, EEntityCatalogType.SUPPLY_CONTAINER_ITEM};
|
||||
foreach (EEntityCatalogType kind : retained)
|
||||
{
|
||||
SCR_EntityCatalog catalog = faction.GetFactionEntityCatalogOfType(kind);
|
||||
if (!Check(catalog != null, faction.GetFactionKey() + " non-vehicle catalog exists " + typename.EnumToString(EEntityCatalogType, kind))) return false;
|
||||
catalog.GetEntityList(entries);
|
||||
PrintFormat("ERR FACTION RETAINED: faction=%1 type=%2 entries=%3", faction.GetFactionKey(), typename.EnumToString(EEntityCatalogType, kind), entries.Count());
|
||||
// Installed US/USSR supply-container catalogs are intentionally empty.
|
||||
if (kind != EEntityCatalogType.SUPPLY_CONTAINER_ITEM && !Check(!entries.IsEmpty(), "retained native content catalog is initialized and nonempty")) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
protected static bool Items(SCR_Faction faction, ResourceName dressing, ResourceName saline, ResourceName radio)
|
||||
{
|
||||
SCR_EntityCatalog items = faction.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM);
|
||||
return Check(items && items.GetEntryWithPrefab(dressing) && items.GetEntryWithPrefab(saline) && items.GetEntryWithPrefab(radio), faction.GetFactionKey() + " native dressing saline and backpack radio remain catalogued");
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
if (!Check(Replication.IsServer() && mode && mode.Active(), "active authority available")) return false;
|
||||
FactionManager manager = GetGame().GetFactionManager();
|
||||
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.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;
|
||||
if (!Catalogs(us) || !Catalogs(ussr)) return false;
|
||||
if (!Items(us, "{A81F501D3EF6F38E}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et", "{00E36F41CA310E2A}Prefabs/Items/Medicine/SalineBag_01/SalineBag_US_01.et", "{9B6B61BB3FE3DFB0}Prefabs/Items/Equipment/Radios/Radio_ANPRC77.et")) return false;
|
||||
if (!Items(ussr, "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et", "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", "{54C68E438DD34265}Prefabs/Items/Equipment/Radios/Radio_R107M.et")) return false;
|
||||
SCR_EntityCatalogManagerComponent catalogManager = SCR_EntityCatalogManagerComponent.GetInstance();
|
||||
if (!Check(catalogManager && catalogManager.GetFactionEntityCatalogOfType(EEntityCatalogType.VEHICLE, "US") && catalogManager.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM, "USSR"), "global catalog manager retains faction vehicle and inventory access")) return false;
|
||||
ERR_SiteRuntime depot = mode.Sites().Find("everon-supply-2");
|
||||
Vehicle helicopter = mode.ExtractionVehicle();
|
||||
if (!Check(depot && depot.Transport && ERR_Actors.Alive(depot.Transport) && depot.Transport.GetPrefabData().GetPrefabName() == UAZ(), "physical finite depot vehicle is supported UAZ")) return false;
|
||||
if (!Check(helicopter && ERR_Actors.Alive(helicopter) && helicopter.GetPrefabData().GetPrefabName() == Mi8() && helicopter.FindComponent(HelicopterControllerComponent), "physical extraction aircraft is supported Mi8")) return false;
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
if (!Check(mode.Extraction().Seats(seats) >= 12, "physical aircraft still has required capacity")) return false;
|
||||
Print("ERR FACTION RESULT: successor faction catalog scope and retained native support; no driving or multiplayer claim");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
#ifdef WORKBENCH
|
||||
// Test-only horizontal envelope, independent of aircraft yaw/pitch/roll.
|
||||
// All distances are around the native physics origin, not the rotor centre.
|
||||
class ERR_FlightFootprintRules
|
||||
{
|
||||
static float CylinderSphere(float originDistance, float radius, float thickness)
|
||||
{
|
||||
// Triangle inequality deliberately overbounds the one-sided rotor cylinder.
|
||||
return originDistance + radius + thickness;
|
||||
}
|
||||
static bool Fits(float radius, float initialDrift, float initialHeight)
|
||||
{
|
||||
if (!(radius > 0 && radius < 32 && initialDrift >= 0 && initialDrift < 32)) return false;
|
||||
if (!(initialHeight >= -3 && initialHeight <= 5)) return false;
|
||||
return radius + initialDrift + 8 + 1 <= 40 && radius + initialHeight + 18 + 1 <= 60;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeFlightFootprint
|
||||
{
|
||||
static const int MAX_ENTITIES = 256;
|
||||
static const int MAX_DEPTH = 16;
|
||||
static const int MAX_HITZONES = 512;
|
||||
protected static bool Refuse(string reason)
|
||||
{
|
||||
Print("ERR POWERED FOOTPRINT: refused " + reason);
|
||||
return false;
|
||||
}
|
||||
protected static bool PointRadius(vector point, vector origin, inout float radius)
|
||||
{
|
||||
float distance = vector.Distance(point, origin);
|
||||
if (!(distance >= 0 && distance < 1000)) return false;
|
||||
radius = Math.Max(radius, distance);
|
||||
return true;
|
||||
}
|
||||
static bool Measure(Vehicle aircraft, VehicleHelicopterSimulation simulation, vector pad, out vector origin, out float radius)
|
||||
{
|
||||
radius = 0;
|
||||
if (!aircraft || !simulation || !simulation.IsValid() || !aircraft.GetPrefabData()) return Refuse("missing initialized native airframe");
|
||||
if (aircraft.GetPrefabData().GetPrefabName() != "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et") return Refuse("different airframe resource");
|
||||
// Model bounds and damage pivots are world-space. The simulation getter
|
||||
// exposes internal physics space (identity at the airport in 100422), so
|
||||
// use this same physical aircraft's actual entity world transform.
|
||||
vector worldTransform[4]; aircraft.GetWorldTransform(worldTransform);
|
||||
origin = worldTransform[3];
|
||||
for (int axis = 0; axis < 3; axis++)
|
||||
if (!(Math.AbsFloat(worldTransform[axis].Length() - 1) < 0.001)) return Refuse("non-unit native entity world basis");
|
||||
|
||||
array<IEntity> entities = {aircraft};
|
||||
array<int> depths = {0};
|
||||
array<HitZone> seenZones = {};
|
||||
int meshes, mainSweeps, tailSweeps;
|
||||
float modelRadius, damageRadius, simulationRadius;
|
||||
for (int index = 0; index < entities.Count(); index++)
|
||||
{
|
||||
IEntity entity = entities[index];
|
||||
if (!entity) return Refuse("hierarchy changed during measurement");
|
||||
if (entity.GetVObject())
|
||||
{
|
||||
vector mins, maxs; entity.GetWorldBounds(mins, maxs);
|
||||
for (int dimension = 0; dimension < 3; dimension++)
|
||||
if (!(mins[dimension] <= maxs[dimension])) return Refuse("invalid model bounds");
|
||||
// A sphere around every corner encloses the complete world AABB. Its radius
|
||||
// remains conservative under any rigid orientation of the whole aircraft.
|
||||
for (int x = 0; x < 2; x++)
|
||||
for (int y = 0; y < 2; y++)
|
||||
for (int z = 0; z < 2; z++)
|
||||
{
|
||||
vector corner = mins;
|
||||
if (x == 1) corner[0] = maxs[0];
|
||||
if (y == 1) corner[1] = maxs[1];
|
||||
if (z == 1) corner[2] = maxs[2];
|
||||
if (!PointRadius(corner, origin, modelRadius)) return Refuse("non-finite or excessive model bounds");
|
||||
}
|
||||
meshes++;
|
||||
}
|
||||
HitZoneContainerComponent container = HitZoneContainerComponent.Cast(entity.FindComponent(HitZoneContainerComponent));
|
||||
if (container)
|
||||
{
|
||||
int count = container.CountAllHitZones();
|
||||
if (count < 0 || count + seenZones.Count() > MAX_HITZONES) return Refuse("hitzone traversal bound");
|
||||
array<HitZone> zones = {};
|
||||
if (container.GetAllHitZones(zones) != count || zones.Count() != count) return Refuse("hitzones changed during measurement");
|
||||
foreach (HitZone zone : zones)
|
||||
{
|
||||
if (!zone || seenZones.Contains(zone)) continue;
|
||||
seenZones.Insert(zone);
|
||||
SCR_RotorHitZone rotorZone = SCR_RotorHitZone.Cast(zone);
|
||||
if (!rotorZone || rotorZone.GetRotorRadius() <= 0) continue; // transmission zones have no sweep
|
||||
int rotorIndex = rotorZone.GetRotorIndex();
|
||||
float expectedRadius, thickness;
|
||||
if (rotorIndex == 0) { expectedRadius = 12; thickness = 1.5; mainSweeps++; }
|
||||
else if (rotorIndex == 1) { expectedRadius = 2.3; thickness = 0.75; tailSweeps++; }
|
||||
else return Refuse("unknown rotor sweep index");
|
||||
// Stock thickness is protected with no getter. Scope to the exact prefab
|
||||
// and verify its public sweep radius against the audited stock constants.
|
||||
if (!(Math.AbsFloat(rotorZone.GetRotorRadius() - expectedRadius) < 0.001)) return Refuse("stock damage sweep radius changed");
|
||||
PointInfo point = rotorZone.GetPointInfo();
|
||||
if (!point) return Refuse("missing actual rotor sweep pivot");
|
||||
vector rotorWorld[4]; point.GetWorldTransform(rotorWorld);
|
||||
// Frobenius norm bounds the transform's maximum stretch even with scale
|
||||
// or shear; do not assume the PointInfo basis is orthonormal.
|
||||
float scaleSquared = 0;
|
||||
for (int basis = 0; basis < 3; basis++) scaleSquared += vector.Dot(rotorWorld[basis], rotorWorld[basis]);
|
||||
float scale = Math.Sqrt(scaleSquared);
|
||||
if (!(scale > 0 && scale < 4)) return Refuse("invalid damage sweep basis");
|
||||
float distance = vector.Distance(origin, rotorWorld[3]);
|
||||
float sweepBound = ERR_FlightFootprintRules.CylinderSphere(distance, expectedRadius * scale, thickness * scale);
|
||||
if (!(sweepBound > 0 && sweepBound < 1000)) return Refuse("invalid damage sweep extent");
|
||||
damageRadius = Math.Max(damageRadius, sweepBound);
|
||||
PrintFormat("ERR POWERED FOOTPRINT ROTOR: index=%1 pivot=%2 damageRadius=%3 thickness=%4 scale=%5 sphereBound=%6", rotorIndex, rotorWorld[3], expectedRadius, thickness, scale, sweepBound);
|
||||
}
|
||||
}
|
||||
IEntity child = entity.GetChildren();
|
||||
while (child)
|
||||
{
|
||||
if (entities.Count() >= MAX_ENTITIES || depths[index] >= MAX_DEPTH || entities.Contains(child)) return Refuse("entity/depth/cycle traversal bound");
|
||||
entities.Insert(child); depths.Insert(depths[index] + 1);
|
||||
child = child.GetSibling();
|
||||
}
|
||||
}
|
||||
if (meshes == 0 || mainSweeps != 1 || tailSweeps != 1) return Refuse("complete model and both actual damage sweeps required");
|
||||
if (simulation.RotorCount() != 2) return Refuse("stock simulation rotor count changed");
|
||||
for (int rotor = 0; rotor < 2; rotor++)
|
||||
{
|
||||
vector localRotor[4]; simulation.RotorGetTransform(rotor, localRotor);
|
||||
float physicalRadius = simulation.RotorGetRadius(rotor);
|
||||
float physicalBound = localRotor[3].Length() + physicalRadius;
|
||||
if (!(physicalRadius > 0 && physicalBound > 0 && physicalBound < 1000)) return Refuse("invalid native simulation rotor extent");
|
||||
simulationRadius = Math.Max(simulationRadius, physicalBound);
|
||||
PrintFormat("ERR POWERED FOOTPRINT PHYSICS ROTOR: index=%1 localPivot=%2 radius=%3 sphereBound=%4", rotor, localRotor[3], physicalRadius, physicalBound);
|
||||
}
|
||||
radius = Math.Max(modelRadius, Math.Max(damageRadius, simulationRadius));
|
||||
float initialDrift = vector.DistanceXZ(origin, pad);
|
||||
float initialHeight = origin[1] - pad[1];
|
||||
PrintFormat("ERR POWERED FOOTPRINT: entities=%1 meshes=%2 zones=%3 modelSphere=%4 damageSphere=%5 simulationSphere=%6 completeSphere=%7 initialDrift=%8 initialHeight=%9", entities.Count(), meshes, seenZones.Count(), modelRadius, damageRadius, simulationRadius, radius, initialDrift, initialHeight);
|
||||
if (!ERR_FlightFootprintRules.Fits(radius, initialDrift, initialHeight)) return Refuse("measured footprint plus initial placement,8m drift and1m margin exceeds40m corridor or60m ceiling");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
#ifdef WORKBENCH
|
||||
// Optional observations only. Never writes pilot controls, signals or settings.
|
||||
modded class ERR_NativePoweredFlightProbe
|
||||
{
|
||||
protected bool m_ERRInputDiagChecked;
|
||||
protected bool m_ERRInputDiagEnabled;
|
||||
protected int m_ERRInputDiagSamples;
|
||||
protected int m_ERRWrittenSequence;
|
||||
protected int m_ERRAppliedSequence;
|
||||
protected int m_ERRApplyCallbacks;
|
||||
protected float m_ERRWrittenAt;
|
||||
protected float m_ERRAppliedAt;
|
||||
protected float m_ERRWrittenCollective;
|
||||
protected float m_ERRAppliedCollective;
|
||||
protected float m_ERRWrittenThrottle;
|
||||
protected float m_ERRAppliedThrottle;
|
||||
// Vector elements are cyclic-forward, cyclic-aside, anti-torque, respectively.
|
||||
protected vector m_ERRWrittenCyclic;
|
||||
protected vector m_ERRAppliedCyclic;
|
||||
|
||||
protected bool ERR_InputDiagActive()
|
||||
{
|
||||
if (!m_ERRInputDiagChecked)
|
||||
{
|
||||
string enabled;
|
||||
m_ERRInputDiagEnabled = System.GetCLIParam("errFlightInputDiag", enabled) && enabled == "1";
|
||||
m_ERRInputDiagChecked = true;
|
||||
}
|
||||
if (!m_ERRInputDiagEnabled || !m_Bound || m_Stopped || !m_Input) return false;
|
||||
return PoweredAuthority();
|
||||
}
|
||||
|
||||
override void PreparePilotActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
|
||||
{
|
||||
int before = m_ControlUpdates;
|
||||
super.PreparePilotActions(controller, owner, am, dt);
|
||||
if (m_ControlUpdates == before || controller != m_Character || owner != m_Pilot || !ERR_InputDiagActive()) return;
|
||||
m_ERRWrittenSequence = m_ControlUpdates;
|
||||
m_ERRWrittenAt = GetGame().GetWorld().GetWorldTime();
|
||||
// These are actual owned virtual-action values, not native consumption.
|
||||
m_ERRWrittenCollective = am.GetActionValue("HelicopterCollectiveIncrease") - am.GetActionValue("HelicopterCollectiveDecrease");
|
||||
float forward = am.GetActionValue("HelicopterCyclicForward") - am.GetActionValue("HelicopterCyclicBack");
|
||||
float aside = am.GetActionValue("HelicopterCyclicRight") - am.GetActionValue("HelicopterCyclicLeft");
|
||||
float torque = am.GetActionValue("HelicopterAntiTorqueRight") - am.GetActionValue("HelicopterAntiTorqueLeft");
|
||||
m_ERRWrittenCyclic = Vector(forward, aside, torque);
|
||||
m_ERRWrittenThrottle = m_Input.GetVehicleThrottle(); // observation only in action mode
|
||||
}
|
||||
|
||||
override void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
|
||||
{
|
||||
// Snapshot native context before this callback's owned final writes.
|
||||
ERR_ObserveAppliedFlightInput(controller, owner);
|
||||
int before = m_ControlUpdates;
|
||||
super.ApplyPilot(controller, owner, dt);
|
||||
if (m_ControlUpdates == before || controller != m_Character || owner != m_Pilot || !ERR_InputDiagActive()) return;
|
||||
m_ERRWrittenSequence = m_ControlUpdates;
|
||||
m_ERRWrittenAt = GetGame().GetWorld().GetWorldTime();
|
||||
m_ERRWrittenCollective = m_Input.GetVehicleCollective();
|
||||
m_ERRWrittenThrottle = m_Input.GetVehicleThrottle();
|
||||
m_ERRWrittenCyclic = Vector(m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque());
|
||||
}
|
||||
|
||||
void ERR_ObserveAppliedFlightInput(CharacterControllerComponent controller, IEntity owner)
|
||||
{
|
||||
if (controller != m_Character || owner != m_Pilot || !ERR_InputDiagActive()) return;
|
||||
m_ERRApplyCallbacks++;
|
||||
m_ERRAppliedSequence = m_ERRWrittenSequence;
|
||||
m_ERRAppliedAt = GetGame().GetWorld().GetWorldTime();
|
||||
m_ERRAppliedCollective = m_Input.GetVehicleCollective();
|
||||
m_ERRAppliedThrottle = m_Input.GetVehicleThrottle();
|
||||
m_ERRAppliedCyclic = Vector(m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque());
|
||||
}
|
||||
|
||||
protected string ERR_ExistingRotorSignal(SignalsManagerComponent signals, string name)
|
||||
{
|
||||
if (!signals) return "no-manager";
|
||||
int index = signals.FindSignal(name);
|
||||
if (index < 0) return "missing";
|
||||
return string.Format("%1", signals.GetSignalValue(index));
|
||||
}
|
||||
|
||||
protected override void Diagnostic(float now)
|
||||
{
|
||||
bool due = m_Simulation && m_Aircraft && now >= m_NextLog;
|
||||
super.Diagnostic(now);
|
||||
// The existing diagnostic gate is two seconds. Cap at120 batches as well,
|
||||
// and never call native getters for an unowned or player-claimed fixture.
|
||||
if (!due || m_ERRInputDiagSamples >= 120 || !ERR_InputDiagActive()) return;
|
||||
m_ERRInputDiagSamples++;
|
||||
float writtenAge = -1;
|
||||
float appliedAge = -1;
|
||||
if (m_ERRWrittenSequence > 0) writtenAge = now - m_ERRWrittenAt;
|
||||
if (m_ERRApplyCallbacks > 0) appliedAge = now - m_ERRAppliedAt;
|
||||
PrintFormat("ERR POWERED INPUT OBSERVATION: sample=%1 writtenSequence=%2 applyCallbacks=%3 beforeWriteSequence=%4 writtenAgeMs=%5 beforeWriteAgeMs=%6", m_ERRInputDiagSamples, m_ERRWrittenSequence, m_ERRApplyCallbacks, m_ERRAppliedSequence, writtenAge, appliedAge);
|
||||
PrintFormat("ERR POWERED INPUT STAGES: writtenCollective=%1 beforeWriteCollective=%2 writtenFRA=%3 beforeWriteFRA=%4 writtenThrottle=%5 beforeWriteThrottle=%6", m_ERRWrittenCollective, m_ERRAppliedCollective, m_ERRWrittenCyclic, m_ERRAppliedCyclic, m_ERRWrittenThrottle, m_ERRAppliedThrottle);
|
||||
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
||||
string collective = ERR_ExistingRotorSignal(signals, "MainRotorCollective");
|
||||
string forward = ERR_ExistingRotorSignal(signals, "MainRotorCyclicForward");
|
||||
string aside = ERR_ExistingRotorSignal(signals, "MainRotorCyclicAside");
|
||||
string tail = ERR_ExistingRotorSignal(signals, "TailRotorCollective");
|
||||
bool autohover = m_Controller.GetAutohoverEnabled();
|
||||
int mode = m_Controller.GetCollectiveMode();
|
||||
PrintFormat("ERR POWERED ROTOR SIGNALS: collective=%1 cyclicForward=%2 cyclicAside=%3 tailCollective=%4 autohover=%5 collectiveMode=%6", collective, forward, aside, tail, autohover, mode);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
#ifdef WORKBENCH
|
||||
// Native queue length is not exposed: SCR_AIGroup.GetSpawnQueueSize() is a
|
||||
// compatibility stub returning zero. Observe dispatch on explicitly tagged
|
||||
// disposable groups instead; every engine call still reaches unchanged super.
|
||||
class ERR_NativeGuardDispatchTrace
|
||||
{
|
||||
static int Total;
|
||||
}
|
||||
modded class SCR_AIGroup
|
||||
{
|
||||
protected bool m_ERRGuardCancelTracked;
|
||||
protected int m_ERRGuardCancelDispatches;
|
||||
protected int m_ERRGuardCancelInitCalls;
|
||||
protected int m_ERRGuardCancelRequestCalls;
|
||||
void ERR_TrackGuardCancel()
|
||||
{
|
||||
m_ERRGuardCancelTracked = true;
|
||||
PrintFormat("ERR GUARD CANCEL TRACK: group=%1 initCalls=%2 requests=%3 agents=%4 spawnImmediately=%5 slots=%6 lifecycle=%7", this, m_ERRGuardCancelInitCalls, m_ERRGuardCancelRequestCalls, GetAgentsCount(), GetSpawnImmediately(), m_aUnitPrefabSlots.Count(), GetLifecyclePolicy());
|
||||
}
|
||||
override void EOnInit(IEntity owner)
|
||||
{
|
||||
m_ERRGuardCancelInitCalls++;
|
||||
if (m_ERRGuardCancelTracked) PrintFormat("ERR GUARD CANCEL LATE INIT: group=%1 initCalls=%2", this, m_ERRGuardCancelInitCalls);
|
||||
super.EOnInit(owner);
|
||||
}
|
||||
override void RequestSpawn(int slotsWanted = -1, float observerRange = 0)
|
||||
{
|
||||
m_ERRGuardCancelRequestCalls++;
|
||||
if (m_ERRGuardCancelTracked) PrintFormat("ERR GUARD CANCEL REQUEST: group=%1 request=%2 wanted=%3 range=%4 slots=%5", this, m_ERRGuardCancelRequestCalls, slotsWanted, observerRange, m_aUnitPrefabSlots.Count());
|
||||
super.RequestSpawn(slotsWanted, observerRange);
|
||||
}
|
||||
int ERR_GuardCancelDispatches() { return m_ERRGuardCancelDispatches; }
|
||||
// EOnInit assigns the cached utility before requesting members. The ignore-
|
||||
// spawning branch instead invokes all-members init immediately; exclude it.
|
||||
bool ERR_GuardCancelPendingInit() { return GetGroupUtilityComponent() && !m_bOnInitInvoked; }
|
||||
override bool ExpandOneMember()
|
||||
{
|
||||
if (m_ERRGuardCancelTracked)
|
||||
{
|
||||
m_ERRGuardCancelDispatches++;
|
||||
ERR_NativeGuardDispatchTrace.Total++;
|
||||
PrintFormat("ERR GUARD CANCEL DISPATCH: group=%1 dispatch=%2 agents=%3 slots=%4 complete=%5", this, m_ERRGuardCancelDispatches, GetAgentsCount(), m_aUnitPrefabSlots.Count(), IsExpandComplete());
|
||||
}
|
||||
return super.ExpandOneMember();
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble only the owned-group portion of PENDING preparation. No authored
|
||||
// mission sites, stock, waypoints or live mission service are cloned or changed.
|
||||
class ERR_NativeGuardCancelOwner : ERR_SiteService
|
||||
{
|
||||
protected int m_Cancellations;
|
||||
void ERR_NativeGuardCancelOwner(ERR_GameMode mode) { m_Mode = mode; }
|
||||
int Cancellations() { return m_Cancellations; }
|
||||
protected override void CancelInitialGuardRequests()
|
||||
{
|
||||
m_Cancellations++;
|
||||
PrintFormat("ERR GUARD CANCEL OWNER: pendingState=%1 calls=%2 ownedSites=%3", m_State, m_Cancellations, m_Sites.Count());
|
||||
super.CancelInitialGuardRequests();
|
||||
foreach (ERR_SiteRuntime site : m_Sites)
|
||||
if (site.Group) PrintFormat("ERR GUARD CANCEL RETIRED: site=%1 group=%2 slots=%3 spawnImmediately=%4 agents=%5 complete=%6", site.Definition.Id, site.Group, site.Group.m_aUnitPrefabSlots.Count(), site.Group.GetSpawnImmediately(), site.Group.GetAgentsCount(), site.Group.IsExpandComplete());
|
||||
}
|
||||
bool Attach(SCR_AIGroup group, string label)
|
||||
{
|
||||
if (!group || m_Begun) return false;
|
||||
m_Begun = true;
|
||||
ERR_SiteRuntime site = new ERR_SiteRuntime();
|
||||
site.Definition = new ERR_SiteDefinition();
|
||||
site.Definition.Id = label;
|
||||
site.Definition.Guarded = true;
|
||||
site.Group = group;
|
||||
if (group.m_aUnitPrefabSlots) site.InitialGuardCount = group.m_aUnitPrefabSlots.Count();
|
||||
m_Sites.Insert(site);
|
||||
m_Entities.Insert(group);
|
||||
return site.InitialGuardCount == 6 && group.GetSpawnImmediately();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeGuardCancelProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ref ERR_NativeGuardCancelOwner m_EmptyOwner;
|
||||
protected ref ERR_NativeGuardCancelOwner m_PartialOwner;
|
||||
protected ref ERR_NativeGuardCancelOwner m_ControlOwner;
|
||||
protected SCR_AIGroup m_Empty;
|
||||
protected SCR_AIGroup m_Partial;
|
||||
protected SCR_AIGroup m_Control;
|
||||
protected ref array<IEntity> m_PartialMembers = {};
|
||||
protected ref array<IEntity> m_DeletedMembers = {};
|
||||
protected ref array<SCR_AIGroup> m_MainGroups = {};
|
||||
protected int m_PartialDispatches;
|
||||
protected int m_TotalBeforeDelete;
|
||||
protected int m_Step;
|
||||
protected float m_Deadline;
|
||||
protected float m_ObserveUntil;
|
||||
protected bool m_RolledBack;
|
||||
void ERR_NativeGuardCancelProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: guard-cancel " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: guard-cancel " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected int Members(SCR_AIGroup group, out array<IEntity> members)
|
||||
{
|
||||
members.Clear();
|
||||
if (!group) return 0;
|
||||
array<AIAgent> agents = {};
|
||||
group.GetAgents(agents);
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (actor && !members.Contains(actor)) members.Insert(actor);
|
||||
}
|
||||
return members.Count();
|
||||
}
|
||||
protected SCR_AIGroup NewGroup(vector offset)
|
||||
{
|
||||
ERR_SiteRuntime outpost = m_Mode.Sites().Find("everon-outpost-1");
|
||||
if (!outpost || !outpost.Group) return null;
|
||||
SCR_AIGroup group = SCR_AIGroup.Cast(ERR_Actors.Spawn(m_Mode.Factions().GuardGroupPrefab, ERR_Actors.Ground(outpost.Group.GetOrigin() + offset)));
|
||||
if (group) group.ERR_TrackGuardCancel();
|
||||
return group;
|
||||
}
|
||||
protected bool OriginalSitesIntact()
|
||||
{
|
||||
if (!m_Mode.Sites() || m_Mode.Sites().Prepare() != ERR_PrepareResult.READY) return false;
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
array<SCR_AIGroup> groups = {};
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
|
||||
if (site && site.Group) groups.Insert(site.Group);
|
||||
}
|
||||
if (groups.Count() != m_MainGroups.Count()) return false;
|
||||
foreach (SCR_AIGroup group : m_MainGroups)
|
||||
if (!group || !groups.Contains(group)) return false;
|
||||
return true;
|
||||
}
|
||||
protected bool CancelledStable()
|
||||
{
|
||||
array<IEntity> emptyMembers = {};
|
||||
array<IEntity> partialMembers = {};
|
||||
if (!m_Empty || Members(m_Empty, emptyMembers) != 0 || m_Empty.ERR_GuardCancelDispatches() != 0) return false;
|
||||
if (!m_Partial || Members(m_Partial, partialMembers) != m_PartialMembers.Count() || m_Partial.ERR_GuardCancelDispatches() != m_PartialDispatches) return false;
|
||||
foreach (IEntity actor : m_PartialMembers)
|
||||
if (!actor || !partialMembers.Contains(actor)) return false;
|
||||
return true;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
// Each owner rolls back at most once. ERR_Actors.DeleteSpawned retains its
|
||||
// stock active-player protection; no player is assigned to these groups.
|
||||
if (m_RolledBack) return;
|
||||
m_RolledBack = true;
|
||||
if (m_EmptyOwner) m_EmptyOwner.Rollback();
|
||||
if (m_PartialOwner) m_PartialOwner.Rollback();
|
||||
if (m_ControlOwner) m_ControlOwner.Rollback();
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Deadline == 0) m_Deadline = now + 90000;
|
||||
if (now > m_Deadline || !m_Mode.Active()) { Check(false, "bounded fixture deadline or active mission lost"); return -1; }
|
||||
if (m_Step == 0)
|
||||
{
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
|
||||
if (site && site.Group) m_MainGroups.Insert(site.Group);
|
||||
}
|
||||
ERR_NativeGuardDispatchTrace.Total = 0;
|
||||
m_EmptyOwner = new ERR_NativeGuardCancelOwner(m_Mode);
|
||||
m_PartialOwner = new ERR_NativeGuardCancelOwner(m_Mode);
|
||||
m_ControlOwner = new ERR_NativeGuardCancelOwner(m_Mode);
|
||||
m_Empty = NewGroup("50 0 60");
|
||||
if (!Check(m_EmptyOwner.Attach(m_Empty, "guard-cancel-empty"), "empty case owns one six-slot automatic native group")) return -1;
|
||||
array<IEntity> initial = {};
|
||||
if (!Check(m_Empty.ERR_GuardCancelPendingInit() && Members(m_Empty, initial) == 0 && m_Empty.ERR_GuardCancelDispatches() == 0 && !m_Empty.IsExpandComplete(), "empty case is cancelled before its first native member dispatch")) return -1;
|
||||
m_EmptyOwner.Stop();
|
||||
m_EmptyOwner.Stop();
|
||||
if (!Check(m_EmptyOwner.Cancellations() == 1 && !m_Empty.GetSpawnImmediately() && m_Empty.m_aUnitPrefabSlots.IsEmpty(), "empty cancellation retires only this instance's spawn intent once")) return -1;
|
||||
if (!Check(m_Empty && m_EmptyOwner.Owns(m_Empty) && m_EmptyOwner.Prepare() == ERR_PrepareResult.FAILED && !m_Mode.Sites().Owns(m_Empty), "repeated actual preparation stop retains only the disposable empty group for observation")) return -1;
|
||||
m_Partial = NewGroup("-50 0 60");
|
||||
if (!Check(m_PartialOwner.Attach(m_Partial, "guard-cancel-partial"), "partial case owns a separate six-slot automatic native group")) return -1;
|
||||
m_Control = NewGroup("0 0 100");
|
||||
if (!Check(m_ControlOwner.Attach(m_Control, "guard-cancel-control"), "uncancelled six-slot control uses the same native queue")) return -1;
|
||||
m_Step = 1;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
array<IEntity> emptyMembers = {};
|
||||
if (!m_Empty || Members(m_Empty, emptyMembers) != 0 || m_Empty.ERR_GuardCancelDispatches() != 0)
|
||||
{ Check(false, "empty group received late dispatch or members after cancellation"); return -1; }
|
||||
if (!m_Partial || !m_Control) { Check(false, "disposable observation group disappeared before cancellation"); return -1; }
|
||||
if (Members(m_Partial, m_PartialMembers) == 0) return 0;
|
||||
if (!Check(m_PartialMembers.Count() < 6 && !m_Partial.IsExpandComplete() && m_Partial.ERR_GuardCancelDispatches() > 0, "native queue produced a real partial squad before cancellation")) return -1;
|
||||
// The fixture tick runs after native dispatch returns, so the queue has
|
||||
// finished re-enqueuing this partial fill. Do not purge from inside AddAgent.
|
||||
m_PartialOwner.Stop();
|
||||
m_PartialOwner.Stop();
|
||||
if (!Check(m_PartialOwner.Cancellations() == 1 && !m_Partial.GetSpawnImmediately() && m_Partial.m_aUnitPrefabSlots.IsEmpty(), "partial cancellation retires future spawn intent without deleting present actors")) return -1;
|
||||
m_PartialDispatches = m_Partial.ERR_GuardCancelDispatches();
|
||||
if (!Check(m_PartialOwner.Prepare() == ERR_PrepareResult.FAILED && m_PartialOwner.Owns(m_Partial), "partial preparation stops idempotently while existing members remain")) return -1;
|
||||
PrintFormat("ERR GUARD CANCEL: partialMembers=%1 dispatches=%2 observationSeconds=15", m_PartialMembers.Count(), m_PartialDispatches);
|
||||
m_ObserveUntil = now + 15000;
|
||||
m_Step = 2;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!CancelledStable()) { Check(false, "cancelled groups changed member identity/count or received another native dispatch"); return -1; }
|
||||
array<IEntity> controlMembers = {};
|
||||
int controlCount = Members(m_Control, controlMembers);
|
||||
if (!m_Control || controlCount > 6) { Check(false, "uncancelled control vanished or exceeded its requested size"); return -1; }
|
||||
if (now < m_ObserveUntil || controlCount != 6 || !m_Control.IsExpandComplete()) return 0;
|
||||
if (!Check(m_Control.ERR_GuardCancelDispatches() >= 6 && m_Control.m_aUnitPrefabSlots.Count() == 6 && m_Control.GetSpawnImmediately(), "uncancelled native control reached six while cancelled groups stayed unchanged for at least fifteen seconds")) return -1;
|
||||
if (!Check(OriginalSitesIntact(), "original mission site groups and ready service remain intact")) return -1;
|
||||
foreach (IEntity partialMember : m_PartialMembers) m_DeletedMembers.Insert(partialMember);
|
||||
foreach (IEntity controlMember : controlMembers) m_DeletedMembers.Insert(controlMember);
|
||||
foreach (IEntity actor : m_DeletedMembers)
|
||||
if (!Check(!ERR_Actors.Player(actor), "rollback target is a disposable non-player character")) return -1;
|
||||
m_TotalBeforeDelete = ERR_NativeGuardDispatchTrace.Total;
|
||||
Stop();
|
||||
m_ObserveUntil = now + 6000;
|
||||
m_Step = 3;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (ERR_NativeGuardDispatchTrace.Total != m_TotalBeforeDelete) { Check(false, "native dispatch occurred after rollback"); return -1; }
|
||||
if (now < m_ObserveUntil) return 0;
|
||||
if (!Check(!m_Empty && !m_Partial && !m_Control, "each disposable group was deleted once by its real owner rollback")) return -1;
|
||||
foreach (IEntity deleted : m_DeletedMembers)
|
||||
if (!Check(!deleted, "each physical partial/control member is actually deleted after rollback")) return -1;
|
||||
if (!Check(OriginalSitesIntact(), "original mission groups remain intact after rollback and six-second observation")) return -1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
#ifdef WORKBENCH
|
||||
// Opt-in fault injection on a fixture-owned stock kit in a private preview world.
|
||||
// Calls the actual teardown boundary while the native callback sources still
|
||||
// exist, so an unsubscribe failure remains observable before entity destruction.
|
||||
modded class SCR_ResupplyMedicalGadgetSupportStationComponent
|
||||
{
|
||||
protected bool m_ERRTeardownProbeWatch;
|
||||
protected int m_ERRTeardownProbeResourceCalls;
|
||||
protected int m_ERRTeardownProbeDamageCalls;
|
||||
protected int m_ERRTeardownProbeInitCalls;
|
||||
|
||||
protected override void DelayedInit(IEntity owner)
|
||||
{
|
||||
if (m_ERRTeardownProbeWatch) m_ERRTeardownProbeInitCalls++;
|
||||
super.DelayedInit(owner);
|
||||
}
|
||||
protected override void TEMP_OnInteractorReplicated()
|
||||
{
|
||||
if (m_ERRTeardownProbeWatch) m_ERRTeardownProbeResourceCalls++;
|
||||
super.TEMP_OnInteractorReplicated();
|
||||
}
|
||||
protected override void OnDamageStateChanged(EDamageState damageState)
|
||||
{
|
||||
if (m_ERRTeardownProbeWatch) m_ERRTeardownProbeDamageCalls++;
|
||||
super.OnDamageStateChanged(damageState);
|
||||
}
|
||||
bool ERR_TeardownProbeInitialize()
|
||||
{
|
||||
// Preview worlds need not process normal world events. Execute the unchanged
|
||||
// native initialization routine once if EOnInit has not already done so.
|
||||
if (!m_ERRTeardownInitialized)
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(DelayedInit);
|
||||
DelayedInit(GetOwner());
|
||||
}
|
||||
return m_ERRTeardownInitialized && m_ERRTeardownScope && m_EntityCatalogManager && !UsesRange();
|
||||
}
|
||||
SCR_ResourceComponent ERR_TeardownProbeResource() { return m_ERRTeardownResource; }
|
||||
void ERR_TeardownProbeWatch() { m_ERRTeardownProbeWatch = true; }
|
||||
int ERR_TeardownProbeResourceCalls() { return m_ERRTeardownProbeResourceCalls; }
|
||||
int ERR_TeardownProbeDamageCalls() { return m_ERRTeardownProbeDamageCalls; }
|
||||
int ERR_TeardownProbeInitCalls() { return m_ERRTeardownProbeInitCalls; }
|
||||
bool ERR_TeardownProbeDone() { return m_ERRTeardownDone; }
|
||||
void ERR_TeardownProbeExpireAndDelete()
|
||||
{
|
||||
// Fault injection only: do not mutate/delete the global manager or any
|
||||
// existing preview/cache entity. Nothing calls InitValidSetup with this null.
|
||||
m_EntityCatalogManager = null;
|
||||
GetGame().GetCallqueue().CallLater(DelayedInit, 100, false, GetOwner());
|
||||
OnDelete(GetOwner());
|
||||
OnDelete(GetOwner());
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeItemStationTeardownProbe
|
||||
{
|
||||
protected ref SharedItemRef m_WorldRef;
|
||||
protected BaseWorld m_World;
|
||||
protected IEntity m_Kit;
|
||||
protected SCR_ResupplyMedicalGadgetSupportStationComponent m_Station;
|
||||
protected SCR_ResourceComponent m_Resource;
|
||||
protected SCR_DamageManagerComponent m_Damage;
|
||||
protected SCR_HitZone m_HitZone;
|
||||
protected SCR_EntityCatalogManagerComponent m_OriginalCatalog;
|
||||
protected int m_Index;
|
||||
protected int m_Step;
|
||||
protected int m_Result;
|
||||
protected bool m_Stopped;
|
||||
protected int m_ExpectedDamageCalls;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: station teardown " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: station teardown " + detail, LogLevel.ERROR);
|
||||
m_Result = -1;
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
m_Stopped = true;
|
||||
ReleaseKit();
|
||||
}
|
||||
protected void ReleaseKit()
|
||||
{
|
||||
// All objects belong to this fixture, never a resource or preview cache.
|
||||
m_Station = null;
|
||||
m_Resource = null;
|
||||
m_Damage = null;
|
||||
m_HitZone = null;
|
||||
if (m_Kit) delete m_Kit;
|
||||
m_Kit = null;
|
||||
m_World = null;
|
||||
m_WorldRef = null;
|
||||
}
|
||||
protected void EmitNativeCallbacks()
|
||||
{
|
||||
m_Resource.TEMP_GetOnInteractorReplicated().Invoke();
|
||||
if (m_Damage) m_Damage.GetOnDamageStateChanged().Invoke(m_Damage.GetState());
|
||||
if (m_HitZone) m_HitZone.GetOnDamageStateChanged().Invoke(m_HitZone);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Result != 0) return m_Result;
|
||||
if (m_Stopped) return -1;
|
||||
if (m_Deadline == 0) m_Deadline = now + 10000;
|
||||
if (now >= m_Deadline) { Check(false, "bounded fixture deadline"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Check(Replication.IsServer() && ERR_GameMode.Get() && SCR_EntityCatalogManagerComponent.GetInstance(), "successor server and real global catalog available")) return -1;
|
||||
m_OriginalCatalog = SCR_EntityCatalogManagerComponent.GetInstance();
|
||||
m_WorldRef = BaseWorld.CreateWorld("Preview", "ERRStationTeardownProbe");
|
||||
if (m_WorldRef) m_World = m_WorldRef.GetRef();
|
||||
if (!Check(m_World && m_World != GetGame().GetWorld(), "owns an independent off-world preview world")) return -1;
|
||||
ResourceName prefab = "{AE578EEA4244D41F}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_US.et";
|
||||
if (m_Index == 1) prefab = "{21EF98BFC1EB3793}Prefabs/Items/Equipment/Kits/MedicalKit_01/MedicalKit_01_USSR.et";
|
||||
Resource resource = Resource.Load(prefab);
|
||||
if (!Check(resource && resource.IsValid(), "stock kit resource resolves")) return -1;
|
||||
m_Kit = GetGame().SpawnEntityPrefab(resource, m_World);
|
||||
if (!Check(m_Kit != null, "isolated stock kit spawned")) return -1;
|
||||
m_Wait = now + 500;
|
||||
m_Step = 1;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
m_Station = SCR_ResupplyMedicalGadgetSupportStationComponent.Cast(m_Kit.FindComponent(SCR_ResupplyMedicalGadgetSupportStationComponent));
|
||||
if (!Check(m_Station && m_Station.ERR_TeardownProbeInitialize(), "actual native initialization completed with valid catalog and no range registration")) return -1;
|
||||
m_Resource = m_Station.ERR_TeardownProbeResource();
|
||||
if (!Check(m_Resource != null, "actual subscribed native resource component captured")) return -1;
|
||||
m_Damage = SCR_DamageManagerComponent.Cast(m_Kit.FindComponent(SCR_DamageManagerComponent));
|
||||
HitZoneContainerComponent hitZones = HitZoneContainerComponent.Cast(m_Kit.FindComponent(HitZoneContainerComponent));
|
||||
if (hitZones) m_HitZone = SCR_HitZone.Cast(hitZones.GetDefaultHitZone());
|
||||
m_ExpectedDamageCalls = 0;
|
||||
if (m_Damage) m_ExpectedDamageCalls++;
|
||||
if (m_HitZone) m_ExpectedDamageCalls++;
|
||||
PrintFormat("ERR STATION TEARDOWN PROBE: kit=%1 nativeDamageSources=%2 resourcePresent=%3", m_Index, m_ExpectedDamageCalls, m_Resource != null);
|
||||
m_Station.ERR_TeardownProbeWatch();
|
||||
EmitNativeCallbacks();
|
||||
if (!Check(m_Station.ERR_TeardownProbeResourceCalls() == 1 && m_Station.ERR_TeardownProbeDamageCalls() == m_ExpectedDamageCalls, "native invokers reach initialized station exactly once per installed source")) return -1;
|
||||
m_Station.ERR_TeardownProbeExpireAndDelete();
|
||||
if (!Check(m_Station.ERR_TeardownProbeDone(), "expired cached catalog takes production cleanup exactly once")) return -1;
|
||||
EmitNativeCallbacks();
|
||||
if (!Check(m_Station.ERR_TeardownProbeResourceCalls() == 1 && m_Station.ERR_TeardownProbeDamageCalls() == m_ExpectedDamageCalls, "resource and installed damage sources no longer invoke detached station")) return -1;
|
||||
m_Wait = now + 500;
|
||||
m_Step = 2;
|
||||
return 0;
|
||||
}
|
||||
if (!Check(m_Station.ERR_TeardownProbeInitCalls() == 0, "queued native initialization did not run after teardown")) return -1;
|
||||
EmitNativeCallbacks();
|
||||
if (!Check(m_Station.ERR_TeardownProbeResourceCalls() == 1 && m_Station.ERR_TeardownProbeDamageCalls() == m_ExpectedDamageCalls, "no late callbacks after 500 ms")) return -1;
|
||||
if (!Check(m_OriginalCatalog && SCR_EntityCatalogManagerComponent.GetInstance() == m_OriginalCatalog, "same global catalog remains alive and unchanged by fault injection")) return -1;
|
||||
ReleaseKit();
|
||||
m_Index++;
|
||||
if (m_Index < 2) { m_Step = 0; return 0; }
|
||||
m_Result = 1;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
#ifdef WORKBENCH
|
||||
[BaseContainerProps(category: "Respawn")]
|
||||
modded class ERR_SpawnLogic
|
||||
{
|
||||
// Test-only ownership setup. This does not change a life record, start a real
|
||||
// spawn request, replace a controller's actor or bypass production finalization.
|
||||
bool ERR_TestPendingActor(SCR_SpawnRequestComponent request, SCR_SpawnData data, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !request || !data || !actor || ERR_Actors.Player(actor)) return false;
|
||||
int id = request.GetPlayerId();
|
||||
if (m_Pending.Contains(id) || !m_Identities.Contains(id)) return false;
|
||||
ERR_SpawnTicket ticket = new ERR_SpawnTicket();
|
||||
ticket.ConnectionId = id; ticket.AccountId = m_Identities.Get(id);
|
||||
ticket.Mode = ERR_GameMode.Get();
|
||||
ticket.Controller = SCR_PlayerController.Cast(request.GetPlayerController());
|
||||
ticket.Request = request; ticket.Data = data; ticket.Actor = actor;
|
||||
m_SpawnTickets.Insert(ticket);
|
||||
m_Pending.Insert(id);
|
||||
m_AuthorizedPositions.Set(id, data.GetPosition());
|
||||
return true;
|
||||
}
|
||||
void ERR_TestClearAdmission(int playerId) { CancelAdmission(playerId, ERR_ObserverReason.NONE); }
|
||||
bool ERR_TestDelayedLossPreservesAdmission(int playerId, SCR_PlayerController controller)
|
||||
{
|
||||
ObserveAfterLoss(playerId, m_Identities.Get(playerId), controller);
|
||||
return m_Pending.Contains(playerId) && m_AuthorizedPositions.Contains(playerId);
|
||||
}
|
||||
bool ERR_TestHasAdmission(int playerId)
|
||||
{
|
||||
if (m_Pending.Contains(playerId) || m_AuthorizedPositions.Contains(playerId)) return true;
|
||||
foreach (ERR_SpawnTicket ticket : m_SpawnTickets)
|
||||
if (ticket.ConnectionId == playerId) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeLifecycleProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected IEntity m_Blocker;
|
||||
protected IEntity m_Preload;
|
||||
protected IEntity m_Cancelled;
|
||||
protected IEntity m_Drift;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected SCR_FreeSpawnRequestComponent m_Request;
|
||||
protected SCR_FreeSpawnHandlerComponent m_Handler;
|
||||
protected ERR_SpawnLogic m_Logic;
|
||||
protected ref SCR_FreeSpawnData m_Data;
|
||||
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
|
||||
protected vector m_Position;
|
||||
protected int m_Step;
|
||||
protected float m_Wait;
|
||||
void ERR_NativeLifecycleProbe(ERR_GameMode mode, IEntity player) { m_Mode = mode; m_Player = player; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: lifecycle " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: lifecycle " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected IEntity Actor(vector position)
|
||||
{
|
||||
IEntity actor = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, position);
|
||||
if (actor)
|
||||
{
|
||||
m_Mode.Own(actor);
|
||||
DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
}
|
||||
return actor;
|
||||
}
|
||||
protected bool Rules()
|
||||
{
|
||||
UUID account = "00e77300-0000-0000-0000-000000000001";
|
||||
UUID other = "00e77300-0000-0000-0000-000000000002";
|
||||
for (int phase = ERR_Phase.PREPARING; phase <= ERR_Phase.DEFEAT; phase++)
|
||||
{
|
||||
bool permitted = phase >= ERR_Phase.PRISON && phase <= ERR_Phase.RADIO_DEFENCE;
|
||||
if (!Check(ERR_SpawnFinalizationRules.Allows(phase, true, false, account, account, true, true, true) == permitted, "finalization phase matrix " + phase.ToString())) return false;
|
||||
}
|
||||
if (!Check(!ERR_SpawnFinalizationRules.Allows(ERR_Phase.PRISON, true, true, account, account, true, true, true), "spent life cannot finalize a delayed admission")) return false;
|
||||
if (!Check(!ERR_SpawnFinalizationRules.Allows(ERR_Phase.PRISON, false, false, account, account, true, true, true), "disconnected account cannot finalize")) return false;
|
||||
if (!Check(!ERR_SpawnFinalizationRules.Allows(ERR_Phase.PRISON, true, false, account, other, true, true, true), "recycled connection identity cannot inherit a pending admission")) return false;
|
||||
if (!Check(!ERR_SpawnFinalizationRules.Allows(ERR_Phase.PRISON, true, false, account, account, false, true, true), "changed controller rejects stale admission")) return false;
|
||||
if (!Check(!ERR_SpawnFinalizationRules.Allows(ERR_Phase.PRISON, true, false, account, account, true, false, true), "changed request rejects stale admission")) return false;
|
||||
if (!Check(!ERR_SpawnFinalizationRules.Allows(ERR_Phase.PRISON, true, false, account, account, true, true, false), "existing controlled actor cannot be replaced by delayed admission")) return false;
|
||||
return Check(ERR_SpawnFinalizationRules.PositionClose("0.3 0.2 0", "0 0 0") && !ERR_SpawnFinalizationRules.PositionClose("0 2 0", "0 0 0") && !ERR_SpawnFinalizationRules.PositionClose("0.7 0 0", "0 0 0"), "native settling tolerance rejects horizontal and vertical preload drift");
|
||||
}
|
||||
protected bool Finalization()
|
||||
{
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
m_Request = SCR_FreeSpawnRequestComponent.Cast(m_Controller.FindComponent(SCR_FreeSpawnRequestComponent));
|
||||
if (m_Request) m_Handler = SCR_FreeSpawnHandlerComponent.Cast(m_Request.GetHandlerComponent());
|
||||
SCR_RespawnSystemComponent system = SCR_RespawnSystemComponent.GetInstance();
|
||||
m_Logic = ERR_SpawnLogic.Cast(system.GetSpawnLogic());
|
||||
if (!Check(m_Request && m_Handler && m_Logic, "real local free-spawn request, handler and admission logic exist")) return false;
|
||||
int id = m_Controller.GetPlayerId();
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
||||
if (!Check(life && life.UsedLife && life.Actor == m_Player && m_Controller.GetControlledEntity() == m_Player && !m_Logic.ERR_TestHasAdmission(id), "real prisoner has a spent life and no pending admission")) return false;
|
||||
m_Preload = Actor(m_Position);
|
||||
m_Data = new SCR_FreeSpawnData(m_Mode.Factions().PrisonerPrefab, m_Position);
|
||||
if (!Check(m_Preload && m_Logic.ERR_TestPendingActor(m_Request, m_Data, m_Preload), "isolated unpossessed actor receives an explicit test-owned admission ticket")) return false;
|
||||
SCR_FreeSpawnData different = new SCR_FreeSpawnData(m_Mode.Factions().PrisonerPrefab, m_Position);
|
||||
if (!Check(m_Handler.FinalizeRequest_S(m_Request, different, m_Preload) == SCR_ESpawnResult.SPAWN_NOT_ALLOWED && ERR_Actors.Alive(m_Preload), "different request payload cannot claim or delete the owned actor")) return false;
|
||||
return Check(m_Handler.FinalizeRequest_S(m_Request, m_Data, m_Preload) == SCR_ESpawnResult.SPAWN_NOT_ALLOWED, "native finalization refuses the spent-life preload");
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Rules()) return -1;
|
||||
map<int, vector> reservations = new map<int, vector>();
|
||||
vector first, second;
|
||||
if (!Check(m_Placement.Prison(m_Mode.PrisonPosition(), reservations, first), "native prison has a clear candidate")) return -1;
|
||||
reservations.Set(1, first);
|
||||
if (!Check(m_Placement.Prison(m_Mode.PrisonPosition(), reservations, second) && vector.DistanceXZ(first, second) >= 1.2, "IDs one and thirteen receive distinct reserved slots")) return -1;
|
||||
reservations.Set(13, second);
|
||||
vector position;
|
||||
int count = 2;
|
||||
while (count < 16 && m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position))
|
||||
{
|
||||
reservations.Set(100 + count, position);
|
||||
count++;
|
||||
}
|
||||
PrintFormat("ERR SPAWN CAPACITY: remaining clear prison positions=%1 plus current controlled prisoner", count);
|
||||
if (!Check(count >= 11 && !m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position), "occupied and pending slots exhaust a bounded grid without overlap or outside fallback")) return -1;
|
||||
vector prison = m_Mode.PrisonPosition();
|
||||
foreach (int id, vector reserved : reservations)
|
||||
if (!Check(Math.AbsFloat(reserved[0] - prison[0]) <= 4.6 && Math.AbsFloat(reserved[2] - prison[2]) <= 4.6, "candidate remains inside authored prison")) return -1;
|
||||
reservations.Remove(1);
|
||||
if (!Check(m_Placement.Prison(m_Mode.PrisonPosition(), reservations, position) && vector.DistanceXZ(position, first) < 0.1, "released pending reservation becomes available again")) return -1;
|
||||
m_Position = first;
|
||||
m_Blocker = Actor(first);
|
||||
if (!Check(m_Blocker != null, "uncontrolled physical body created at a previously clear slot")) return -1;
|
||||
m_Step = 1; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
map<int, vector> empty = new map<int, vector>();
|
||||
if (!Check(!m_Placement.Clear(m_Position, empty), "native character body blocks a slot even without a connected player ID")) return -1;
|
||||
ERR_Actors.DeleteSpawned(m_Blocker);
|
||||
m_Step = 2; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!Finalization()) return -1;
|
||||
m_Step = 3; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
int id = m_Controller.GetPlayerId();
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
||||
if (!Check(!m_Preload, "refused native preload was deleted after lifecycle processing")) return -1;
|
||||
m_Logic.ERR_TestClearAdmission(id);
|
||||
if (!Check(!m_Logic.ERR_TestHasAdmission(id) && life.UsedLife && life.Actor == m_Player && m_Controller.GetControlledEntity() == m_Player, "refusal releases reservations without changing life or controlled actor")) return -1;
|
||||
if (!Check(m_Handler.FinalizeRequest_S(m_Request, m_Data, m_Player) == SCR_ESpawnResult.SPAWN_NOT_ALLOWED && ERR_Actors.Alive(m_Player), "unticketed repeated callback cannot delete or replace the controlled prisoner")) return -1;
|
||||
m_Cancelled = Actor(m_Position);
|
||||
if (!Check(m_Cancelled && m_Logic.ERR_TestPendingActor(m_Request, m_Data, m_Cancelled), "second isolated pending actor prepared for cancellation")) return -1;
|
||||
if (!Check(m_Logic.ERR_TestDelayedLossPreservesAdmission(id, m_Controller) && m_Controller.ERR_ObserverState() == ERR_ObserverReason.NONE, "delayed loss callback preserves reacquired living controller and admission metadata")) return -1;
|
||||
m_Logic.ERR_TestClearAdmission(id);
|
||||
m_Step = 4; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
int id = m_Controller.GetPlayerId();
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
||||
if (!Check(!m_Cancelled && !m_Logic.ERR_TestHasAdmission(id) && life.UsedLife && life.Actor == m_Player, "pending-request cancellation cleans its actor and never grants another life")) return -1;
|
||||
m_Drift = Actor(m_Position);
|
||||
if (!Check(m_Drift != null, "physical actor created for vertical drift check")) return -1;
|
||||
vector transform[4]; m_Drift.GetWorldTransform(transform);
|
||||
transform[3] = m_Position + "0 10 0";
|
||||
ChimeraCharacter.Cast(m_Drift).Teleport(transform);
|
||||
m_Step = 5; m_Wait = now + 750;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(!ERR_SpawnFinalizationRules.PositionClose(m_Drift.GetOrigin(), m_Position), "actual vertically displaced character fails finalization position tolerance after native movement")) return -1;
|
||||
ERR_Actors.DeleteSpawned(m_Drift);
|
||||
m_Step = 6;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
#ifdef WORKBENCH
|
||||
[BaseContainerProps(category: "Respawn")]
|
||||
modded class ERR_SpawnLogic
|
||||
{
|
||||
ERR_LocalAdmission ERR_LocalProbeHelper()
|
||||
{
|
||||
if (!ERR_NativeLocalAdmissionProbe.Enabled()) return null;
|
||||
return m_LocalAdmission;
|
||||
}
|
||||
bool ERR_LocalProbeIdle(int connection)
|
||||
{
|
||||
return ERR_NativeLocalAdmissionProbe.Enabled() && !m_Attempts.Contains(connection) && !m_Retries.Contains(connection) && !m_Pending.Contains(connection) && !m_AuthorizedPositions.Contains(connection) && m_SpawnTickets.IsEmpty();
|
||||
}
|
||||
bool ERR_LocalProbeClosed()
|
||||
{
|
||||
return ERR_NativeLocalAdmissionProbe.Enabled() && m_AdmissionsStopped && m_LocalAdmissionFailed;
|
||||
}
|
||||
bool ERR_LocalProbeRefuseController(int connection)
|
||||
{
|
||||
if (!ERR_NativeLocalAdmissionProbe.Enabled() || !m_LocalAdmission || !m_LocalAdmission.OfflineMode() || !m_LocalAdmission.Selected() || !m_Identities.Contains(connection)) return false;
|
||||
// Deliberately invalid METHOD ARGUMENT, not a fabricated native disconnect or
|
||||
// replacement controller. The real controller and backend remain untouched.
|
||||
UUID rejected = m_LocalAdmission.Resolve(ERR_GameMode.Get(), connection, null, m_Identities.Get(connection));
|
||||
if (!rejected.IsNull()) return false;
|
||||
return !CheckLocalAdmissionContext();
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_NativeLocalAdmissionProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected SCR_PlayerController m_Controller;
|
||||
protected IEntity m_Actor;
|
||||
protected ERR_SpawnLogic m_Logic;
|
||||
protected ERR_LocalAdmission m_Helper;
|
||||
protected ERR_PlayerLife m_Life;
|
||||
protected UUID m_Key;
|
||||
protected int m_Connection;
|
||||
protected int m_Participants;
|
||||
protected int m_Changes;
|
||||
protected int m_Step;
|
||||
protected float m_Deadline;
|
||||
protected float m_Wait;
|
||||
protected bool m_Listening;
|
||||
protected bool m_Stopped;
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "local-admission";
|
||||
}
|
||||
void ERR_NativeLocalAdmissionProbe(ERR_GameMode mode)
|
||||
{
|
||||
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: local admission " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: local admission " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected void Changed(int connection) { m_Changes++; }
|
||||
protected int Participants()
|
||||
{
|
||||
array<ref ERR_PlayerLife> lives = {}; m_Mode.Players().Participants(lives); return lives.Count();
|
||||
}
|
||||
protected bool Unchanged()
|
||||
{
|
||||
return m_Controller && m_Actor && m_Life && m_Mode.Players().Get(m_Connection) == m_Life && m_Mode.Players().CurrentActor(m_Actor) == m_Life && m_Life.AccountId == m_Key && m_Life.Connected && m_Life.UsedLife && m_Life.Actor == m_Actor && m_Controller.GetControlledEntity() == m_Actor && GetGame().GetPlayerManager().GetPlayerController(m_Connection) == m_Controller && Participants() == m_Participants && !m_Mode.Players().CanEnter(m_Connection, ERR_Phase.PRISON) && m_Logic.ERR_LocalProbeIdle(m_Connection);
|
||||
}
|
||||
protected bool DisposableHelpers()
|
||||
{
|
||||
ERR_LocalAdmission disconnected = new ERR_LocalAdmission(m_Mode);
|
||||
if (!Check(disconnected.Resolve(m_Mode, m_Connection, m_Controller, m_Key) == m_Key && disconnected.Validate() && !disconnected.LocalKey(), "disposable helper accepts actual sole native controller using existing round key")) return false;
|
||||
// Opaque alternative input only: no backend read/write, roster bind or login.
|
||||
UUID alternative = "00e77500-0000-0000-0000-000000000001";
|
||||
if (!Check(disconnected.Resolve(m_Mode, m_Connection, m_Controller, alternative) == m_Key && disconnected.Resolve(m_Mode, m_Connection, m_Controller, UUID.NULL_UUID) == m_Key, "late opaque identity input and null input retain the selected key")) return false;
|
||||
if (!Check(!disconnected.Disconnected(m_Connection + 1) && disconnected.Validate(), "unrelated disconnect argument does not close helper")) return false;
|
||||
if (!Check(disconnected.Disconnected(m_Connection) && !disconnected.Validate() && disconnected.Resolve(m_Mode, m_Connection, m_Controller, m_Key).IsNull(), "matching disconnect argument latches disposable helper closed")) return false;
|
||||
disconnected.Stop();
|
||||
if (!Check(!disconnected.Validate() && disconnected.Resolve(m_Mode, m_Connection, m_Controller, m_Key).IsNull(), "stopped disposable helper cannot reopen with the genuine controller")) return false;
|
||||
ERR_LocalAdmission invalid = new ERR_LocalAdmission(m_Mode);
|
||||
if (!Check(invalid.Resolve(m_Mode, m_Connection, null, m_Key).IsNull() && invalid.Resolve(m_Mode, m_Connection, m_Controller, m_Key).IsNull() && !invalid.Selected(), "invalid controller argument before selection fails closed without selecting another life")) return false;
|
||||
invalid.Stop();
|
||||
Print("ERR LOCAL ADMISSION LIMIT: disposable method-argument fault injection; no native disconnect or replacement occurred; Listen/Dedicated pass-through remains source-policy coverage, not multiplayer evidence");
|
||||
return true;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Listening && m_Mode) m_Mode.Players().OnChanged().Remove(Changed);
|
||||
m_Listening = false;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (!Enabled() || now > m_Deadline) { Check(false, "explicit enabled fixture completes within fifteen seconds"); return -1; }
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
if (respawn) m_Logic = ERR_SpawnLogic.Cast(respawn.GetSpawnLogic());
|
||||
if (!Check(RplSession.Mode() == RplMode.None && m_Controller && m_Logic && GetGame().GetPlayerManager().GetPlayerCount() == 1, "actual standalone session has one native local controller and successor spawn logic")) return -1;
|
||||
m_Connection = m_Controller.GetPlayerId(); m_Actor = m_Controller.GetControlledEntity();
|
||||
m_Life = m_Mode.Players().Get(m_Connection); m_Helper = m_Logic.ERR_LocalProbeHelper();
|
||||
if (!Check(m_Actor && m_Life && m_Helper && m_Helper.OfflineMode() && m_Helper.Selected() && m_Helper.Validate(), "real native admission already created the local controlled prisoner and selected helper")) return -1;
|
||||
m_Key = m_Life.AccountId; m_Participants = Participants();
|
||||
if (!Check(!m_Key.IsNull() && Unchanged(), "existing prisoner has one spent life and no pending spawn metadata")) return -1;
|
||||
if (!DisposableHelpers()) return -1;
|
||||
m_Mode.Players().OnChanged().Insert(Changed); m_Listening = true;
|
||||
Print("ERR LOCAL ADMISSION REPLAY: invoking existing callback twice for already admitted real controller; this does not represent new native authentication/audits");
|
||||
m_Logic.OnPlayerAuditSuccess_S(m_Connection);
|
||||
m_Logic.OnPlayerAuditSuccess_S(m_Connection);
|
||||
if (!Check(Unchanged() && m_Changes == 0, "replayed callbacks preserve exact roster object, used life, key, actor and no roster-change event")) return -1;
|
||||
m_Step = 1; m_Wait = now + 1500; return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!Check(Unchanged() && m_Changes == 0 && m_Helper.Validate(), "native update interval after replay creates no replacement actor, life or pending request")) return -1;
|
||||
if (!Check(m_Logic.ERR_LocalProbeRefuseController(m_Connection), "live helper rejects invalid controller argument through production context guard")) return -1;
|
||||
// The guard has synchronously entered the real terminal transition and
|
||||
// stopped services. No second callback, login, UUID or actor is introduced.
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.DEFEAT && m_Mode.EndReason().Contains("offline player context changed") && m_Logic.ERR_LocalProbeClosed() && !m_Helper.Validate() && Unchanged(), "production failure closes admission and ends round while preserving original used life and controlled actor")) return -1;
|
||||
Stop(); m_Step = 2; return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
#ifdef WORKBENCH
|
||||
// Opt-in native medical-effects fixture. Treatment enters at the real item's
|
||||
// completion boundary; manual user actions/animations and remote clients are not
|
||||
// simulated. Only fixture-owned characters are injured or deleted.
|
||||
class ERR_NativeMedicalProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Casualty;
|
||||
protected IEntity m_Healer;
|
||||
protected IEntity m_Dressing;
|
||||
protected IEntity m_Saline;
|
||||
protected IEntity m_SpareSaline;
|
||||
protected SCR_CharacterDamageManagerComponent m_Damage;
|
||||
protected CharacterControllerComponent m_Controller;
|
||||
protected SCR_CharacterBloodHitZone m_Blood;
|
||||
protected SCR_SalineStorageComponent m_SalineStorage;
|
||||
protected SCR_InventoryStorageManagerComponent m_Inventory;
|
||||
protected ref array<IEntity> m_Owned = {};
|
||||
protected int m_Step;
|
||||
protected int m_Result;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Recovered;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
protected float m_InfusionStarted;
|
||||
protected float m_SeedBlood;
|
||||
protected float m_PreInfusionBlood;
|
||||
protected float m_RegenSpeed;
|
||||
protected float m_NextDiagnostic;
|
||||
|
||||
void ERR_NativeMedicalProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: medical " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: medical " + detail, LogLevel.ERROR);
|
||||
Trace("failure");
|
||||
m_Result = -1;
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected IEntity Spawn(ResourceName prefab, vector position)
|
||||
{
|
||||
IEntity entity = ERR_Actors.Spawn(prefab, ERR_Actors.Ground(position));
|
||||
if (entity) { m_Owned.Insert(entity); m_Mode.Own(entity); }
|
||||
return entity;
|
||||
}
|
||||
protected int SalineEffects()
|
||||
{
|
||||
if (!m_Damage) return 0;
|
||||
array<ref SCR_PersistentDamageEffect> effects = {};
|
||||
m_Damage.GetPersistentEffects(effects);
|
||||
int count;
|
||||
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||
if (SCR_SalineDamageEffect.Cast(effect)) count++;
|
||||
return count;
|
||||
}
|
||||
protected int AttachedBags()
|
||||
{
|
||||
if (!m_SalineStorage) return 0;
|
||||
array<IEntity> items = {};
|
||||
m_SalineStorage.GetAll(items);
|
||||
return items.Count();
|
||||
}
|
||||
protected void Trace(string stage)
|
||||
{
|
||||
if (!m_Blood || !m_Controller || !m_Damage) return;
|
||||
PrintFormat("ERR MEDICAL STATE: stage=%1 blood=%2 bloodState=%3 unconscious=%4 bleeding=%5 salineEffects=%6 attachedBags=%7", stage, m_Blood.GetHealth(), m_Blood.GetDamageState(), m_Controller.IsUnconscious(), m_Damage.IsBleeding(), SalineEffects(), AttachedBags());
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
// Items first, then characters; native scheduled saline deletion is null-safe.
|
||||
for (int i = m_Owned.Count() - 1; i >= 0; i--) ERR_Actors.DeleteSpawned(m_Owned[i]);
|
||||
m_Owned.Clear();
|
||||
m_Casualty = null;
|
||||
m_Healer = null;
|
||||
m_Dressing = null;
|
||||
m_Saline = null;
|
||||
m_SpareSaline = null;
|
||||
m_Damage = null;
|
||||
m_Controller = null;
|
||||
m_Blood = null;
|
||||
m_SalineStorage = null;
|
||||
m_Inventory = null;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Result != 0) return m_Result;
|
||||
if (m_Stopped) return -1;
|
||||
if (!Replication.IsServer() || !m_Mode || !m_Mode.Active())
|
||||
{
|
||||
Check(false, "fixture requires the active authority");
|
||||
return -1;
|
||||
}
|
||||
if (m_Deadline > 0 && now > m_Deadline)
|
||||
{
|
||||
Check(false, "native medical state timed out at step " + m_Step.ToString());
|
||||
return -1;
|
||||
}
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
// Keep fixture actors separate from the real prisoner spawn.
|
||||
vector anchor = m_Mode.PrisonPosition() + "100 0 100";
|
||||
m_Casualty = Spawn(m_Mode.Factions().PrisonerPrefab, anchor);
|
||||
m_Healer = Spawn(m_Mode.Factions().PrisonerPrefab, anchor + "3 0 0");
|
||||
m_Dressing = Spawn("{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et", anchor + "4 0 0");
|
||||
m_Saline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "5 0 0");
|
||||
m_SpareSaline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "6 0 0");
|
||||
if (!Check(m_Casualty && m_Healer && m_Dressing && m_Saline && m_SpareSaline, "two isolated characters and finite stock-type medicine spawned")) return -1;
|
||||
if (!Check(!ERR_Actors.Player(m_Casualty) && !ERR_Actors.Player(m_Healer), "neither medical actor is a real player")) return -1;
|
||||
m_Deadline = now + 120000;
|
||||
m_Wait = now + 1000;
|
||||
m_Step = 1;
|
||||
return 0;
|
||||
}
|
||||
if (!ERR_Actors.Alive(m_Casualty) || !ERR_Actors.Alive(m_Healer))
|
||||
{
|
||||
Check(false, "owned casualty or healer died unexpectedly");
|
||||
return -1;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
m_Damage = SCR_CharacterDamageManagerComponent.Cast(m_Casualty.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||
m_Controller = CharacterControllerComponent.Cast(m_Casualty.FindComponent(CharacterControllerComponent));
|
||||
m_SalineStorage = SCR_SalineStorageComponent.Cast(m_Casualty.FindComponent(SCR_SalineStorageComponent));
|
||||
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Healer.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!Check(m_Damage && m_Controller && m_SalineStorage && m_Inventory, "native health, controller and medical storage components available")) return -1;
|
||||
m_Blood = m_Damage.GetBloodHitZone();
|
||||
if (!Check(m_Blood && !m_Controller.IsUnconscious() && !m_Damage.IsBleeding(), "casualty begins healthy and conscious")) return -1;
|
||||
if (!Check(m_Inventory.TryInsertItem(m_Dressing) && m_Inventory.TryInsertItem(m_Saline) && m_Inventory.TryInsertItem(m_SpareSaline), "finite medical items enter healer inventory")) return -1;
|
||||
SCR_ConsumableItemComponent dressing = SCR_ConsumableItemComponent.Cast(m_Dressing.FindComponent(SCR_ConsumableItemComponent));
|
||||
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
|
||||
if (!Check(dressing && saline, "stock items contain actual native consumable components")) return -1;
|
||||
SCR_ConsumableBandage bandageEffect = SCR_ConsumableBandage.Cast(dressing.GetConsumableEffect());
|
||||
SCR_ConsumableSalineBag salineEffect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
|
||||
SCR_EConsumableFailReason reason;
|
||||
if (!Check(bandageEffect && salineEffect && !bandageEffect.CanApplyEffect(m_Casualty, m_Healer, reason) && !salineEffect.CanApplyEffect(m_Casualty, m_Healer, reason), "healthy casualty rejects bandage and saline")) return -1;
|
||||
array<HitZone> zones = {};
|
||||
m_Damage.GetHitZonesOfGroupFromOwner(ECharacterHitZoneGroup.LEFTARM, zones);
|
||||
SCR_CharacterHitZone wound;
|
||||
foreach (HitZone zone : zones)
|
||||
{
|
||||
SCR_CharacterHitZone candidate = SCR_CharacterHitZone.Cast(zone);
|
||||
if (candidate && candidate.GetMaxBleedingRate() > 0) { wound = candidate; break; }
|
||||
}
|
||||
if (!Check(wound != null, "native left-arm hit zone can bleed")) return -1;
|
||||
wound.SetHealthScaled(0.5);
|
||||
m_Damage.AddBleedingEffectOnHitZone(wound);
|
||||
float threshold = m_Blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS) * m_Blood.GetMaxHealth();
|
||||
float destroyed = m_Blood.GetDamageStateThreshold(ECharacterBloodState.DESTROYED) * m_Blood.GetMaxHealth();
|
||||
if (!Check(threshold - destroyed > 200, "native blood thresholds permit a survivable recovery fixture")) return -1;
|
||||
// Initial casualty condition only. All subsequent blood changes are native effects.
|
||||
m_Blood.SetHealth(threshold - 100);
|
||||
m_SeedBlood = m_Blood.GetHealth();
|
||||
m_Wait = now + 5000;
|
||||
m_Step = 2;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
Trace("untreated blood loss");
|
||||
if (!Check(m_Damage.IsBleeding() && m_Blood.GetHealth() < m_SeedBlood && m_Controller.IsUnconscious(), "native bleeding reduces real blood and causes unconsciousness")) return -1;
|
||||
SCR_ConsumableItemComponent dressing = SCR_ConsumableItemComponent.Cast(m_Dressing.FindComponent(SCR_ConsumableItemComponent));
|
||||
SCR_ConsumableBandage effect = SCR_ConsumableBandage.Cast(dressing.GetConsumableEffect());
|
||||
SCR_EConsumableFailReason reason;
|
||||
if (!Check(effect.CanApplyEffectToHZ(m_Casualty, m_Healer, ECharacterHitZoneGroup.LEFTARM, reason), "real dressing accepts bleeding arm")) return -1;
|
||||
ItemUseParameters parameters = effect.GetAnimationParameters(m_Dressing, m_Casualty, ECharacterHitZoneGroup.LEFTARM);
|
||||
if (!Check(parameters && effect.GetDeleteOnUse(), "native dressing parameters and finite consumption policy are valid")) return -1;
|
||||
dressing.ApplyItemEffect(m_Casualty, m_Healer, parameters, m_Dressing, effect.GetDeleteOnUse());
|
||||
m_Wait = now + 500;
|
||||
m_Step = 3;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!Check(!m_Dressing && !m_Damage.IsBleeding() && m_Damage.GetGroupDamageOverTime(ECharacterHitZoneGroup.LEFTARM, EDamageType.BLEEDING) == 0, "dressing is consumed and native bleeding is stopped")) return -1;
|
||||
if (!Check(m_Controller.IsUnconscious(), "bandage alone has not fabricated blood recovery")) return -1;
|
||||
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
|
||||
SCR_ConsumableSalineBag effect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
|
||||
SCR_EConsumableFailReason reason;
|
||||
if (!Check(effect && effect.CanApplyEffect(m_Casualty, m_Healer, reason) && !effect.GetDeleteOnUse(), "native saline accepts real blood loss and retains bag for infusion")) return -1;
|
||||
ItemUseParameters parameters = effect.GetAnimationParameters(m_Saline, m_Casualty);
|
||||
if (!Check(parameters != null, "native saline item parameters available")) return -1;
|
||||
m_PreInfusionBlood = m_Blood.GetHealth();
|
||||
m_RegenSpeed = effect.GetItemRegenSpeed();
|
||||
saline.ApplyItemEffect(m_Casualty, m_Healer, parameters, m_Saline, effect.GetDeleteOnUse());
|
||||
m_InfusionStarted = now;
|
||||
m_Wait = now + 1000;
|
||||
m_Step = 4;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
array<IEntity> attached = {};
|
||||
m_SalineStorage.GetAll(attached);
|
||||
if (!Check(m_Saline && attached.Count() == 1 && attached.Contains(m_Saline) && m_Damage.GetGroupSalineBagged(ECharacterHitZoneGroup.RIGHTARM) && SalineEffects() == 1, "real saline bag occupies native treatment slot with one healing effect")) return -1;
|
||||
SCR_ConsumableItemComponent spare = SCR_ConsumableItemComponent.Cast(m_SpareSaline.FindComponent(SCR_ConsumableItemComponent));
|
||||
SCR_EConsumableFailReason reason;
|
||||
if (!Check(!spare.GetConsumableEffect().CanApplyEffect(m_Casualty, m_Healer, reason) && reason == SCR_EConsumableFailReason.ALREADY_APPLIED && m_SpareSaline, "occupied treatment slot rejects another bag without consuming spare")) return -1;
|
||||
m_NextDiagnostic = now;
|
||||
m_Step = 5;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (now >= m_NextDiagnostic) { Trace("native infusion"); m_NextDiagnostic = now + 10000; }
|
||||
if (!m_Controller.IsUnconscious()) m_Recovered = true;
|
||||
if (m_Saline || AttachedBags() != 0 || SalineEffects() != 0 || m_Damage.GetGroupSalineBagged(ECharacterHitZoneGroup.RIGHTARM)) return 0;
|
||||
float requiredGain = Math.Min(m_RegenSpeed * 60, (m_Blood.GetMaxHealth() - m_PreInfusionBlood) * 0.8);
|
||||
if (!Check(m_RegenSpeed > 0 && m_Blood.GetHealth() - m_PreInfusionBlood >= requiredGain, "native infusion restores substantial blood without setting health again")) return -1;
|
||||
if (!Check(m_Recovered && !m_Controller.IsUnconscious() && ERR_Actors.CanFight(m_Casualty), "casualty regains consciousness through native health recovery")) return -1;
|
||||
if (!Check(now - m_InfusionStarted >= 85000 && !m_Saline && AttachedBags() == 0 && SalineEffects() == 0, "finite saline expires after native duration and releases treatment state")) return -1;
|
||||
if (!Check(m_SpareSaline != null, "unused spare survives the completed infusion")) return -1;
|
||||
InventoryItemComponent spareItem = InventoryItemComponent.Cast(m_SpareSaline.FindComponent(InventoryItemComponent));
|
||||
if (!Check(spareItem && spareItem.GetParentSlot(), "rejected spare bag remains a physical inventory item")) return -1;
|
||||
m_Result = 1;
|
||||
Stop();
|
||||
Print("ERR MEDICAL RESULT: native blood loss, finite treatment and recovery; not manual action or multiplayer acceptance");
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
#ifdef WORKBENCH
|
||||
// Explicit disposable-world fixture: destroy all four authored radio documents,
|
||||
// then move the real participant to the station. No replacement clues or actors
|
||||
// are used to grant discovery; the normal site tick must identify the station.
|
||||
class ERR_NativeObjectiveLossProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected ERR_SiteRuntime m_Radio;
|
||||
protected ref array<IEntity> m_Documents = {};
|
||||
protected IEntity m_NonParticipant;
|
||||
protected IEntity m_Blocker;
|
||||
protected bool m_Partial;
|
||||
protected bool m_SettingSaved;
|
||||
protected int m_RevealChance;
|
||||
protected int m_Step;
|
||||
protected int m_Identifications;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
void ERR_NativeObjectiveLossProbe(ERR_GameMode mode, IEntity player, bool partial)
|
||||
{
|
||||
m_Mode = mode; m_Player = player; m_Partial = partial;
|
||||
m_Mode.OnLocationDiscovered().Insert(Discovered);
|
||||
}
|
||||
protected void Discovered(ERR_LocationComponent location, int previous, int current)
|
||||
{
|
||||
if (m_Radio && location == m_Radio.Location && current == 2) m_Identifications++;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: objective-loss " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: objective-loss " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected void Move(vector position)
|
||||
{
|
||||
vector transform[4];
|
||||
m_Player.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(position);
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
}
|
||||
protected void CollectDocuments(out array<IEntity> documents)
|
||||
{
|
||||
documents.Clear();
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
array<IEntity> stock = {};
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().Find(location.Id());
|
||||
if (!site || !site.Container) continue;
|
||||
ERR_Inventory.AppendUnique(InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent)), stock);
|
||||
}
|
||||
ERR_Inventory.AppendUnique(InventoryStorageManagerComponent.Cast(m_Player.FindComponent(InventoryStorageManagerComponent)), stock);
|
||||
foreach (IEntity item : stock)
|
||||
{
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(item.FindComponent(ERR_DocumentComponent));
|
||||
if (document && document.TargetId() == m_Radio.Location.Id()) documents.Insert(item);
|
||||
}
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Mode)
|
||||
{
|
||||
m_Mode.OnLocationDiscovered().Remove(Discovered);
|
||||
if (m_SettingSaved) m_Mode.Settings().Set("intelRevealChancePercent", m_RevealChance);
|
||||
}
|
||||
m_SettingSaved = false;
|
||||
ERR_Actors.DeleteSpawned(m_NonParticipant);
|
||||
ERR_Actors.DeleteSpawned(m_Blocker);
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Deadline == 0) m_Deadline = now + 60000;
|
||||
if (now > m_Deadline || !m_Mode.Active() || !ERR_Actors.CanFight(m_Player))
|
||||
{
|
||||
Check(false, "fixture lost its active conscious participant or exceeded its deadline");
|
||||
return -1;
|
||||
}
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_Radio = m_Mode.Sites().Find("everon-radio-1");
|
||||
if (!Check(m_Radio && m_Radio.Location && m_Radio.Location.Knowledge() == 0 && m_Radio.Definition.DiscoverRadius == 8, "authored station is initially unknown with bounded eight-metre discovery")) return -1;
|
||||
CollectDocuments(m_Documents);
|
||||
if (!Check(m_Documents.Count() == 4, "four distinct physical documents exist before deliberate loss")) return -1;
|
||||
if (!Check(m_Mode.UnlockPrison(m_Player) && m_Mode.Phase() == ERR_Phase.RADIO_SEARCH, "real prisoner enters radio search through ordinary mission interface")) return -1;
|
||||
if (m_Partial)
|
||||
{
|
||||
m_RevealChance = m_Mode.Settings().Get("intelRevealChancePercent");
|
||||
m_SettingSaved = true;
|
||||
if (!Check(m_Mode.Settings().Set("intelRevealChancePercent", 0), "isolated first document produces only partial knowledge")) return -1;
|
||||
ERR_SiteRuntime cache = m_Mode.Sites().Find("everon-supply-1");
|
||||
Move(cache.Container.GetOrigin() + "0 0 2");
|
||||
InventoryStorageManagerComponent source = InventoryStorageManagerComponent.Cast(cache.Container.FindComponent(InventoryStorageManagerComponent));
|
||||
SCR_InventoryStorageManagerComponent target = SCR_InventoryStorageManagerComponent.Cast(m_Player.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
array<IEntity> stock = {};
|
||||
ERR_Inventory.Items(source, stock);
|
||||
IEntity selected;
|
||||
foreach (IEntity item : stock) if (m_Documents.Contains(item)) { selected = item; break; }
|
||||
BaseInventoryStorageComponent destination;
|
||||
if (selected && target) destination = target.FindStorageForItem(selected);
|
||||
if (!Check(destination && source.TryMoveItemToStorage(selected, destination), "one real document transfer accepted before the remaining clues are lost")) return -1;
|
||||
m_Wait = now + 8000;
|
||||
}
|
||||
m_Step = 1;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
int expected;
|
||||
if (m_Partial) expected = 1;
|
||||
if (!Check(m_Radio.Location.Knowledge() == expected, "ordinary clue processing leaves the requested unknown or partial station state")) return -1;
|
||||
foreach (IEntity document : m_Documents) if (document) SCR_EntityHelper.DeleteEntityAndChildren(document);
|
||||
m_Step = 2;
|
||||
m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
foreach (IEntity removed : m_Documents)
|
||||
if (!Check(!removed, "each original physical document is deleted")) return -1;
|
||||
array<IEntity> remaining = {};
|
||||
CollectDocuments(remaining);
|
||||
if (!Check(remaining.IsEmpty() && m_Mode.Sites().Prepare() == ERR_PrepareResult.READY, "all targeted documents are absent and preparation does not replenish them")) return -1;
|
||||
Move(m_Radio.Location.Position() + "0 0 20");
|
||||
m_NonParticipant = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, ERR_Actors.Ground(m_Radio.Location.Position() + "3 0 0"));
|
||||
if (!Check(m_NonParticipant != null, "disposable unpossessed character created for authorization rejection")) return -1;
|
||||
m_Mode.Own(m_NonParticipant);
|
||||
if (!Check(!m_Mode.Sites().CanObserve(m_NonParticipant, m_Radio), "unpossessed character cannot identify the station")) return -1;
|
||||
m_Step = 3;
|
||||
m_Wait = now + 6000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
int expected;
|
||||
if (m_Partial) expected = 1;
|
||||
if (!Check(m_Radio.Location.Knowledge() == expected && !m_Mode.Sites().CanObserve(m_Player, m_Radio), "distant participant and nearby nonparticipant do not reveal the station")) return -1;
|
||||
ERR_Actors.DeleteSpawned(m_NonParticipant);
|
||||
m_Blocker = ERR_Actors.Spawn("{A67420C1FB91856E}Prefabs/ERR/PrisonWall.et", ERR_Actors.Ground(m_Radio.Location.Position() + "-1.5 0 2"));
|
||||
if (!Check(m_Blocker != null, "physical obstruction created before close approach")) return -1;
|
||||
m_Mode.Own(m_Blocker);
|
||||
Move(m_Radio.Location.Position() + "0 0 4");
|
||||
m_Step = 4;
|
||||
m_Wait = now + 6000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
int expected;
|
||||
if (m_Partial) expected = 1;
|
||||
PrintFormat("ERR OBJECTIVE LOSS: player=%1 radio=%2 knowledge=%3 visible=%4", m_Player.GetOrigin(), m_Radio.Location.Position(), m_Radio.Location.Knowledge(), m_Mode.Sites().CanObserve(m_Player, m_Radio));
|
||||
if (!Check(m_Radio.Location.Knowledge() == expected && !m_Mode.Sites().CanObserve(m_Player, m_Radio), "a nearby station behind a wall remains unidentified")) return -1;
|
||||
ERR_Actors.DeleteSpawned(m_Blocker);
|
||||
m_Step = 5;
|
||||
m_Wait = now + 6000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
TraceParam sight = new TraceParam();
|
||||
sight.Start = m_Player.GetOrigin() + "0 1.5 0";
|
||||
sight.End = m_Radio.Location.Position() + "0 1.3 0";
|
||||
sight.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
sight.LayerMask = TRACE_LAYER_CAMERA;
|
||||
sight.Exclude = m_Player;
|
||||
float fraction = m_Mode.GetWorld().TraceMove(sight, null);
|
||||
PrintFormat("ERR OBJECTIVE LOSS FINAL: player=%1 radio=%2 knowledge=%3 identifications=%4 visible=%5", m_Player.GetOrigin(), m_Radio.Location.Position(), m_Radio.Location.Knowledge(), m_Identifications, m_Mode.Sites().CanObserve(m_Player, m_Radio));
|
||||
PrintFormat("ERR OBJECTIVE LOSS SIGHT: fraction=%1 hit=%2 blockerRemaining=%3", fraction, sight.TraceEnt, m_Blocker);
|
||||
if (!Check(m_Mode.Sites().CanObserve(m_Player, m_Radio) && m_Radio.Location.Knowledge() == 2 && m_Identifications == 1, "actual participant with clear close approach identifies the station once through normal discovery")) return -1;
|
||||
array<IEntity> remaining = {};
|
||||
CollectDocuments(remaining);
|
||||
if (!Check(remaining.IsEmpty(), "physical discovery creates no replacement documents")) return -1;
|
||||
if (!Check(m_Mode.RadioDefence().CanBegin(m_Player, m_Radio.Location) && m_Mode.TuneRadio(m_Player, m_Radio.Location) && m_Mode.Phase() == ERR_Phase.RADIO_DEFENCE, "identified station permits ordinary authorized tuning after complete document loss")) return -1;
|
||||
Stop();
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+238
@@ -0,0 +1,238 @@
|
||||
#ifdef WORKBENCH
|
||||
// Exercise native task entries and detail text on the private test display.
|
||||
// This is local presentation evidence, not remote task replication acceptance.
|
||||
class ERR_NativeObjectiveProbe
|
||||
{
|
||||
protected static bool s_Failed;
|
||||
protected static Widget s_Entry;
|
||||
protected static Widget s_Detail;
|
||||
protected static SCR_Task s_Task;
|
||||
protected static SCR_TaskListEntryUIComponent s_EntryHandler;
|
||||
protected static SCR_TaskListEntryDescriptionUIComponent s_DetailHandler;
|
||||
protected static ref SCR_TaskListEntryDescriptionUIComponent s_ClosedHandler;
|
||||
protected static float s_CloseReadyAt;
|
||||
protected static SCR_Task s_IsolatedTask;
|
||||
protected static bool s_IsolatedDeleteRequested;
|
||||
protected static SCR_Task s_PreservedMissionTask;
|
||||
protected static ref SCR_TaskExecutor s_RestoreExecutor;
|
||||
static void BeginRound()
|
||||
{
|
||||
Stop();
|
||||
s_Failed = false;
|
||||
}
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: objective UI " + detail); return true; }
|
||||
s_Failed = true;
|
||||
Stop();
|
||||
Print("ERR SMOKE FAIL: objective UI " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
static void Stop()
|
||||
{
|
||||
if (s_EntryHandler && s_DetailHandler) s_EntryHandler.GetOnTaskVisualChanged().Remove(s_DetailHandler.InitDescription);
|
||||
if (s_Detail) s_Detail.RemoveFromHierarchy();
|
||||
if (s_Entry) s_Entry.RemoveFromHierarchy();
|
||||
s_Detail = null;
|
||||
s_Entry = null;
|
||||
s_DetailHandler = null;
|
||||
s_EntryHandler = null;
|
||||
s_Task = null;
|
||||
s_ClosedHandler = null;
|
||||
s_CloseReadyAt = 0;
|
||||
SCR_TaskSystem tasks = SCR_TaskSystem.GetInstance();
|
||||
if (tasks && s_IsolatedTask && !s_IsolatedDeleteRequested) tasks.DeleteTask(s_IsolatedTask);
|
||||
if (tasks && s_PreservedMissionTask && s_RestoreExecutor && !s_PreservedMissionTask.IsTaskAssignedTo(s_RestoreExecutor))
|
||||
tasks.AssignTask(s_PreservedMissionTask, s_RestoreExecutor, true);
|
||||
s_IsolatedTask = null;
|
||||
s_IsolatedDeleteRequested = false;
|
||||
s_PreservedMissionTask = null;
|
||||
s_RestoreExecutor = null;
|
||||
}
|
||||
// Emit unchanged task state: exercise actual invoker subscriptions without
|
||||
// changing assignment, visibility, ownership or mission progression.
|
||||
protected static void EmitTaskCallbacks(SCR_Task task)
|
||||
{
|
||||
task.GetOnTaskAssigneeAdded().Invoke(task, null, 0);
|
||||
task.GetOnTaskAssigneeRemoved().Invoke(task, null, 0);
|
||||
task.GetOnTaskUIVisibilityChanged().Invoke(task, task.GetTaskUIVisibility());
|
||||
task.GetOnTaskVisibilityChanged().Invoke(task, task.GetTaskVisibility());
|
||||
task.GetOnTaskOwnershipChanged().Invoke(task, task.GetTaskOwnership());
|
||||
task.GetOnDisplayDataChanged().Invoke();
|
||||
}
|
||||
static bool BeginCloseReopen(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (!Observe(mode)) return false;
|
||||
// Reinitialization is the vanilla route when the selected task changes.
|
||||
s_DetailHandler.InitDescription(s_Task);
|
||||
s_DetailHandler.ERR_ProbeWatch();
|
||||
EmitTaskCallbacks(s_Task);
|
||||
if (!Check(s_DetailHandler.ERR_ProbeTaskCallbacks() == 5, "rebinding keeps one subscription to each native task event")) return false;
|
||||
// Keep only the handler alive so a leaked event or queued overflow callback
|
||||
// can be counted safely after actual widget destruction. No manual unbinding.
|
||||
SCR_Task task = s_Task;
|
||||
ref SCR_TaskListEntryDescriptionUIComponent closed = s_DetailHandler;
|
||||
Stop();
|
||||
s_ClosedHandler = closed;
|
||||
s_ClosedHandler.ERR_ProbeClosed();
|
||||
if (!Check(s_ClosedHandler.ERR_ProbeWasDetached() && s_ClosedHandler.ERR_ProbeHasNoTask(), "closing native widget uses production detach and releases its task")) return false;
|
||||
EmitTaskCallbacks(task);
|
||||
if (!Check(s_ClosedHandler.ERR_ProbeLateCallbacks() == 0, "closed native widget receives no task callbacks")) return false;
|
||||
s_CloseReadyAt = now + 500;
|
||||
return true;
|
||||
}
|
||||
// One normal smoke tick allows native 100ms overflow work to become due.
|
||||
static int TickCloseReopen(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (s_Failed || !s_ClosedHandler) return -1;
|
||||
if (now < s_CloseReadyAt) return 0;
|
||||
if (!Check(s_ClosedHandler.ERR_ProbeLateCallbacks() == 0, "closed native widget has no delayed overflow callbacks")) return -1;
|
||||
if (!Observe(mode)) return -1;
|
||||
if (!Check(s_DetailHandler != null, "details reopen through native layout and current objective binding")) return -1;
|
||||
return 1;
|
||||
}
|
||||
// Dedicated objective-ui case: delete an owned test task before its panel.
|
||||
// The real objective remains alive and regains its local assignment immediately.
|
||||
static bool BeginDeletedTaskClose(ERR_GameMode mode, float now)
|
||||
{
|
||||
Stop();
|
||||
SCR_TaskSystem tasks = SCR_TaskSystem.GetInstance();
|
||||
s_PreservedMissionTask = mode.Objectives().Current();
|
||||
s_RestoreExecutor = SCR_TaskSystem.TaskExecutorFromPlayerID(SCR_PlayerController.GetLocalPlayerId());
|
||||
if (!Check(tasks && s_PreservedMissionTask && s_RestoreExecutor, "isolated deletion has a native task system, mission task and local executor")) return false;
|
||||
s_IsolatedTask = tasks.CreateTask("{1D0F815858EE24AD}Prefabs/Tasks/BaseTask.et", "erri-mission-probe-teardown", "Objective cleanup probe", "Isolated native task for widget teardown.");
|
||||
if (!Check(s_IsolatedTask && s_IsolatedTask != s_PreservedMissionTask, "deletion fixture owns a separate native task")) return false;
|
||||
tasks.SetTaskUIVisibility(s_IsolatedTask, SCR_ETaskUIVisibility.LIST_ONLY);
|
||||
// Vanilla permits one assigned task per executor. Cleanup restores the real one.
|
||||
tasks.AssignTask(s_IsolatedTask, s_RestoreExecutor, true);
|
||||
if (!Check(s_IsolatedTask.IsTaskAssignedTo(s_RestoreExecutor), "isolated task has an actual local assignee")) return false;
|
||||
s_Detail = GetGame().GetWorkspace().CreateWidgets("{552EB1921B5C548C}UI/layouts/Task/TaskListEntryDescription.layout");
|
||||
if (!Check(s_Detail != null, "isolated native details layout created")) return false;
|
||||
s_Detail.SetVisible(false);
|
||||
s_DetailHandler = SCR_TaskListEntryDescriptionUIComponent.Cast(s_Detail.FindHandler(SCR_TaskListEntryDescriptionUIComponent));
|
||||
if (!Check(s_DetailHandler != null, "isolated native details handler attached")) return false;
|
||||
s_DetailHandler.InitDescription(s_IsolatedTask);
|
||||
s_DetailHandler.ERR_ProbeWatch();
|
||||
s_IsolatedDeleteRequested = true;
|
||||
tasks.DeleteTask(s_IsolatedTask);
|
||||
// These are native weak references, not manually cleared probe variables.
|
||||
if (!Check(s_IsolatedTask == null && s_DetailHandler.ERR_ProbeHasNoTask(), "native task deletion clears both task and bound-widget weak references")) return false;
|
||||
s_ClosedHandler = s_DetailHandler;
|
||||
s_Detail.RemoveFromHierarchy();
|
||||
s_Detail = null;
|
||||
s_DetailHandler = null;
|
||||
s_ClosedHandler.ERR_ProbeClosed();
|
||||
if (!Check(s_ClosedHandler.ERR_ProbeWasDetached(), "deleted-task panel closes through production detach")) return false;
|
||||
tasks.AssignTask(s_PreservedMissionTask, s_RestoreExecutor, true);
|
||||
if (!Check(mode.Objectives().Current() == s_PreservedMissionTask && s_PreservedMissionTask.IsTaskAssignedTo(s_RestoreExecutor), "real mission objective survives isolated deletion and assignment is restored")) return false;
|
||||
s_CloseReadyAt = now + 500;
|
||||
return true;
|
||||
}
|
||||
static int TickDeletedTaskClose(ERR_GameMode mode, float now)
|
||||
{
|
||||
if (s_Failed || !s_ClosedHandler) return -1;
|
||||
if (now < s_CloseReadyAt) return 0;
|
||||
if (!Check(s_ClosedHandler.ERR_ProbeLateCallbacks() == 0, "deleted-task panel has no delayed callbacks after weak references clear")) return -1;
|
||||
if (!Check(s_PreservedMissionTask && mode.Objectives().Current() == s_PreservedMissionTask && s_PreservedMissionTask.IsTaskAssignedTo(s_RestoreExecutor), "real mission objective remains intact after delayed teardown check")) return -1;
|
||||
Stop();
|
||||
return 1;
|
||||
}
|
||||
static bool Observe(ERR_GameMode mode)
|
||||
{
|
||||
if (s_Failed) return false;
|
||||
SCR_Task task = mode.Objectives().Current();
|
||||
if (!Check(task != null, "current native task exists")) return false;
|
||||
if (s_Task != task)
|
||||
{
|
||||
Stop();
|
||||
s_Task = task;
|
||||
s_Entry = GetGame().GetWorkspace().CreateWidgets("{B71975032150BAFB}UI/layouts/Task/TaskListEntry.layout");
|
||||
s_Detail = GetGame().GetWorkspace().CreateWidgets("{552EB1921B5C548C}UI/layouts/Task/TaskListEntryDescription.layout");
|
||||
if (!Check(s_Entry && s_Detail, "native list and detail layouts created")) return false;
|
||||
s_Entry.SetVisible(false);
|
||||
s_Detail.SetVisible(false);
|
||||
s_EntryHandler = SCR_TaskListEntryUIComponent.Cast(s_Entry.FindHandler(SCR_TaskListEntryUIComponent));
|
||||
s_DetailHandler = SCR_TaskListEntryDescriptionUIComponent.Cast(s_Detail.FindHandler(SCR_TaskListEntryDescriptionUIComponent));
|
||||
if (!Check(s_EntryHandler && s_DetailHandler, "native handlers attached")) return false;
|
||||
s_EntryHandler.InitTask(task);
|
||||
s_DetailHandler.InitDescription(task);
|
||||
// Same visual-change route used by the vanilla task list.
|
||||
s_EntryHandler.GetOnTaskVisualChanged().Insert(s_DetailHandler.InitDescription);
|
||||
}
|
||||
RichTextWidget title = RichTextWidget.Cast(s_Entry.FindAnyWidget("m_wTaskTitle"));
|
||||
RichTextWidget description = RichTextWidget.Cast(s_Detail.FindAnyWidget("m_wDescription"));
|
||||
if (!Check(title && description && title.GetText() == task.GetTaskName() && description.GetText() == task.GetTaskDescription(), "open list and details contain current title and instructions")) return false;
|
||||
Widget showMap = s_Detail.FindAnyWidget("m_wButtonShowMap");
|
||||
if (!Check(showMap && !showMap.IsVisible(), "list-only task cannot expose an undiscovered map position")) return false;
|
||||
SCR_TaskExecutor player = SCR_TaskSystem.TaskExecutorFromPlayerID(SCR_PlayerController.GetLocalPlayerId());
|
||||
if (!Check(player && task.IsTaskAssignedTo(player), "actual local player receives the active objective")) return false;
|
||||
if (!Check(ERR_ObjectiveText.Extraction(12500).Contains("12.5") && !ERR_ObjectiveText.Extraction(12500).Contains("9 kilometres"), "extraction instructions follow a nondefault boundary")) return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Observers exist only in the opt-in native fixture. They do not unsubscribe or
|
||||
// cancel work: a late callback is counted as failure before avoiding dead-widget
|
||||
// dereferences, allowing the fixture to report the production cleanup defect.
|
||||
modded class SCR_TaskListEntryDescriptionUIComponent
|
||||
{
|
||||
protected bool m_ERRProbeWatching;
|
||||
protected bool m_ERRProbeClosed;
|
||||
protected bool m_ERRProbeDetached;
|
||||
protected int m_ERRProbeTaskCallbacks;
|
||||
protected int m_ERRProbeLateCallbacks;
|
||||
void ERR_ProbeWatch() { m_ERRProbeWatching = true; m_ERRProbeTaskCallbacks = 0; }
|
||||
void ERR_ProbeClosed() { m_ERRProbeClosed = true; }
|
||||
int ERR_ProbeTaskCallbacks() { return m_ERRProbeTaskCallbacks; }
|
||||
int ERR_ProbeLateCallbacks() { return m_ERRProbeLateCallbacks; }
|
||||
bool ERR_ProbeWasDetached() { return m_ERRProbeDetached; }
|
||||
bool ERR_ProbeHasNoTask() { return m_Task == null; }
|
||||
protected bool ERR_ProbeCallback(bool taskEvent, string kind)
|
||||
{
|
||||
if (!m_ERRProbeWatching) return false;
|
||||
if (m_ERRProbeClosed)
|
||||
{
|
||||
m_ERRProbeLateCallbacks++;
|
||||
PrintFormat("ERR OBJECTIVE LATE CALLBACK: kind=%1 count=%2 task=%3 detached=%4 root=%5", kind, m_ERRProbeLateCallbacks, m_Task != null, m_ERRProbeDetached, m_wRoot != null);
|
||||
return true;
|
||||
}
|
||||
if (taskEvent) m_ERRProbeTaskCallbacks++;
|
||||
return false;
|
||||
}
|
||||
override void HandlerDeattached(Widget w)
|
||||
{
|
||||
m_ERRProbeDetached = true;
|
||||
super.HandlerDeattached(w);
|
||||
}
|
||||
protected override void UpdateAssignees(notnull SCR_Task task, SCR_TaskExecutor newAssignee = null, int requesterID = 0)
|
||||
{
|
||||
if (ERR_ProbeCallback(true, "UpdateAssignees")) return;
|
||||
super.UpdateAssignees(task, newAssignee, requesterID);
|
||||
}
|
||||
protected override void OnTaskUIVisibilityChanged(SCR_Task task, SCR_ETaskUIVisibility visible)
|
||||
{
|
||||
if (ERR_ProbeCallback(true, "OnTaskUIVisibilityChanged")) return;
|
||||
super.OnTaskUIVisibilityChanged(task, visible);
|
||||
}
|
||||
protected override void OnTaskVisibilityChanged(SCR_Task task, SCR_ETaskVisibility visible)
|
||||
{
|
||||
if (ERR_ProbeCallback(true, "OnTaskVisibilityChanged")) return;
|
||||
super.OnTaskVisibilityChanged(task, visible);
|
||||
}
|
||||
protected override void OnTaskOwnershipChanged(SCR_Task task, SCR_ETaskOwnership ownership)
|
||||
{
|
||||
if (ERR_ProbeCallback(true, "OnTaskOwnershipChanged")) return;
|
||||
super.OnTaskOwnershipChanged(task, ownership);
|
||||
}
|
||||
protected override void CheckOverflow(Widget w)
|
||||
{
|
||||
if (ERR_ProbeCallback(false, "CheckOverflow")) return;
|
||||
super.CheckOverflow(w);
|
||||
}
|
||||
override void UpdateTask()
|
||||
{
|
||||
if (ERR_ProbeCallback(false, "UpdateTask")) return;
|
||||
super.UpdateTask();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
#ifdef WORKBENCH
|
||||
// Observe existing physical guards. No teleporting, spawned soldiers or forced
|
||||
// completion: a full ordered loop and physical movement must happen natively.
|
||||
class ERR_NativePatrolProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected string m_SiteId;
|
||||
protected ERR_SiteRuntime m_Site;
|
||||
protected IEntity m_Actor;
|
||||
protected vector m_Origin;
|
||||
protected float m_Deadline;
|
||||
protected int m_Completions;
|
||||
protected int m_LastIndex = -1;
|
||||
protected bool m_BadSequence;
|
||||
protected bool m_Moved;
|
||||
protected bool m_Started;
|
||||
protected bool m_Prepared;
|
||||
protected float m_AcquireDeadline;
|
||||
protected bool m_Finished;
|
||||
protected int m_LastQueueSample = -1;
|
||||
protected ref map<string, int> m_QueueBounds = new map<string, int>();
|
||||
void ERR_NativePatrolProbe(ERR_GameMode mode, string siteId = "everon-outpost-1") { m_Mode = mode; m_SiteId = siteId; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: patrol " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: patrol " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected void Completed(AIWaypoint waypoint)
|
||||
{
|
||||
if (!m_Site) return;
|
||||
int index = m_Site.PatrolWaypoints.Find(waypoint);
|
||||
if (index < 0) return;
|
||||
if (m_LastIndex >= 0 && index != (m_LastIndex + 1) % m_Site.PatrolWaypoints.Count()) m_BadSequence = true;
|
||||
m_LastIndex = index;
|
||||
m_Completions++;
|
||||
PrintFormat("ERR PATROL COMPLETED: site=%1 index=%2 count=%3", m_Site.Definition.Id, index, m_Completions);
|
||||
}
|
||||
protected bool DefinitionChecks()
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.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;
|
||||
ref array<ref ERR_SitePatrolPoint> savedRoute = site.Patrol;
|
||||
site.Patrol = new array<ref ERR_SitePatrolPoint>();
|
||||
site.Patrol.Insert(savedRoute[0]);
|
||||
if (!Check(!definition.Valid(), "single-point route rejected")) return false;
|
||||
for (int extra = 1; extra < 7; extra++) site.Patrol.Insert(savedRoute[extra % savedRoute.Count()]);
|
||||
if (!Check(!definition.Valid(), "over-six-point route rejected")) return false;
|
||||
site.Patrol.Clear();
|
||||
site.Patrol.Insert(null);
|
||||
site.Patrol.Insert(savedRoute[1]);
|
||||
if (!Check(!definition.Valid(), "missing point rejected")) return false;
|
||||
site.Patrol = savedRoute;
|
||||
vector saved = site.Patrol[0].Offset;
|
||||
site.Patrol[0].Offset = "61 0 0";
|
||||
if (!Check(!definition.Valid(), "unbounded route rejected")) return false;
|
||||
site.Patrol[0].Offset = "0 1 0";
|
||||
if (!Check(!definition.Valid(), "floating route rejected")) return false;
|
||||
site.Patrol[0].Offset = site.Patrol[1].Offset;
|
||||
if (!Check(!definition.Valid(), "duplicate route points rejected")) return false;
|
||||
site.Patrol[0].Offset = saved;
|
||||
site.Guarded = false;
|
||||
if (!Check(!definition.Valid(), "route without a physical guard rejected")) return false;
|
||||
site.Guarded = true;
|
||||
ERR_SiteDefinition prison = definition.Find("everon-prison-1");
|
||||
prison.Patrol = site.Patrol;
|
||||
if (!Check(!definition.Valid(), "prison patrol rejected; gate guards remain stationary")) return false;
|
||||
prison.Patrol = null;
|
||||
return Check(definition.Valid(), "isolated authoring mutations restored");
|
||||
}
|
||||
protected bool PreparedRoute(string id, bool deferDuplicateFailure = false)
|
||||
{
|
||||
ERR_SiteRuntime site = m_Mode.Sites().Find(id);
|
||||
if (!Check(site && site.Group && site.PatrolCycle && site.PatrolWaypoints.Count() == 4, id + " existing group has one cycle and four native moves")) return false;
|
||||
array<AIWaypoint> route = {};
|
||||
site.PatrolCycle.GetWaypoints(route);
|
||||
if (!Check(route.Count() == 4 && site.PatrolCycle.GetRerunCounter() == -1 && m_Mode.Sites().Owns(site.PatrolCycle), id + " finite owner holds repeating cycle")) return false;
|
||||
for (int i = 0; i < route.Count(); i++)
|
||||
{
|
||||
vector expected = site.Definition.Position + site.Definition.GuardOffset + site.Definition.Patrol[i].Offset;
|
||||
if (!Check(route[i] == site.PatrolWaypoints[i] && m_Mode.Sites().Owns(route[i]) && vector.DistanceXZ(route[i].GetOrigin(), expected) < 0.1, id + " authored point " + i.ToString())) return false;
|
||||
}
|
||||
array<AIWaypoint> queued = {};
|
||||
site.Group.GetWaypoints(queued);
|
||||
PrintFormat("ERR PATROL QUEUE: site=%1 count=%2 current=%3", id, queued.Count(), site.Group.GetCurrentWaypoint());
|
||||
for (int diagnosticIndex = 0; diagnosticIndex < Math.Min(queued.Count(), 20); diagnosticIndex++)
|
||||
{
|
||||
AIWaypoint entry = queued[diagnosticIndex];
|
||||
PrintFormat("ERR PATROL QUEUE ENTRY: site=%1 index=%2 point=%3 cycle=%4 current=%5 entity=%6", id, diagnosticIndex, site.PatrolWaypoints.Find(entry), entry == site.PatrolCycle, entry == site.Group.GetCurrentWaypoint(), entry);
|
||||
}
|
||||
// Diagnostic only: retain duplicate rejection at final acceptance, but first
|
||||
// observe whether native completion events execute the apparent repetitions.
|
||||
// GetWaypoints has no documented raw-queue/expanded-cycle distinction.
|
||||
int repeated;
|
||||
for (int j = 0; j < queued.Count(); j++)
|
||||
{
|
||||
if (!Check(queued[j] && (queued[j] == site.PatrolCycle || site.PatrolWaypoints.Contains(queued[j])), id + " queue contains only owned patrol references")) return false;
|
||||
for (int k = 0; k < j; k++) if (queued[j] == queued[k]) repeated++;
|
||||
}
|
||||
int currentIndex = site.PatrolWaypoints.Find(site.Group.GetCurrentWaypoint());
|
||||
string stateKey = id + ":" + currentIndex.ToString();
|
||||
if (m_QueueBounds.Contains(stateKey))
|
||||
{
|
||||
if (!Check(queued.Count() <= m_QueueBounds.Get(stateKey), id + " queue does not grow at the same current route point")) return false;
|
||||
}
|
||||
else m_QueueBounds.Insert(stateKey, queued.Count());
|
||||
PrintFormat("ERR PATROL QUEUE DIAGNOSTIC: site=%1 repeatedPairs=%2 deferred=%3 completions=%4", id, repeated, deferDuplicateFailure, m_Completions);
|
||||
if (!deferDuplicateFailure && !Check(repeated == 0, id + " no duplicate queue references")) return false;
|
||||
return true;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Finished) return 1;
|
||||
if (!m_Started)
|
||||
{
|
||||
if (!m_Prepared)
|
||||
{
|
||||
if (!DefinitionChecks() || !PreparedRoute("everon-supply-2", true) || !PreparedRoute("everon-outpost-1", true)) return -1;
|
||||
ERR_SiteRuntime prison = m_Mode.Sites().Find("everon-prison-1");
|
||||
if (!Check(prison && prison.Group && !prison.PatrolCycle && prison.PatrolWaypoints.IsEmpty(), "prison guards retain stationary orders")) return -1;
|
||||
m_Site = m_Mode.Sites().Find(m_SiteId);
|
||||
if (!Check(m_Site && m_Site.Group && m_Site.InitialGuardCount == 6, "selected native guard profile requests six physical members")) return -1;
|
||||
m_AcquireDeadline = now + 15000;
|
||||
m_Prepared = true;
|
||||
}
|
||||
if (!m_Site || !m_Site.Group) { Check(false, "original guard group disappeared before observation"); return -1; }
|
||||
array<AIAgent> agents = {};
|
||||
m_Site.Group.GetAgents(agents);
|
||||
array<IEntity> ready = {};
|
||||
foreach (AIAgent agent : agents)
|
||||
{
|
||||
if (!agent) continue;
|
||||
IEntity actor = agent.GetControlledEntity();
|
||||
if (ERR_Actors.CanFight(actor) && !ready.Contains(actor)) ready.Insert(actor);
|
||||
}
|
||||
if (ready.Count() > m_Site.InitialGuardCount) { Check(false, "original group contains excess living physical guards"); return -1; }
|
||||
if (ready.Count() < m_Site.InitialGuardCount)
|
||||
{
|
||||
if (now < m_AcquireDeadline) return 0;
|
||||
PrintFormat("ERR PATROL ACTOR TIMEOUT: site=%1 agents=%2 requested=%3 nativeComplete=%4", m_SiteId, agents.Count(), m_Site.InitialGuardCount, m_Site.Group.IsExpandComplete());
|
||||
Check(false, "existing living physical guard unavailable after bounded readiness wait");
|
||||
return -1;
|
||||
}
|
||||
m_Actor = ready[0];
|
||||
if (!Check(ready.Count() == 6, "six distinct original living physical guards available")) return -1;
|
||||
m_Origin = m_Actor.GetOrigin();
|
||||
m_Site.Group.GetOnWaypointCompleted().Insert(Completed);
|
||||
m_Deadline = now + 180000;
|
||||
m_Started = true;
|
||||
return 0;
|
||||
}
|
||||
if (m_Actor && vector.DistanceXZ(m_Origin, m_Actor.GetOrigin()) >= 8) m_Moved = true;
|
||||
if (m_BadSequence) { Stop(); Check(false, "native route completion order"); return -1; }
|
||||
if (m_Completions != m_LastQueueSample)
|
||||
{
|
||||
m_LastQueueSample = m_Completions;
|
||||
if (!PreparedRoute(m_SiteId, true)) { Stop(); return -1; }
|
||||
}
|
||||
// Five completions prove traversal across a cycle boundary, from any starting point.
|
||||
if (m_Completions >= 5 && m_Moved)
|
||||
{
|
||||
Stop();
|
||||
if (!Check(true, "existing physical guard moved at least 8m and five native moves completed in authored cyclic order")) return -1;
|
||||
if (!PreparedRoute(m_SiteId)) return -1;
|
||||
m_Finished = true;
|
||||
return 1;
|
||||
}
|
||||
if (now >= m_Deadline)
|
||||
{
|
||||
PrintFormat("ERR PATROL TIMEOUT: completions=%1 moved=%2 actor=%3 current=%4", m_Completions, m_Moved, m_Actor, m_Site.Group.GetCurrentWaypoint());
|
||||
Stop(); Check(false, "native route traversal completed within 180s"); return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// Run last in a disposable mission: deliberately stops both route owners.
|
||||
static bool StopChecks(ERR_GameMode mode)
|
||||
{
|
||||
ERR_SiteRuntime site = mode.Sites().Find("everon-outpost-1");
|
||||
if (!site || !site.Group || !site.PatrolCycle) return false;
|
||||
AIWaypoint search = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), ERR_Actors.Ground(site.Definition.Position + "40 0 0")));
|
||||
if (!search) return false;
|
||||
mode.Own(search);
|
||||
site.Group.AddWaypointAt(search, 0);
|
||||
mode.Sites().Stop();
|
||||
mode.Sites().Stop();
|
||||
array<AIWaypoint> queued = {};
|
||||
site.Group.GetWaypoints(queued);
|
||||
bool valid = !site.PatrolCycle && site.PatrolWaypoints.IsEmpty() && queued.Contains(search) && mode.Sites().Prepare() == ERR_PrepareResult.FAILED;
|
||||
site.Group.RemoveWaypoint(search);
|
||||
ERR_Actors.DeleteSpawned(search);
|
||||
if (valid) Print("ERR SMOKE PASS: patrol stop clears owned routes once and preserves foreign Search");
|
||||
else Print("ERR SMOKE FAIL: patrol stop ownership", LogLevel.ERROR);
|
||||
return valid;
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Site && m_Site.Group) m_Site.Group.GetOnWaypointCompleted().Remove(Completed);
|
||||
}
|
||||
void ~ERR_NativePatrolProbe() { Stop(); }
|
||||
}
|
||||
#endif
|
||||
+819
@@ -0,0 +1,819 @@
|
||||
#ifdef WORKBENCH
|
||||
// New native-input feasibility experiment. No original Escapists flight code.
|
||||
// Only the exact fixture-owned AI pilot and helicopter receive these hooks.
|
||||
modded class SCR_CharacterControllerComponent
|
||||
{
|
||||
override void OnPrepareControls(IEntity owner, ActionManager am, float dt, bool player)
|
||||
{
|
||||
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
|
||||
if (probe && !player) probe.PreparePilotActions(this, owner, am, dt);
|
||||
super.OnPrepareControls(owner, am, dt, player);
|
||||
if (probe && !player) probe.ObservePilotPreparation(this, owner);
|
||||
}
|
||||
protected override void OnApplyControls(IEntity owner, float timeSlice)
|
||||
{
|
||||
super.OnApplyControls(owner, timeSlice);
|
||||
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
|
||||
if (probe) probe.ApplyPilot(this, owner, timeSlice);
|
||||
}
|
||||
}
|
||||
modded class SCR_HelicopterControllerComponent
|
||||
{
|
||||
protected override event void OnPrepareControls()
|
||||
{
|
||||
super.OnPrepareControls();
|
||||
ERR_NativePoweredFlightProbe probe = ERR_NativePoweredFlightProbe.Current();
|
||||
if (probe) probe.PrepareHelicopter(this);
|
||||
}
|
||||
}
|
||||
class ERR_PoweredFlightRules
|
||||
{
|
||||
static const int PAD_CANDIDATES = 128;
|
||||
static const int PAD_REFINEMENTS = 80;
|
||||
static const int AIRPORT_CANDIDATES = 169;
|
||||
static vector AirportOffset(int index)
|
||||
{
|
||||
if (index < 0 || index >= AIRPORT_CANDIDATES) return vector.Zero;
|
||||
int x = Math.Mod(index, 13) - 6;
|
||||
int z = index / 13;
|
||||
z -= 6;
|
||||
return Vector(x * 50, 0, z * 50);
|
||||
}
|
||||
static vector RefineOffset(int index)
|
||||
{
|
||||
if (index < 0 || index >= PAD_REFINEMENTS) return vector.Zero;
|
||||
int sample = index;
|
||||
if (sample >= 40) sample++;
|
||||
int x = Math.Mod(sample, 9) - 4;
|
||||
int z = sample / 9;
|
||||
z -= 4;
|
||||
return Vector(x * 10, 0, z * 10);
|
||||
}
|
||||
static vector PadOffset(int index)
|
||||
{
|
||||
if (index < 0 || index >= PAD_CANDIDATES) return vector.Zero;
|
||||
float radius = 120;
|
||||
int segments = 12;
|
||||
int sample = index;
|
||||
float phase = 0;
|
||||
if (index >= 12 && index < 108)
|
||||
{
|
||||
int ring = (index - 12) / 24;
|
||||
radius = 200 + ring * 80;
|
||||
segments = 24; sample = Math.Mod(index - 12, 24); phase = Math.Mod(ring, 2) * 0.5;
|
||||
}
|
||||
else if (index >= 108)
|
||||
{
|
||||
radius = 500; segments = 20; sample = index - 108; phase = 0.25;
|
||||
}
|
||||
float angle = (sample + phase) * 2 * Math.PI / segments;
|
||||
return Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius);
|
||||
}
|
||||
static bool Authority(bool server, bool enabled, bool stopped, bool ownedPilot, bool ownedSeat, bool playerPresent, bool alive, bool fueled)
|
||||
{
|
||||
return server && enabled && !stopped && ownedPilot && ownedSeat && !playerPresent && alive && fueled;
|
||||
}
|
||||
static float ClampInput(float value, float limit = 1)
|
||||
{
|
||||
return Math.Clamp(value, -limit, limit);
|
||||
}
|
||||
static float NativeCollective(float signedDemand)
|
||||
{
|
||||
// Preserve the legacy Apply experiment unchanged. Later observations showed
|
||||
// overwritten positive inputs too, so this is not a proven native domain.
|
||||
return (ClampInput(signedDemand) + 1) * 0.5;
|
||||
}
|
||||
static float CollectiveCommand(float signedDemand, bool actions, int stage)
|
||||
{
|
||||
// Preserve pre-engine/startup zero and the old Apply comparison branch.
|
||||
if (!actions) return NativeCollective(signedDemand);
|
||||
if (stage == 9) return 0.05;
|
||||
if (stage == 10) return -0.05;
|
||||
if (stage == 7) return -1; // keep minimum pitch while the stopped engine's rotors coast
|
||||
if (stage >= 4 && stage <= 6) return ClampInput(signedDemand);
|
||||
return 0;
|
||||
}
|
||||
static bool NegativeCollectiveObserved(float command, float signal)
|
||||
{
|
||||
return command >= -0.06 && command <= -0.04 && Math.AbsFloat(signal - command) <= 0.01;
|
||||
}
|
||||
static bool CollectiveObserved(float command, float signal)
|
||||
{
|
||||
return command >= 0.04 && command <= 0.06 && Math.AbsFloat(signal - command) <= 0.01;
|
||||
}
|
||||
static float DesiredVertical(float height, bool landing)
|
||||
{
|
||||
float target = 8;
|
||||
if (landing) target = 0;
|
||||
return Math.Clamp((target - height) * 0.6, -0.8, 1.5);
|
||||
}
|
||||
static bool Hover(float height, float speed, float vertical, float upright, float drift)
|
||||
{
|
||||
return height >= 7 && height <= 9 && speed < 1.5 && Math.AbsFloat(vertical) < 0.5 && upright > 0.98 && drift < 8;
|
||||
}
|
||||
static bool Landed(bool contact, float height, float speed, float angular, float upright)
|
||||
{
|
||||
return contact && height < 1 && height > -1 && speed < 0.5 && angular < 0.1 && upright > 0.98;
|
||||
}
|
||||
static bool TranslationInputs(bool selected, bool actions, bool autohover)
|
||||
{
|
||||
return !selected || (actions && autohover);
|
||||
}
|
||||
static bool TranslationActive(bool selected, bool frozen, bool actions, bool autohover, int stage)
|
||||
{
|
||||
return selected && frozen && actions && autohover && stage == 5;
|
||||
}
|
||||
static bool TranslationTarget(vector home, vector start, vector fallback, out vector target)
|
||||
{
|
||||
target = vector.Zero;
|
||||
if (!(vector.DistanceXZ(home, start) < 8)) return false;
|
||||
vector direction = home - start; direction[1] = 0;
|
||||
if (direction.Length() < 0.5) { direction = fallback; direction[1] = 0; }
|
||||
float length = direction.Length();
|
||||
if (!(length >= 0.5)) return false;
|
||||
target = start + direction * (4 / length);
|
||||
return vector.DistanceXZ(start, target) >= 3.99 && vector.DistanceXZ(start, target) <= 4.01 && vector.DistanceXZ(home, target) <= 5;
|
||||
}
|
||||
static vector TranslationDemand(vector target, vector position, vector velocity)
|
||||
{
|
||||
vector error = target - position; error[1] = 0;
|
||||
vector desired = error * 0.25;
|
||||
float speed = desired.Length();
|
||||
if (speed > 0.5) desired = desired * (0.5 / speed);
|
||||
velocity[1] = 0;
|
||||
// Experimental outer velocity request only; native autohover stabilizes attitude.
|
||||
vector demand = (desired - velocity) * 0.05;
|
||||
float magnitude = demand.Length();
|
||||
if (magnitude > 0.03) demand = demand * (0.03 / magnitude);
|
||||
return demand;
|
||||
}
|
||||
static bool TranslationHeld(bool contact, float height, float speed, float vertical, float upright, float drift, float horizontalSpeed, float targetError, float displacement)
|
||||
{
|
||||
return !contact && Hover(height, speed, vertical, upright, drift) && horizontalSpeed >= 0 && horizontalSpeed < 0.25 && targetError >= 0 && targetError <= 0.75 && displacement >= 3.25;
|
||||
}
|
||||
}
|
||||
class ERR_NativePoweredFlightProbe
|
||||
{
|
||||
protected static ERR_NativePoweredFlightProbe s_Current;
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected Vehicle m_Aircraft;
|
||||
protected SCR_AIGroup m_Group;
|
||||
protected IEntity m_Pilot;
|
||||
protected HelicopterControllerComponent m_Controller;
|
||||
protected CharacterControllerComponent m_Character;
|
||||
protected CharacterInputContext m_Input;
|
||||
protected VehicleHelicopterSimulation m_Simulation;
|
||||
protected FuelManagerComponent m_Fuel;
|
||||
protected PilotCompartmentSlot m_Seat;
|
||||
protected CollectiveMode m_OriginalMode;
|
||||
protected vector m_Home;
|
||||
protected vector m_TestPad;
|
||||
protected float m_Heading;
|
||||
protected float m_Deadline;
|
||||
protected float m_StageDeadline;
|
||||
protected float m_Stable = -1;
|
||||
protected float m_PreviousTick = -1;
|
||||
protected float m_NextLog;
|
||||
protected float m_InitialFuel;
|
||||
protected float m_Collective = -1;
|
||||
protected float m_LastControl;
|
||||
protected int m_Stage;
|
||||
protected int m_Wakes;
|
||||
protected int m_StartRequests;
|
||||
protected int m_ControlUpdates;
|
||||
protected int m_PilotPreparations;
|
||||
protected int m_PositiveGateUpdates;
|
||||
protected float m_PositiveSignalSince = -1;
|
||||
protected int m_NegativeGateUpdates;
|
||||
protected float m_NegativeSignalSince = -1;
|
||||
protected bool m_SignedCollectiveProven;
|
||||
protected int m_HelicopterUpdates;
|
||||
protected bool m_PilotSpawnRequested;
|
||||
protected AIAgent m_PilotAgent;
|
||||
protected int m_OriginalPilotLOD;
|
||||
protected bool m_PilotLODSaved;
|
||||
protected bool m_PilotSeatsLogged;
|
||||
protected float m_NextInitializationLog;
|
||||
protected float m_NextSeatingLog;
|
||||
protected bool m_Bound;
|
||||
protected bool m_ModeSaved;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_Complete;
|
||||
protected bool m_ReadbackOK = true;
|
||||
protected bool m_ActionInputs;
|
||||
protected bool m_Started;
|
||||
protected bool m_AutohoverMode;
|
||||
protected bool m_TranslationMode;
|
||||
protected bool m_TranslationFrozen;
|
||||
protected bool m_TranslationProven;
|
||||
protected vector m_TranslationOrigin;
|
||||
protected vector m_TranslationTarget;
|
||||
protected int m_TranslationUpdates;
|
||||
protected bool m_AutohoverSaved;
|
||||
protected bool m_OriginalAutohover;
|
||||
protected HelicopterControllerComponent m_AutohoverController;
|
||||
protected ActionManager m_OwnedActions;
|
||||
protected int m_ActionContextSamples;
|
||||
static const ResourceName AIRCRAFT = "{DF5CCB7C0FF049F4}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_unarmed_transport.et";
|
||||
static const ResourceName GROUP = "{000CD338713F2B5A}Prefabs/AI/Groups/Group_Base.et";
|
||||
static const ResourceName PILOT = "{86E565387A634103}Prefabs/Characters/Campaign/Final/OPFOR/USSR_Army/Regular/Campaign_USSR_Player_MortarCrew.et";
|
||||
static bool Enabled()
|
||||
{
|
||||
string enabled, outcome;
|
||||
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1" && System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "powered-flight" || outcome == "powered-translate");
|
||||
}
|
||||
static ERR_NativePoweredFlightProbe Current() { return s_Current; }
|
||||
void ERR_NativePoweredFlightProbe(ERR_GameMode mode)
|
||||
{
|
||||
string actionInputs;
|
||||
m_ActionInputs = System.GetCLIParam("errFlightActionInputs", actionInputs) && actionInputs == "1";
|
||||
string outcome;
|
||||
m_TranslationMode = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "powered-translate";
|
||||
m_Mode = mode; m_Deadline = GetGame().GetWorld().GetWorldTime() + 240000;
|
||||
if (Enabled() && !s_Current) s_Current = this;
|
||||
else m_Stopped = true;
|
||||
}
|
||||
// Fixture configuration only: caller must select this before the first Tick.
|
||||
bool SetAutohoverMode(bool enabled)
|
||||
{
|
||||
if (m_Started || m_Stopped || m_Complete || m_Bound || m_Aircraft) return false;
|
||||
m_AutohoverMode = enabled;
|
||||
return true;
|
||||
}
|
||||
protected bool BeginAutohover()
|
||||
{
|
||||
if (!m_AutohoverMode) return true;
|
||||
if (m_AutohoverSaved || !m_Bound || !PoweredAuthority() || !m_Controller || m_Controller.GetOwner() != m_Aircraft) return false;
|
||||
m_AutohoverController = m_Controller;
|
||||
m_OriginalAutohover = m_Controller.GetAutohoverEnabled();
|
||||
m_AutohoverSaved = true;
|
||||
m_Controller.SetAutohoverEnabled(true);
|
||||
return m_Controller.GetAutohoverEnabled();
|
||||
}
|
||||
protected bool AutohoverReady()
|
||||
{
|
||||
if (!m_AutohoverMode) return true;
|
||||
// Native autohover also rewrites collective. Prove direct input consumption
|
||||
// first, then enable its controller only for the bounded flight phase.
|
||||
if (!m_AutohoverSaved && (m_Stage == 2 || m_Stage == 3 || m_Stage == 9 || m_Stage == 10)) return true;
|
||||
return m_AutohoverSaved && m_AutohoverController && m_AutohoverController == m_Controller && m_Controller.GetOwner() == m_Aircraft && m_Controller.GetAutohoverEnabled();
|
||||
}
|
||||
protected void RestoreAutohover(bool ownedSeat)
|
||||
{
|
||||
if (ownedSeat && m_AutohoverSaved && m_AutohoverController && m_AutohoverController == m_Controller && m_Controller.GetOwner() == m_Aircraft)
|
||||
{
|
||||
m_Controller.SetAutohoverEnabled(m_OriginalAutohover);
|
||||
PrintFormat("ERR POWERED AUTOHOVER RESTORE: original=%1 observed=%2", m_OriginalAutohover, m_Controller.GetAutohoverEnabled());
|
||||
if (m_Controller.GetAutohoverEnabled() != m_OriginalAutohover) Print("ERR SMOKE FAIL: powered-flight original owned autohover state restored", LogLevel.ERROR);
|
||||
}
|
||||
m_AutohoverSaved = false;
|
||||
m_AutohoverController = null;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: powered-flight " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: powered-flight " + detail, LogLevel.ERROR); Stop(); return false;
|
||||
}
|
||||
protected bool PlayerPresent()
|
||||
{
|
||||
if (!m_Aircraft || !m_Group || m_Group.GetPlayerCount(true) > 0) return true;
|
||||
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||
foreach (int id : ids)
|
||||
{
|
||||
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||
if (actor && (actor == m_Pilot || CompartmentAccessComponent.GetVehicleIn(actor) == m_Aircraft)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
protected bool OwnPilot()
|
||||
{
|
||||
return m_Pilot && m_Group && ERR_Actors.Group(m_Pilot) == m_Group && !ERR_Actors.Player(m_Pilot);
|
||||
}
|
||||
protected bool PoweredAuthority()
|
||||
{
|
||||
bool occupied = m_Seat && m_Seat.GetOccupant() == m_Pilot && m_Controller && m_Controller.GetPilotCompartmentSlot() == m_Seat && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
bool alive = ERR_Actors.Alive(m_Aircraft) && ERR_Actors.CanFight(m_Pilot);
|
||||
SCR_VehicleDamageManagerComponent damage;
|
||||
if (m_Aircraft) damage = SCR_VehicleDamageManagerComponent.Cast(m_Aircraft.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||
alive = alive && damage && damage.GetEngineFunctional() && damage.GetMovementDamage() < 1 && !damage.IsOnFire();
|
||||
if (m_Simulation)
|
||||
for (int rotor = 0; rotor < m_Simulation.RotorCount(); rotor++) if (m_Simulation.RotorGetState(rotor) == RotorState.DESTROYED) alive = false;
|
||||
return ERR_PoweredFlightRules.Authority(Replication.IsServer(), Enabled(), m_Stopped, OwnPilot(), occupied, PlayerPresent(), alive, m_Fuel && m_Fuel.HasFuel());
|
||||
}
|
||||
void PrepareHelicopter(HelicopterControllerComponent controller)
|
||||
{
|
||||
if (!m_Bound || controller != m_Controller || !PoweredAuthority()) return;
|
||||
// Native SCR controller normally reapplies device preferences every update.
|
||||
// Override only this owned fixture controller, never global/user settings.
|
||||
controller.SetCollectiveMode(CollectiveMode.Direct); m_HelicopterUpdates++;
|
||||
}
|
||||
bool UsesActionInputs() { return m_ActionInputs; }
|
||||
void ObservePilotPreparation(CharacterControllerComponent controller, IEntity owner)
|
||||
{
|
||||
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority()) return;
|
||||
m_PilotPreparations++;
|
||||
}
|
||||
// Both selectable input paths share one computation. Only the selected
|
||||
// path integrates per callback; neither path invokes the other writer.
|
||||
protected bool ComputePilotDemand(CharacterControllerComponent controller, IEntity owner, float dt, out float collective, out vector axes)
|
||||
{
|
||||
if (!m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority() || !AutohoverReady() || !m_Input || !m_Simulation || dt <= 0 || dt > 0.2) return false;
|
||||
if (m_ActionInputs && m_Stage >= 4 && m_Stage <= 6 && !m_SignedCollectiveProven) return false;
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
vector velocity = m_Simulation.GetVelocity();
|
||||
vector angular = m_Simulation.GetAngularVelocityLocal();
|
||||
float height = transform[3][1] - m_Home[1];
|
||||
float forward, aside, torque;
|
||||
collective = -1;
|
||||
if (m_Stage == 9) collective = -0.9; // native0.05, bounded signal-consumption proof
|
||||
if (m_Stage >= 4 && m_Stage <= 6 && m_Controller.IsEngineOn())
|
||||
{
|
||||
float desiredVertical = ERR_PoweredFlightRules.DesiredVertical(height, m_Stage == 6);
|
||||
float error = desiredVertical - velocity[1];
|
||||
m_Collective = Math.Clamp(m_Collective + error * dt * 0.12, -0.95, 0.8);
|
||||
collective = Math.Clamp(m_Collective + error * 0.12, -1, 0.9);
|
||||
vector offset = m_Home - transform[3]; offset[1] = 0;
|
||||
// Native105226 already brakes before8m refusal; increase only velocity damping.
|
||||
vector acceleration = offset * 0.08 - velocity * 0.6; acceleration[1] = 0;
|
||||
// Small local cyclic corrections counter drift/tilt through native inputs.
|
||||
// Sign/gain and native Direct-mode response remain experimental evidence.
|
||||
forward = ERR_PoweredFlightRules.ClampInput((vector.Dot(acceleration, transform[2]) / 9.81 + transform[2][1]) * 0.8 - angular[0] * 1.2, 0.15);
|
||||
aside = ERR_PoweredFlightRules.ClampInput((vector.Dot(acceleration, transform[0]) / 9.81 + transform[0][1]) * 0.8 + angular[2] * 1.2, 0.15);
|
||||
// Native autohover also adjusts collective; observe its physical response.
|
||||
// Give its lateral controller zero custom cyclic in this experiment.
|
||||
// Preserve collective/yaw and bounds; never combine two cyclic controllers.
|
||||
if (m_AutohoverMode) { forward = 0; aside = 0; }
|
||||
if (ERR_PoweredFlightRules.TranslationActive(m_TranslationMode, m_TranslationFrozen, m_ActionInputs, m_AutohoverMode, m_Stage))
|
||||
{
|
||||
vector demand = ERR_PoweredFlightRules.TranslationDemand(m_TranslationTarget, transform[3], velocity);
|
||||
forward = ERR_PoweredFlightRules.ClampInput(vector.Dot(demand, transform[2]), 0.03);
|
||||
aside = ERR_PoweredFlightRules.ClampInput(vector.Dot(demand, transform[0]), 0.03);
|
||||
m_TranslationUpdates++;
|
||||
}
|
||||
float headingError = m_Heading - m_Aircraft.GetYawPitchRoll()[0];
|
||||
while (headingError > 180) headingError -= 360;
|
||||
while (headingError < -180) headingError += 360;
|
||||
torque = ERR_PoweredFlightRules.ClampInput(headingError * 0.01 - angular[1] * 0.3, 0.3);
|
||||
if (m_Stage == 6 && ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, velocity.Length(), angular.Length(), transform[1][1])) collective = -1;
|
||||
}
|
||||
collective = ERR_PoweredFlightRules.CollectiveCommand(collective, m_ActionInputs, m_Stage);
|
||||
axes = Vector(forward, aside, torque);
|
||||
return true;
|
||||
}
|
||||
protected bool WriteActionPair(ActionManager am, string positive, string negative, float demand)
|
||||
{
|
||||
float a = Math.Clamp(demand, 0, 1);
|
||||
float b = Math.Clamp(-demand, 0, 1);
|
||||
am.SetActionValue(positive, a); am.SetActionValue(negative, b);
|
||||
return Math.AbsFloat(am.GetActionValue(positive) - a) < 0.001 && Math.AbsFloat(am.GetActionValue(negative) - b) < 0.001;
|
||||
}
|
||||
protected bool OwnActionManager(ActionManager am)
|
||||
{
|
||||
// Read the global manager only to reject shared ownership. Never write it.
|
||||
return am && m_PilotAgent && am == m_PilotAgent.GetActionManager() && am != GetGame().GetInputManager();
|
||||
}
|
||||
void PreparePilotActions(CharacterControllerComponent controller, IEntity owner, ActionManager am, float dt)
|
||||
{
|
||||
if (!m_ActionInputs || !m_Bound || controller != m_Character || owner != m_Pilot || !PoweredAuthority()) return;
|
||||
if (!OwnActionManager(am)) { m_ReadbackOK = false; return; }
|
||||
m_OwnedActions = am;
|
||||
// Same short activation pattern used by vanilla game.c; this manager belongs
|
||||
// solely to the fixture AI, never to global hardware or the human player.
|
||||
// Native104256 reports false from ActivateContext while already active.
|
||||
// Treat observed active state as authority, not an undocumented return meaning.
|
||||
bool alreadyActive = am.IsContextActive("HelicopterContext");
|
||||
bool activated;
|
||||
if (!alreadyActive) activated = am.ActivateContext("HelicopterContext", 1);
|
||||
bool contextActive = am.IsContextActive("HelicopterContext");
|
||||
if (m_ActionContextSamples < 4)
|
||||
{
|
||||
m_ActionContextSamples++;
|
||||
PrintFormat("ERR POWERED ACTION CONTEXT: sample=%1 activated=%2 active=%3 dt=%4 input=%5 simulation=%6", m_ActionContextSamples, activated, contextActive, dt, m_Input != null, m_Simulation != null);
|
||||
}
|
||||
if (!contextActive) { m_ReadbackOK = false; return; }
|
||||
float collective; vector axes;
|
||||
if (!ComputePilotDemand(controller, owner, dt, collective, axes)) return;
|
||||
bool a = WriteActionPair(am, "HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", collective);
|
||||
bool b = WriteActionPair(am, "HelicopterCyclicForward", "HelicopterCyclicBack", axes[0]);
|
||||
bool c = WriteActionPair(am, "HelicopterCyclicRight", "HelicopterCyclicLeft", axes[1]);
|
||||
bool d = WriteActionPair(am, "HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", axes[2]);
|
||||
m_ReadbackOK = m_ReadbackOK && a && b && c && d;
|
||||
m_ControlUpdates++; m_LastControl = GetGame().GetWorld().GetWorldTime();
|
||||
}
|
||||
void ApplyPilot(CharacterControllerComponent controller, IEntity owner, float dt)
|
||||
{
|
||||
// The experimental actor-action path and old context path never both write.
|
||||
if (m_ActionInputs) return;
|
||||
float collective; vector axes;
|
||||
if (!ComputePilotDemand(controller, owner, dt, collective, axes)) return;
|
||||
float forward = axes[0]; float aside = axes[1]; float torque = axes[2];
|
||||
m_Input.SetVehicleThrottle(1);
|
||||
m_Input.SetVehicleCollective(collective);
|
||||
m_Input.SetVehicleCyclicForward(forward);
|
||||
m_Input.SetVehicleCyclicAside(aside);
|
||||
m_Input.SetVehicleAntiTorque(torque);
|
||||
m_ReadbackOK = m_ReadbackOK && Math.AbsFloat(m_Input.GetVehicleCollective() - collective) < 0.001 && Math.AbsFloat(m_Input.GetVehicleCyclicForward() - forward) < 0.001 && Math.AbsFloat(m_Input.GetVehicleCyclicAside() - aside) < 0.001 && Math.AbsFloat(m_Input.GetVehicleAntiTorque() - torque) < 0.001;
|
||||
m_ControlUpdates++; m_LastControl = GetGame().GetWorld().GetWorldTime();
|
||||
}
|
||||
protected bool FindPad(out vector result)
|
||||
{
|
||||
vector anchor = m_Mode.ExtractionVehicle().GetOrigin();
|
||||
int tested, lowGround, slopeRejected, cylinderRejected, sweepRejected;
|
||||
float bestSlope = float.MAX;
|
||||
vector bestCandidate;
|
||||
vector refinementAnchor;
|
||||
int localCandidates = ERR_PoweredFlightRules.PAD_CANDIDATES + ERR_PoweredFlightRules.PAD_REFINEMENTS;
|
||||
for (int index = 0; index < localCandidates + ERR_PoweredFlightRules.AIRPORT_CANDIDATES; index++)
|
||||
{
|
||||
if (index == ERR_PoweredFlightRules.PAD_CANDIDATES)
|
||||
{
|
||||
if (bestSlope == float.MAX) refinementAnchor = anchor;
|
||||
else refinementAnchor = bestCandidate;
|
||||
// Freeze the refinement centre; improvements must not move the grid.
|
||||
}
|
||||
tested++;
|
||||
vector candidate;
|
||||
if (index < ERR_PoweredFlightRules.PAD_CANDIDATES) candidate = ERR_Actors.Ground(anchor + ERR_PoweredFlightRules.PadOffset(index));
|
||||
else if (index < localCandidates) candidate = ERR_Actors.Ground(refinementAnchor + ERR_PoweredFlightRules.RefineOffset(index - ERR_PoweredFlightRules.PAD_CANDIDATES));
|
||||
// Installed vanilla Eden/Airport location centre; candidate only, never
|
||||
// accepted without exactly the same terrain and physical clearance queries.
|
||||
else candidate = ERR_Actors.Ground("4900.953 0 11787.412" + ERR_PoweredFlightRules.AirportOffset(index - localCandidates));
|
||||
if (candidate[1] < 2) { lowGround++; continue; }
|
||||
float maximumDelta = 0;
|
||||
for (int edge = 0; edge < 4; edge++)
|
||||
{
|
||||
float side = edge * Math.PI / 2;
|
||||
vector point = candidate + Vector(Math.Cos(side) * 14, 0, Math.Sin(side) * 14);
|
||||
maximumDelta = Math.Max(maximumDelta, Math.AbsFloat(SCR_TerrainHelper.GetTerrainY(point) - candidate[1]));
|
||||
}
|
||||
if (maximumDelta < bestSlope) { bestSlope = maximumDelta; bestCandidate = candidate; }
|
||||
if (maximumDelta > 0.8) { slopeRejected++; continue; }
|
||||
// Retain slope and exact native rejection semantics; widen the tested
|
||||
// column for a runtime-measured whole-aircraft footprint plus8m drift.
|
||||
if (!SCR_WorldTools.TraceCilinderUtil(candidate + "0 2.5 0", 40, 4, TraceFlags.ENTS)) { cylinderRejected++; continue; }
|
||||
TraceBox clearance = new TraceBox(); clearance.Start = candidate + "0 3 0"; clearance.End = candidate + "0 60 0";
|
||||
clearance.Mins = "-40 -1 -40"; clearance.Maxs = "40 1 40"; clearance.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||
if (GetGame().GetWorld().TraceMove(clearance, null) < 1) { sweepRejected++; continue; }
|
||||
PrintFormat("ERR POWERED PAD: found=%1 tested=%2 distance=%3m maximumHeightDelta=%4m lowGround=%5 slopeRejected=%6 cylinderRejected=%7 sweepRejected=%8", candidate, tested, vector.DistanceXZ(anchor, candidate), maximumDelta, lowGround, slopeRejected, cylinderRejected, sweepRejected);
|
||||
result = candidate; return true;
|
||||
}
|
||||
if (bestSlope == float.MAX) bestSlope = -1;
|
||||
PrintFormat("ERR POWERED PAD: no safe candidate; tested=%1 localRadius=557m plus airport300m-grid lowGround=%2 slopeRejected=%3 cylinderRejected=%4 sweepRejected=%5 bestHeightDelta=%6m bestCandidate=%7 (diagnostic only; never a fallback)", tested, lowGround, slopeRejected, cylinderRejected, sweepRejected, bestSlope, bestCandidate);
|
||||
return false;
|
||||
}
|
||||
protected void Diagnostic(float now)
|
||||
{
|
||||
if (!m_Simulation || !m_Aircraft || now < m_NextLog) return;
|
||||
m_NextLog = now + 2000;
|
||||
if (m_Stage == 4 && now < m_StageDeadline - 57000) m_NextLog = now + 250;
|
||||
if (m_TranslationMode && m_Stage == 5) m_NextLog = now + 500;
|
||||
if (m_ActionInputs)
|
||||
{
|
||||
bool ownedActions = OwnActionManager(m_OwnedActions);
|
||||
bool contextActive = m_OwnedActions && m_OwnedActions.IsContextActive("HelicopterContext");
|
||||
PrintFormat("ERR POWERED ACTION MANAGER: selected=1 exactOwnedNonGlobal=%1 contextActive=%2 virtualWrites=%3", ownedActions, contextActive, m_ControlUpdates);
|
||||
}
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
PrintFormat("ERR POWERED AUTOHOVER: selected=%1 saved=%2 enabled=%3 exactController=%4", m_AutohoverMode, m_AutohoverSaved, m_Controller.GetAutohoverEnabled(), m_AutohoverController == m_Controller);
|
||||
PrintFormat("ERR POWERED FLIGHT: stage=%1 height=%2 drift=%3 speed=%4 vertical=%5 up=%6 groundContact=%7 fuel=%8 engine=%9", m_Stage, transform[3][1] - m_Home[1], vector.DistanceXZ(transform[3], m_Home), m_Simulation.GetVelocity().Length(), m_Simulation.GetVelocity()[1], transform[1][1], m_Simulation.HasAnyGroundContact(), m_Fuel.GetTotalFuel(), m_Controller.IsEngineOn());
|
||||
vector rotorAxis; m_Simulation.RotorGetAxis(0, rotorAxis);
|
||||
PrintFormat("ERR POWERED HEADING: desiredYaw=%1 actualYawPitchRoll=%2", m_Heading, m_Aircraft.GetYawPitchRoll());
|
||||
PrintFormat("ERR POWERED ROTOR AXIS: local=%1 acceleration=%2", rotorAxis, m_Simulation.GetAcceleration());
|
||||
PrintFormat("ERR POWERED MOTION: offset=%1 velocity=%2 angularLocal=%3 forward=%4 right=%5", m_Home - transform[3], m_Simulation.GetVelocity(), m_Simulation.GetAngularVelocityLocal(), transform[2], transform[0]);
|
||||
PrintFormat("ERR POWERED INPUT: pilotHooks=%1 vehicleHooks=%2 readback=%3 collective=%4 cyclicF=%5 cyclicR=%6 antiTorque=%7 rotorRPM=%8 targetRPM=%9", m_ControlUpdates, m_HelicopterUpdates, m_ReadbackOK, m_Input.GetVehicleCollective(), m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), m_Input.GetVehicleAntiTorque(), m_Simulation.RotorGetRPM(0), m_Simulation.RotorGetRPMTarget(0));
|
||||
if (m_TranslationMode && m_TranslationFrozen)
|
||||
{
|
||||
vector direction = m_TranslationTarget - m_TranslationOrigin; direction[1] = 0; direction = direction * 0.25;
|
||||
vector travel = transform[3] - m_TranslationOrigin; travel[1] = 0;
|
||||
float along = vector.Dot(travel, direction);
|
||||
vector cross = travel - direction * along;
|
||||
vector horizontal = m_Simulation.GetVelocity(); horizontal[1] = 0;
|
||||
PrintFormat("ERR POWERED TRANSLATION: stage=%1 origin=%2 target=%3 position=%4 along=%5 cross=%6 error=%7 horizontalSpeed=%8 updates=%9", m_Stage, m_TranslationOrigin, m_TranslationTarget, transform[3], along, cross.Length(), vector.DistanceXZ(transform[3], m_TranslationTarget), horizontal.Length(), m_TranslationUpdates);
|
||||
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
||||
if (signals)
|
||||
{
|
||||
int cyclicForward = signals.FindSignal("MainRotorCyclicForward");
|
||||
int cyclicAside = signals.FindSignal("MainRotorCyclicAside");
|
||||
if (cyclicForward >= 0 && cyclicAside >= 0) PrintFormat("ERR POWERED TRANSLATION SIGNALS: pilotForward=%1 pilotAside=%2 rotorForward=%3 rotorAside=%4 (native autohover may modify rotor commands)", m_Input.GetVehicleCyclicForward(), m_Input.GetVehicleCyclicAside(), signals.GetSignalValue(cyclicForward), signals.GetSignalValue(cyclicAside));
|
||||
}
|
||||
}
|
||||
}
|
||||
protected bool PollNativeFlightComponents(float now)
|
||||
{
|
||||
// Component pointers, simulation and tank contents may initialize after spawn.
|
||||
// Enumerate the real empty primary cockpit slot; an active pilot assignment
|
||||
// is a later occupancy requirement, not a prerequisite to creating its pilot.
|
||||
m_Controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||
m_Simulation = null;
|
||||
if (m_Controller) m_Simulation = m_Controller.GetSimulation();
|
||||
m_Fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Aircraft.FindComponent(BaseCompartmentManagerComponent));
|
||||
array<BaseCompartmentSlot> slots = {};
|
||||
if (manager) manager.GetCompartments(slots);
|
||||
m_Seat = null;
|
||||
if (slots.Count() <= 64)
|
||||
{
|
||||
foreach (BaseCompartmentSlot slot : slots)
|
||||
{
|
||||
PilotCompartmentSlot pilotSlot = PilotCompartmentSlot.Cast(slot);
|
||||
if (!pilotSlot || pilotSlot.GetOwner() != m_Aircraft || pilotSlot.GetVehicle() != m_Aircraft) continue;
|
||||
if (!m_PilotSeatsLogged) PrintFormat("ERR POWERED INITIAL SEAT: name=%1 unique=%2 type=%3 occupied=%4 accessible=%5 piloting=%6", pilotSlot.GetCompartmentName(), pilotSlot.GetCompartmentUniqueName(), pilotSlot.GetType(), pilotSlot.GetOccupant() != null, pilotSlot.IsCompartmentAccessible(), pilotSlot.IsPiloting());
|
||||
// This is the actual object name in the audited stock Mi8 prefab. Do not
|
||||
// choose the copilot merely because it has the same PilotCompartment type.
|
||||
if (pilotSlot.GetCompartmentName() != "PilotCompartment") continue;
|
||||
if (pilotSlot.GetType() != ECompartmentType.PILOT || !pilotSlot.IsCompartmentAccessible()) continue;
|
||||
if (pilotSlot.GetOccupant() || pilotSlot.IsReserved() || pilotSlot.IsGetInLocked()) continue;
|
||||
m_Seat = pilotSlot;
|
||||
}
|
||||
}
|
||||
if (slots.Count() > 0) m_PilotSeatsLogged = true;
|
||||
bool validSimulation = m_Simulation && m_Simulation.IsValid();
|
||||
bool fueled = m_Fuel && m_Fuel.HasFuel();
|
||||
PilotCompartmentSlot active;
|
||||
if (m_Controller) active = m_Controller.GetPilotCompartmentSlot();
|
||||
if (now >= m_NextInitializationLog || now >= m_StageDeadline)
|
||||
{
|
||||
m_NextInitializationLog = now + 2000;
|
||||
float fuel = -1;
|
||||
if (m_Fuel) fuel = m_Fuel.GetTotalFuel();
|
||||
PrintFormat("ERR POWERED INITIALIZATION: controller=%1 simulation=%2 valid=%3 manager=%4 slots=%5 primaryEmptySeat=%6 activeSeat=%7 fuelManager=%8", m_Controller != null, m_Simulation != null, validSimulation, manager != null, slots.Count(), m_Seat != null, active != null, m_Fuel != null);
|
||||
PrintFormat("ERR POWERED INITIALIZATION STOCK: fueled=%1 totalFuel=%2 agents=%3 pilotSpawnRequested=%4", fueled, fuel, m_Group.GetAgentsCount(), m_PilotSpawnRequested);
|
||||
}
|
||||
return m_Controller && validSimulation && m_Seat && fueled;
|
||||
}
|
||||
protected void SeatingDiagnostic(float now)
|
||||
{
|
||||
// Observe each existing stage-1 predicate without changing seating, AI,
|
||||
// physics, health, fuel, or the shared preparation deadline.
|
||||
if (now < m_NextSeatingLog && now < m_StageDeadline) return;
|
||||
m_NextSeatingLog = now + 2000;
|
||||
bool ownPilot = OwnPilot();
|
||||
bool seatMatches = m_Seat.GetOccupant() == m_Pilot;
|
||||
bool activeMatches = m_Controller.GetPilotCompartmentSlot() == m_Seat;
|
||||
bool vehicleMatches = m_Pilot && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
bool aircraftReady = ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft);
|
||||
bool pilotAlive = ERR_Actors.Alive(m_Pilot);
|
||||
bool pilotConscious = ERR_Actors.CanFight(m_Pilot);
|
||||
bool aircraftAlive = ERR_Actors.Alive(m_Aircraft);
|
||||
bool sameGroup = ERR_Actors.Group(m_Pilot) == m_Group;
|
||||
bool player = ERR_Actors.Player(m_Pilot);
|
||||
PrintFormat("ERR POWERED SEATING: ownPilot=%1 seatOccupantMatches=%2 activeSlotMatches=%3 vehicleMatches=%4 aircraftReady=%5 wakes=%6 stableSince=%7 remainingMs=%8", ownPilot, seatMatches, activeMatches, vehicleMatches, aircraftReady, m_Wakes, m_Stable, m_StageDeadline - now);
|
||||
PrintFormat("ERR POWERED SEATING ACTORS: pilotExists=%1 alive=%2 conscious=%3 sameGroup=%4 player=%5 groupAgents=%6 aircraftAlive=%7", m_Pilot != null, pilotAlive, pilotConscious, sameGroup, player, m_Group.GetAgentsCount(), aircraftAlive);
|
||||
PrintFormat("ERR POWERED SEATING IDENTITIES: pilot=%1 occupant=%2 selectedSeat=%3 activeSeat=%4 group=%5 actualGroup=%6", m_Pilot, m_Seat.GetOccupant(), m_Seat, m_Controller.GetPilotCompartmentSlot(), m_Group, ERR_Actors.Group(m_Pilot));
|
||||
if (m_PilotAgent) PrintFormat("ERR POWERED SEATING AGENT: actualLOD=%1 forcedLOD=%2 activated=%3", m_PilotAgent.GetLOD(), m_PilotAgent.GetPermanentLOD(), m_PilotAgent.IsAIActivated());
|
||||
CompartmentAccessComponent access;
|
||||
if (m_Pilot) access = CompartmentAccessComponent.Cast(m_Pilot.FindComponent(CompartmentAccessComponent));
|
||||
if (access) PrintFormat("ERR POWERED SEATING ACCESS: gettingIn=%1 gettingOut=%2", access.IsGettingIn(), access.IsGettingOut());
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
vector entityTransform[4]; m_Aircraft.GetWorldTransform(entityTransform);
|
||||
if (physics) PrintFormat("ERR POWERED SEATING PHYSICS: dynamic=%1 awake=%2 speed=%3 angularSpeed=%4 entityUp=%5 entityOrigin=%6", physics.IsDynamic(), physics.IsActive(), physics.GetVelocity().Length(), physics.GetAngularVelocity().Length(), entityTransform[1][1], entityTransform[3]);
|
||||
vector nativeTransform[4]; m_Simulation.GetWorldTransform(nativeTransform);
|
||||
bool valid = m_Simulation.IsValid();
|
||||
bool nativeContact = m_Simulation.HasAnyGroundContact();
|
||||
bool parkedSupport = ERR_CasualtyGroundSupport.Parked(m_Aircraft, now >= m_StageDeadline);
|
||||
PrintFormat("ERR POWERED SEATING SIMULATION: valid=%1 nativeContact=%2 parkedSupport=%3 engineOn=%4 nativeUp=%5 nativeOrigin=%6", valid, nativeContact, parkedSupport, m_Controller.IsEngineOn(), nativeTransform[1][1], nativeTransform[3]);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
// Input cleanup is confined to the same unclaimed test pilot/aircraft.
|
||||
bool ours = m_Bound && OwnPilot() && !PlayerPresent();
|
||||
bool ownSeat = ours && m_Seat && m_Seat.GetOccupant() == m_Pilot && m_Controller && m_Controller.GetPilotCompartmentSlot() == m_Seat && CompartmentAccessComponent.GetVehicleIn(m_Pilot) == m_Aircraft;
|
||||
RestoreAutohover(ownSeat);
|
||||
m_Stopped = true; m_Bound = false;
|
||||
if (s_Current == this) s_Current = null;
|
||||
if (ours && m_ActionInputs && OwnActionManager(m_OwnedActions))
|
||||
{
|
||||
WriteActionPair(m_OwnedActions, "HelicopterCollectiveIncrease", "HelicopterCollectiveDecrease", 0);
|
||||
WriteActionPair(m_OwnedActions, "HelicopterCyclicForward", "HelicopterCyclicBack", 0);
|
||||
WriteActionPair(m_OwnedActions, "HelicopterCyclicRight", "HelicopterCyclicLeft", 0);
|
||||
WriteActionPair(m_OwnedActions, "HelicopterAntiTorqueRight", "HelicopterAntiTorqueLeft", 0);
|
||||
}
|
||||
if (ours && !m_ActionInputs && m_Input)
|
||||
{
|
||||
m_Input.SetVehicleCollective(0); m_Input.SetVehicleThrottle(0);
|
||||
m_Input.SetVehicleCyclicForward(0); m_Input.SetVehicleCyclicAside(0); m_Input.SetVehicleAntiTorque(0);
|
||||
}
|
||||
if (ours && m_Controller && m_ModeSaved) m_Controller.SetCollectiveMode(m_OriginalMode);
|
||||
m_OwnedActions = null;
|
||||
if (m_PilotLODSaved && m_PilotAgent && OwnPilot() && !PlayerPresent()) m_PilotAgent.SetPermanentLOD(m_OriginalPilotLOD);
|
||||
if (m_Group && m_Group.GetPlayerCount(true) == 0) m_Group.SetPermanentLOD(-1);
|
||||
// Keep real airborne/failed test actors for diagnosis and mode-owned cleanup.
|
||||
// Stop never repairs, stabilizes, teleports, refuels or replaces them.
|
||||
}
|
||||
protected bool BeginTranslation(float now, vector position, vector fallback)
|
||||
{
|
||||
if (!Check(m_TranslationMode && !m_TranslationFrozen && m_Stage == 4 && m_ActionInputs && m_AutohoverMode && AutohoverReady(), "translation starts once after the original native hover stage")) return false;
|
||||
vector target;
|
||||
if (!Check(ERR_PoweredFlightRules.TranslationTarget(m_Home, position, fallback, target), "frozen four-metre horizontal target remains within five metres of original home")) return false;
|
||||
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
||||
if (!Check(signals && signals.FindSignal("MainRotorCyclicForward") >= 0 && signals.FindSignal("MainRotorCyclicAside") >= 0, "existing native cyclic signals available for translation observation")) return false;
|
||||
m_TranslationOrigin = position;
|
||||
m_TranslationTarget = target;
|
||||
m_TranslationFrozen = true;
|
||||
m_Stage = 5; m_StageDeadline = now + 45000; m_Stable = -1; m_NextLog = 0;
|
||||
PrintFormat("ERR POWERED TRANSLATION BEGIN: origin=%1 target=%2 originalHome=%3 deadline=%4 overallDeadline=%5", m_TranslationOrigin, m_TranslationTarget, m_Home, m_StageDeadline, m_Deadline);
|
||||
return true;
|
||||
}
|
||||
protected bool CheckTranslation(float now, vector position, float height, float speed, float upright, float drift)
|
||||
{
|
||||
if (!ERR_PoweredFlightRules.TranslationActive(m_TranslationMode, m_TranslationFrozen, m_ActionInputs, m_AutohoverMode, m_Stage)) return Check(false, "translation retains its selected inputs and frozen stage ownership");
|
||||
if (now >= m_StageDeadline) return Check(false, "four-metre translation and five-second target hold finish within 45 seconds");
|
||||
vector velocity = m_Simulation.GetVelocity();
|
||||
vector horizontal = velocity; horizontal[1] = 0;
|
||||
bool held = m_TranslationUpdates >= 3 && ERR_PoweredFlightRules.TranslationHeld(m_Simulation.HasAnyGroundContact(), height, speed, velocity[1], upright, drift, horizontal.Length(), vector.DistanceXZ(position, m_TranslationTarget), vector.DistanceXZ(position, m_TranslationOrigin));
|
||||
if (!held) m_Stable = -1;
|
||||
if (held && m_Stable < 0) m_Stable = now;
|
||||
if (m_Stable < 0 || now - m_Stable < 5000) return true;
|
||||
if (!Check(true, "actual horizontal translation reaches frozen target and holds five seconds at 7–9m with horizontal speed below 0.25m/s")) return false;
|
||||
m_TranslationProven = true;
|
||||
m_Stage = 6; m_StageDeadline = now + 65000; m_Stable = -1;
|
||||
return true;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Complete) return 1;
|
||||
if (m_Stopped) return -1;
|
||||
m_Started = true;
|
||||
if (!Enabled() || !m_Mode || now >= m_Deadline || ERR_MissionRules.Terminal(m_Mode.Phase())) { Check(false, "server fixture active within 240-second bound"); return -1; }
|
||||
if (!ERR_PoweredFlightRules.TranslationInputs(m_TranslationMode, m_ActionInputs, m_AutohoverMode)) { Check(false, "translation requires actor-local actions and selected native autohover before allocation"); return -1; }
|
||||
if (m_PreviousTick >= 0 && (now < m_PreviousTick || now - m_PreviousTick > 1500)) { m_Stable = -1; m_PositiveSignalSince = -1; m_NegativeSignalSince = -1; }
|
||||
m_PreviousTick = now;
|
||||
if (m_Stage == 0)
|
||||
{
|
||||
if (!m_Mode.ExtractionVehicle()) return 0;
|
||||
vector pad;
|
||||
if (!Check(FindPad(pad), "separate clear flat test pad found; extraction vehicle untouched")) return -1;
|
||||
m_TestPad = pad;
|
||||
m_Aircraft = Vehicle.Cast(ERR_Actors.Spawn(AIRCRAFT, pad));
|
||||
m_Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(GROUP, pad + "20 0 0"));
|
||||
m_Mode.Own(m_Aircraft); m_Mode.Own(m_Group);
|
||||
if (!Check(m_Aircraft && m_Group, "one stock unarmed airframe and own empty group created once")) return -1;
|
||||
// Group_USSR_Base authors six queued members; use the memberless native
|
||||
// base group so adding one pilot cannot initiate a hidden crew allocation.
|
||||
if (!Check(m_Group.InitFactionKey("USSR") && m_Group.GetFactionName() == "USSR", "memberless native group receives explicit USSR faction")) return -1;
|
||||
m_Group.SetPermanentLOD(0); m_Group.ActivateAI();
|
||||
m_Stage = 8; m_StageDeadline = now + 30000; return 0;
|
||||
}
|
||||
if (m_Stage == 8)
|
||||
{
|
||||
if (!m_Aircraft || !m_Group || PlayerPresent()) { Check(false, "initial fixture airframe/group remain owned and unclaimed"); return -1; }
|
||||
bool componentsReady = PollNativeFlightComponents(now);
|
||||
if (now >= m_StageDeadline) { Check(false, "native flight components and empty primary pilot seat initialize within shared 30-second preparation bound"); return -1; }
|
||||
if (!componentsReady) return 0;
|
||||
if (!Check(!m_PilotSpawnRequested && m_Group.GetAgentsCount() == 0, "native components initialized before the only pilot allocation")) return -1;
|
||||
m_PilotSpawnRequested = true;
|
||||
AIGroup nativeGroup = m_Group;
|
||||
m_Pilot = m_Seat.SpawnCharacterInCompartment(PILOT, nativeGroup, GROUP);
|
||||
if (m_Pilot) m_Mode.Own(m_Pilot);
|
||||
if (!Check(m_Pilot && nativeGroup == m_Group, "one existing vanilla character spawned through native pilot-seat mechanism")) return -1;
|
||||
AIControlComponent pilotControl = AIControlComponent.Cast(m_Pilot.FindComponent(AIControlComponent));
|
||||
if (pilotControl) m_PilotAgent = pilotControl.GetControlAIAgent();
|
||||
if (!Check(m_PilotAgent && OwnPilot(), "single native pilot owns its actual control agent and fixture group")) return -1;
|
||||
// Group LOD does not establish each member's own native LOD. Keep the
|
||||
// distant fixture pilot's actual control agent active, as physical crews do.
|
||||
m_OriginalPilotLOD = m_PilotAgent.GetPermanentLOD(); m_PilotLODSaved = true;
|
||||
m_PilotAgent.SetPermanentLOD(0); m_PilotAgent.ActivateAI();
|
||||
m_Stage = 1; return 0;
|
||||
}
|
||||
if (m_Stage == 1)
|
||||
{
|
||||
if (!m_Aircraft || !m_Group || !m_Controller || !m_Simulation || !m_Seat) { Check(false, "same native flight objects survive seating"); return -1; }
|
||||
SeatingDiagnostic(now);
|
||||
Physics physics = m_Aircraft.GetPhysics();
|
||||
if (physics && physics.IsDynamic() && m_Simulation.IsValid() && m_Wakes == 0) { m_Wakes = 1; physics.SetActive(ActiveState.ACTIVE); m_Stable = -1; return 0; }
|
||||
if (now >= m_StageDeadline) { Check(false, "initial native seating and ground settling complete in 30 seconds"); return -1; }
|
||||
if (!m_Wakes || !OwnPilot() || m_Seat.GetOccupant() != m_Pilot || m_Controller.GetPilotCompartmentSlot() != m_Seat || !ERR_CasualtyLoadingRules.AircraftReady(m_Aircraft)) { m_Stable = -1; return 0; }
|
||||
if (m_Stable < 0) m_Stable = now;
|
||||
if (now - m_Stable < 1000) return 0;
|
||||
m_Character = CharacterControllerComponent.Cast(m_Pilot.FindComponent(CharacterControllerComponent));
|
||||
if (m_Character) m_Input = m_Character.GetInputContext();
|
||||
if (!Check(m_Input && m_Group.GetAgentsCount() == 1 && PoweredAuthority(), "one living native AI pilot physically occupies its own fueled helicopter")) return -1;
|
||||
m_Home = m_Aircraft.GetOrigin(); m_Heading = m_Aircraft.GetYawPitchRoll()[0];
|
||||
m_OriginalMode = m_Controller.GetCollectiveMode(); m_ModeSaved = true;
|
||||
m_InitialFuel = m_Fuel.GetTotalFuel(); m_Bound = true;
|
||||
m_Stage = 2; m_StageDeadline = now + 5000; return 0;
|
||||
}
|
||||
Diagnostic(now);
|
||||
if (!PoweredAuthority() || !m_ReadbackOK || !AutohoverReady()) { Check(false, "current owned conscious pilot, fuel, rotors, selected autohover and exact input readback remain valid"); return -1; }
|
||||
if (m_Stage == 2)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "native AI input and helicopter preparation hooks execute before engine start"); return -1; }
|
||||
if (m_PilotPreparations < 3 || m_ControlUpdates < 3 || m_HelicopterUpdates < 3) return 0;
|
||||
if (!Check(m_Controller.GetCollectiveMode() == CollectiveMode.Direct && m_Input.GetVehicleCollective() == 0, "owned native input hooks apply minimum collective in Direct mode")) return -1;
|
||||
if (!Check(!m_Controller.IsEngineOn(), "native engine remains off until the complete footprint is qualified")) return -1;
|
||||
vector measuredOrigin;
|
||||
float measuredRadius;
|
||||
if (!Check(ERR_NativeFlightFootprint.Measure(m_Aircraft, m_Simulation, m_TestPad, measuredOrigin, measuredRadius), "actual complete model and rotor footprint fits the cleared flight column before engine start")) return -1;
|
||||
// Baseline the actual entity world origin used by geometry and telemetry,
|
||||
// after native placement; this never changes an entity transform.
|
||||
m_Home = measuredOrigin;
|
||||
m_StartRequests = 1;
|
||||
bool started = m_Controller.StartEngine();
|
||||
PrintFormat("ERR POWERED ENGINE REQUEST: StartEngine returned=%1; normal procedure, no force-start or simulation EngineStart", started);
|
||||
m_Stage = 3; m_StageDeadline = now + 65000; return 0;
|
||||
}
|
||||
if (now - m_LastControl > 1000) { Check(false, "native control callback remains live during test flight"); return -1; }
|
||||
vector transform[4]; m_Aircraft.GetWorldTransform(transform);
|
||||
float height = transform[3][1] - m_Home[1];
|
||||
float speed = m_Simulation.GetVelocity().Length();
|
||||
float drift = vector.DistanceXZ(transform[3], m_Home);
|
||||
if (height > 18 || height < -3 || drift >= 8 || transform[1][1] < 0.85) { Check(false, "physical test stays inside height, drift and tilt safety envelope"); return -1; }
|
||||
if (m_Stage == 3)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "normal engine startup and native rotor RPM complete within 65 seconds"); return -1; }
|
||||
float targetRPM = m_Simulation.RotorGetRPMTarget(0);
|
||||
if (!m_Controller.IsEngineOn() || targetRPM <= 0 || m_Simulation.RotorGetRPM(0) < targetRPM * 0.9 || m_Fuel.GetTotalFuel() >= m_InitialFuel) return 0;
|
||||
if (!Check(m_StartRequests == 1 && m_Fuel.GetTotalFuel() < m_InitialFuel, "single normal engine start spins physical rotor and consumes native fuel")) return -1;
|
||||
m_PositiveGateUpdates = m_ControlUpdates; m_PositiveSignalSince = -1;
|
||||
m_Stage = 9; m_StageDeadline = now + 3000; return 0;
|
||||
}
|
||||
if (((m_Stage >= 4 && m_Stage <= 6) || m_Stage == 9 || m_Stage == 10) && !m_Controller.IsEngineOn()) { Check(false, "powered flight retains real running engine"); return -1; }
|
||||
if (m_Stage == 9)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "positive native collective reaches actual rotor signal before climb within three seconds"); return -1; }
|
||||
SignalsManagerComponent signals = m_Controller.GetSignalsManager();
|
||||
int signalIndex = -1;
|
||||
if (signals) signalIndex = signals.FindSignal("MainRotorCollective");
|
||||
if (!Check(signalIndex >= 0, "existing native main rotor collective signal available for consumption proof")) return -1;
|
||||
float observed = signals.GetSignalValue(signalIndex);
|
||||
bool matched = m_ControlUpdates >= m_PositiveGateUpdates + 3 && ERR_PoweredFlightRules.CollectiveObserved(m_Input.GetVehicleCollective(), observed);
|
||||
PrintFormat("ERR POWERED COLLECTIVE GATE: command=%1 rotorSignal=%2 applyUpdates=%3 matched=%4", m_Input.GetVehicleCollective(), observed, m_ControlUpdates - m_PositiveGateUpdates, matched);
|
||||
if (!matched) { m_PositiveSignalSince = -1; return 0; }
|
||||
if (m_PositiveSignalSince < 0) m_PositiveSignalSince = now;
|
||||
if (now - m_PositiveSignalSince < 500) return 0;
|
||||
if (!Check(true, "positive collective persists in existing native rotor signal across actual ticks before climb")) return -1;
|
||||
if (m_ActionInputs)
|
||||
{
|
||||
m_NegativeGateUpdates = m_ControlUpdates; m_NegativeSignalSince = -1;
|
||||
m_Stage = 10; m_StageDeadline = now + 3000; return 0;
|
||||
}
|
||||
if (!Check(BeginAutohover(), "selected native autohover enabled after direct collective consumption proof; exact owned controller readback")) return -1;
|
||||
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
|
||||
}
|
||||
if (m_Stage == 10)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "negative actor collective reaches actual rotor signal before climb within three seconds"); return -1; }
|
||||
if (!Check(m_ActionInputs && m_Controller.GetCollectiveMode() == CollectiveMode.Direct && !m_Controller.GetAutohoverEnabled(), "negative consumption proof uses owned actor actions in Direct mode with autohover off")) return -1;
|
||||
SignalsManagerComponent negativeSignals = m_Controller.GetSignalsManager();
|
||||
int negativeIndex = -1;
|
||||
if (negativeSignals) negativeIndex = negativeSignals.FindSignal("MainRotorCollective");
|
||||
if (!Check(negativeIndex >= 0, "existing native main rotor collective signal available for negative proof")) return -1;
|
||||
float negativeObserved = negativeSignals.GetSignalValue(negativeIndex);
|
||||
bool negativeMatched = m_ControlUpdates >= m_NegativeGateUpdates + 3 && ERR_PoweredFlightRules.NegativeCollectiveObserved(m_Input.GetVehicleCollective(), negativeObserved);
|
||||
PrintFormat("ERR POWERED NEGATIVE COLLECTIVE GATE: command=%1 rotorSignal=%2 actionUpdates=%3 matched=%4", m_Input.GetVehicleCollective(), negativeObserved, m_ControlUpdates - m_NegativeGateUpdates, negativeMatched);
|
||||
if (!negativeMatched) { m_NegativeSignalSince = -1; return 0; }
|
||||
if (m_NegativeSignalSince < 0) m_NegativeSignalSince = now;
|
||||
if (now - m_NegativeSignalSince < 500) return 0;
|
||||
if (!Check(true, "negative actor collective persists in existing native rotor signal across actual ticks before signed flight demand")) return -1;
|
||||
m_SignedCollectiveProven = true;
|
||||
if (!Check(BeginAutohover(), "selected native autohover enabled only after both direct collective consumption proofs")) return -1;
|
||||
m_Collective = -0.8; m_Stage = 4; m_StageDeadline = now + 65000; m_Stable = -1; return 0;
|
||||
}
|
||||
if (m_TranslationMode && m_Stage == 5)
|
||||
{
|
||||
if (!CheckTranslation(now, transform[3], height, speed, transform[1][1], drift)) return -1;
|
||||
return 0;
|
||||
}
|
||||
if (m_Stage == 4 || m_Stage == 5)
|
||||
{
|
||||
bool hover = !m_Simulation.HasAnyGroundContact() && ERR_PoweredFlightRules.Hover(height, speed, m_Simulation.GetVelocity()[1], transform[1][1], drift);
|
||||
if (!hover) m_Stable = -1;
|
||||
if (hover && m_Stable < 0) m_Stable = now;
|
||||
if (now >= m_StageDeadline) { Check(false, "actual powered takeoff and five-second stable hover reached in 65 seconds"); return -1; }
|
||||
if (m_Stable >= 0 && now - m_Stable >= 5000)
|
||||
{
|
||||
if (!Check(m_ControlUpdates > 100 && m_Fuel.GetTotalFuel() < m_InitialFuel - 0.05, "actual 7–9 metre powered hover stable five seconds with native fuel consumption")) return -1;
|
||||
if (m_TranslationMode)
|
||||
{
|
||||
if (!BeginTranslation(now, transform[3], transform[0])) return -1;
|
||||
return 0;
|
||||
}
|
||||
m_Stage = 6; m_StageDeadline = now + 65000; m_Stable = -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (m_Stage == 6)
|
||||
{
|
||||
if (m_TranslationMode && !m_TranslationProven) { Check(false, "translation reaches its frozen target before landing"); return -1; }
|
||||
bool landed = ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1]);
|
||||
if (!landed) m_Stable = -1;
|
||||
if (landed && m_Stable < 0) m_Stable = now;
|
||||
if (now >= m_StageDeadline) { Check(false, "actual native contact landing reached within 65 seconds"); return -1; }
|
||||
if (m_Stable < 0 || now - m_Stable < 2000) return 0;
|
||||
if (!Check(drift < 8 && m_Fuel.GetTotalFuel() < m_InitialFuel - 0.05 && m_Wakes == 1 && m_StartRequests == 1, "same physical aircraft and living AI pilot land with stable ground contact after powered hover")) return -1;
|
||||
m_Controller.StopEngine(false);
|
||||
m_Stage = 7; m_StageDeadline = now + 15000; return 0;
|
||||
}
|
||||
if (m_Stage == 7)
|
||||
{
|
||||
if (now >= m_StageDeadline) { Check(false, "normal engine shutdown completed on actual ground"); return -1; }
|
||||
if (m_Controller.IsEngineOn()) return 0;
|
||||
PrintFormat("ERR POWERED SHUTDOWN: height=%1 speed=%2 angular=%3 up=%4 contact=%5 authority=%6 collective=%7 rotorRPM=%8", height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1], m_Simulation.HasAnyGroundContact(), PoweredAuthority(), m_Input.GetVehicleCollective(), m_Simulation.RotorGetRPM(0));
|
||||
if (!Check(ERR_PoweredFlightRules.Landed(m_Simulation.HasAnyGroundContact(), height, speed, m_Simulation.GetAngularVelocity().Length(), transform[1][1]) && PoweredAuthority(), "native engine off after contact landing; no transform/velocity/health/fuel correction")) return -1;
|
||||
Stop(); m_Complete = true; return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
#ifdef WORKBENCH
|
||||
// Disposable-world fixture. Direct native door boundary, gate damage and real admitted character;
|
||||
// deliberate asynchronous repositioning tests sampling, not walking/input/MP.
|
||||
class ERR_NativePrisonBreachProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected vector m_Center;
|
||||
protected vector m_Target;
|
||||
protected int m_Step;
|
||||
protected int m_Transitions;
|
||||
protected float m_Wait;
|
||||
protected float m_Deadline;
|
||||
void ERR_NativePrisonBreachProbe(ERR_GameMode mode, IEntity player)
|
||||
{
|
||||
m_Mode = mode; m_Player = player;
|
||||
m_Mode.OnPhaseChanged().Insert(PhaseChanged);
|
||||
}
|
||||
protected void PhaseChanged(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (previous == ERR_Phase.PRISON && current == ERR_Phase.RADIO_SEARCH) m_Transitions++;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: prison-breach " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: prison-breach " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected void Move(vector offset)
|
||||
{
|
||||
vector transform[4];
|
||||
m_Player.GetWorldTransform(transform);
|
||||
m_Target = ERR_Actors.Ground(m_Center + offset);
|
||||
transform[3] = m_Target;
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
}
|
||||
protected bool Arrived() { return vector.Distance(m_Player.GetOrigin(), m_Target) < 1; }
|
||||
void Stop() { if (m_Mode) m_Mode.OnPhaseChanged().Remove(PhaseChanged); }
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Deadline == 0) m_Deadline = now + 45000;
|
||||
if (now > m_Deadline || !m_Mode.Active() || !ERR_Actors.CanFight(m_Player) || !m_Mode.Players().CurrentActor(m_Player))
|
||||
{
|
||||
vector observed;
|
||||
if (m_Player) observed = m_Player.GetOrigin();
|
||||
PrintFormat("ERR PRISON BREACH STATE: step=%1 player=%2 target=%3 phase=%4", m_Step, observed, m_Target, m_Mode.Phase());
|
||||
Check(false, "deadline or canonical conscious participant lost");
|
||||
return -1;
|
||||
}
|
||||
if (now < m_Wait) return 0;
|
||||
ERR_PrisonEscapeService escape = m_Mode.PrisonEscape();
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (escape) Print("ERR PRISON BREACH REGISTRATION: " + escape.RegistrationReason());
|
||||
if (!Check(escape && escape.Registered() && escape.Gate() && !escape.GateLost() && m_Mode.Phase() == ERR_Phase.PRISON, "actual authored gate registered before any breach")) return -1;
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
m_Mode.Locations(locations);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
if (location.Kind() == ERR_LocationKind.PRISON) m_Center = location.Position();
|
||||
Move("1.5 0 -4");
|
||||
m_Step = 1; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!Arrived() || !escape.ProbeArmed(m_Player)) return 0;
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "inside sample alone does not complete prison")) return -1;
|
||||
IEntity gate = escape.Gate();
|
||||
BaseDoorComponent door = BaseDoorComponent.Cast(gate.FindComponent(BaseDoorComponent));
|
||||
BaseActionsManagerComponent manager = BaseActionsManagerComponent.Cast(gate.FindComponent(BaseActionsManagerComponent));
|
||||
array<BaseUserAction> actions = {};
|
||||
if (manager) manager.GetActionsList(actions);
|
||||
ERR_PrisonDoorAction guardedAction;
|
||||
foreach (BaseUserAction action : actions)
|
||||
{
|
||||
ERR_PrisonDoorAction candidate = ERR_PrisonDoorAction.Cast(action);
|
||||
if (candidate && candidate.GetOwner() == gate) guardedAction = candidate;
|
||||
}
|
||||
ERR_SiteRuntime prison = m_Mode.Sites().Find("everon-prison-1");
|
||||
array<AIAgent> guards = {};
|
||||
if (prison && prison.Group) prison.Group.GetAgents(guards);
|
||||
IEntity actualGuard;
|
||||
foreach (AIAgent guard : guards)
|
||||
if (guard && ERR_Actors.CanFight(guard.GetControlledEntity()) && !ERR_Actors.Player(guard.GetControlledEntity())) { actualGuard = guard.GetControlledEntity(); break; }
|
||||
if (!Check(door && guardedAction && actualGuard && Math.AbsFloat(door.GetNormalizedDoorState()) < 0.01 && !door.IsOpening(), "actual intact gate is closed with successor door action and original living guard")) return -1;
|
||||
if (!Check(!guardedAction.CanBePerformedScript(actualGuard) && !guardedAction.CanBePerformedScript(m_Player), "locked phase rejects both original AI and actual prisoner action permission")) return -1;
|
||||
// Direct calls deliberately match SCR_AIOpenDoor's execution boundary.
|
||||
// This is not autonomous AI navigation or a player held-input test.
|
||||
guardedAction.PerformAction(gate, actualGuard);
|
||||
guardedAction.PerformAction(gate, m_Player);
|
||||
m_Step = 11; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 11)
|
||||
{
|
||||
IEntity gate = escape.Gate();
|
||||
BaseDoorComponent door;
|
||||
if (gate) door = BaseDoorComponent.Cast(gate.FindComponent(BaseDoorComponent));
|
||||
if (!Check(door && !escape.GateLost() && Math.AbsFloat(door.GetNormalizedDoorState()) < 0.01 && !door.IsOpening() && m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "direct AI/player PerformAction cannot open locked native gate or advance progression")) return -1;
|
||||
SCR_DestructionMultiPhaseComponent damage = SCR_DestructionMultiPhaseComponent.Cast(gate.FindComponent(SCR_DestructionMultiPhaseComponent));
|
||||
if (!Check(damage && damage.GetDefaultHitZone(), "actual gate has its inherited multiphase destruction component and hit zone")) return -1;
|
||||
PrintFormat("ERR PRISON BREACH DAMAGE: health=%1 enabled=%2 threshold=%3", damage.GetHealth(), damage.IsDamageHandlingEnabled(), damage.GetTotalDestructionThreshold());
|
||||
vector hit[3]; hit[0] = gate.GetOrigin(); hit[1] = "0 0 1"; hit[2] = "0 0 -1";
|
||||
SCR_DamageContext context = new SCR_DamageContext(EDamageType.TRUE, 100000, hit, gate, damage.GetDefaultHitZone(), Instigator.CreateInstigator(m_Player), null, -1, -1);
|
||||
damage.HandleDamage(context);
|
||||
m_Step = 2; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!escape.GateLost()) return 0;
|
||||
PrintFormat("ERR PRISON BREACH GATE: remaining=%1 lost=%2", escape.Gate(), escape.GateLost());
|
||||
ERR_PrisonEscapeService late = new ERR_PrisonEscapeService(m_Mode);
|
||||
late.PhaseChanged(ERR_Phase.PREPARING, ERR_Phase.PRISON);
|
||||
bool refusedLate = !late.Registered() && !late.GateLost();
|
||||
late.Stop();
|
||||
if (!Check(refusedLate, "fresh registration after native gate loss stays unregistered and cannot grant escape")) return -1;
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Transitions == 0, "native gate destruction alone cannot advance the round")) return -1;
|
||||
Move("1.5 0 -60");
|
||||
m_Step = 3; m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!Arrived()) return 0;
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && !escape.ProbeArmed(m_Player), "actual far displacement is rejected and clears inside continuity")) return -1;
|
||||
Move("1.5 0 -9");
|
||||
m_Step = 4; m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (!Arrived()) return 0;
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && !escape.ProbeArmed(m_Player), "outside-only arrival cannot finish prison")) return -1;
|
||||
Move("1.5 0 -4");
|
||||
m_Step = 5; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Arrived() || !escape.ProbeArmed(m_Player)) return 0;
|
||||
Move("1.5 0 -9");
|
||||
m_Step = 6; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
if (!Arrived() || m_Mode.Phase() == ERR_Phase.PRISON) return 0;
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Transitions == 1, "bounded inside-to-outside crossing advances once through ordinary phase event")) return -1;
|
||||
ERR_SiteRuntime first = m_Mode.Sites().Find("everon-supply-1");
|
||||
ERR_SiteRuntime second = m_Mode.Sites().Find("everon-supply-2");
|
||||
if (!Check(first && second && first.Location.Knowledge() == 2 && second.Location.Knowledge() == 2, "ordinary escape handlers reveal both scavenging sites")) return -1;
|
||||
m_Step = 7; m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 7)
|
||||
{
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Transitions == 1, "later production ticks do not repeat prison completion")) return -1;
|
||||
Stop();
|
||||
m_Step = 8;
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
#ifdef WORKBENCH
|
||||
// One real controller plus eleven disposable physical characters. This probes
|
||||
// native inventory and transfer compatibility, not twelve clients or UI pickup.
|
||||
class ERR_NativePrisonerProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected ref array<IEntity> m_Actors = {};
|
||||
protected ref map<int, vector> m_Reservations = new map<int, vector>();
|
||||
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
|
||||
protected ref map<string, int> m_Expected = new map<string, int>();
|
||||
protected ERR_SiteRuntime m_Cache;
|
||||
protected InventoryStorageManagerComponent m_Source;
|
||||
protected SCR_InventoryStorageManagerComponent m_Inventory;
|
||||
protected IEntity m_Document;
|
||||
protected vector m_Original[4];
|
||||
protected int m_Step;
|
||||
protected int m_Checked;
|
||||
protected float m_Wait;
|
||||
void ERR_NativePrisonerProbe(ERR_GameMode mode, IEntity player)
|
||||
{
|
||||
m_Mode = mode; m_Player = player;
|
||||
m_Expected.Set("Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et", 1);
|
||||
m_Expected.Set("Prefabs/Characters/Uniforms/Jacket_US_BDU_rolledup.et", 1);
|
||||
m_Expected.Set("Prefabs/Characters/Uniforms/Pants_US_BDU.et", 1);
|
||||
m_Expected.Set("Prefabs/Characters/Footwear/CombatBoots_US_01.et", 1);
|
||||
m_Expected.Set("Prefabs/Items/Equipment/Watches/Watch_SandY184A.et", 1);
|
||||
m_Expected.Set("Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_US_01.et", 2);
|
||||
m_Expected.Set("Prefabs/Items/Medicine/Tourniquet_01/Tourniquet_US_01.et", 1);
|
||||
m_Expected.Set("Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et", 1);
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: prisoner " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: prisoner " + detail, LogLevel.ERROR);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
protected void Positions(string stage)
|
||||
{
|
||||
foreach (int index, IEntity actor : m_Actors)
|
||||
{
|
||||
if (!actor) continue;
|
||||
vector lower, upper;
|
||||
actor.GetWorldBounds(lower, upper);
|
||||
PrintFormat("ERR PRISONER POSITION: stage=%1 index=%2 requested=%3 actual=%4 boundsMin=%5 boundsMax=%6", stage, index, m_Reservations.Get(index), actor.GetOrigin(), lower, upper);
|
||||
}
|
||||
}
|
||||
protected bool Kit(IEntity actor, int index)
|
||||
{
|
||||
if (!Check(actor && actor.GetPrefabData() && actor.GetPrefabData().GetPrefabName() == "{3FF693715C9F8928}Prefabs/ERR/Prisoner.et", "character " + index.ToString() + " uses successor resource")) return false;
|
||||
if (!Check(actor.FindComponent(SCR_CharacterDamageManagerComponent) && actor.FindComponent(CharacterControllerComponent) && actor.FindComponent(SCR_SalineStorageComponent) && actor.FindComponent(SCR_TourniquetStorageComponent) && actor.FindComponent(CompartmentAccessComponent), "character " + index.ToString() + " retains vanilla health, treatment and vehicle components")) return false;
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!Check(inventory != null, "character inventory exists")) return false;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
map<string, int> counts = new map<string, int>();
|
||||
int identityItems;
|
||||
array<IEntity> seen = {};
|
||||
foreach (IEntity item : items)
|
||||
{
|
||||
if (!item || !item.GetPrefabData()) return Check(false, "inventory item has no resource identity");
|
||||
ResourceName resource = item.GetPrefabData().GetPrefabName();
|
||||
PrintFormat("ERR PRISONER ITEM: actor=%1 item=%2 resource=%3 duplicateReference=%4", index, item, resource, seen.Contains(item));
|
||||
if (seen.Contains(item)) continue;
|
||||
seen.Insert(item);
|
||||
// Vanilla creates identification on possession, death or unconsciousness.
|
||||
// Preserve this native identity record; it is not mission navigation equipment.
|
||||
if (resource.EndsWith("Prefabs/Items/PersonalBelongings/PersonalBelongings_US.et"))
|
||||
{
|
||||
if (!Check(ERR_Actors.Player(actor) && item.FindComponent(SCR_IdentityInventoryItemComponent), "possessed prisoner retains native identification")) return false;
|
||||
identityItems++;
|
||||
continue;
|
||||
}
|
||||
// A non-equipment belt is authored by the rolled-up vanilla jacket itself.
|
||||
if (resource.EndsWith("Prefabs/Characters/Vests/Vest_ALICE/Vest_ALICE_belt_dummy.et")) continue;
|
||||
bool allowed = false;
|
||||
foreach (string path, int expected : m_Expected)
|
||||
{
|
||||
if (!resource.EndsWith(path)) continue;
|
||||
counts.Set(path, counts.Get(path) + 1);
|
||||
allowed = true;
|
||||
break;
|
||||
}
|
||||
if (!allowed) return Check(false, "unexpected starter inventory resource: " + resource);
|
||||
}
|
||||
int expectedIdentity;
|
||||
if (ERR_Actors.Player(actor)) expectedIdentity = 1;
|
||||
if (!Check(identityItems == expectedIdentity, "native identification is created exactly once for the possessed prisoner")) return false;
|
||||
foreach (string path, int expected : m_Expected)
|
||||
if (counts.Get(path) != expected) return Check(false, string.Format("character %1 expected %2 of %3, found %4", index, expected, path, counts.Get(path)));
|
||||
PrintFormat("ERR PRISONER INVENTORY: character=%1 storedItems=%2", index, items.Count());
|
||||
return Check(true, "character " + index.ToString() + " has exact clothing/watch/medical kit and no inherited navigation, radio, binoculars, tools, flares or weapons");
|
||||
}
|
||||
protected bool Contains(InventoryStorageManagerComponent inventory, IEntity item)
|
||||
{
|
||||
if (!inventory || !item) return false;
|
||||
array<IEntity> items = {};
|
||||
inventory.GetItems(items);
|
||||
return items.Contains(item);
|
||||
}
|
||||
void Stop()
|
||||
{
|
||||
// Return borrowed stock when its source still exists; never spawn replacements.
|
||||
if (m_Document && m_Cache && m_Cache.Container && m_Inventory && Contains(m_Inventory, m_Document))
|
||||
{
|
||||
BaseInventoryStorageComponent destination = BaseInventoryStorageComponent.Cast(m_Cache.Container.FindComponent(BaseInventoryStorageComponent));
|
||||
if (destination) m_Inventory.TryMoveItemToStorage(m_Document, destination);
|
||||
}
|
||||
for (int i = 1; i < m_Actors.Count(); i++) ERR_Actors.DeleteSpawned(m_Actors[i]);
|
||||
m_Reservations.Clear();
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Check(m_Mode && m_Mode.Phase() == ERR_Phase.PRISON && ERR_Actors.Player(m_Player) && ERR_Actors.CanFight(m_Player), "fixture begins with a real conscious prisoner")) return -1;
|
||||
m_Player.GetWorldTransform(m_Original);
|
||||
m_Actors.Insert(m_Player);
|
||||
m_Reservations.Set(0, m_Player.GetOrigin());
|
||||
Positions("initial");
|
||||
m_Step = 1;
|
||||
m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (m_Actors.Count() < 12)
|
||||
{
|
||||
vector position;
|
||||
bool available = m_Placement.Prison(m_Mode.PrisonPosition(), m_Reservations, position);
|
||||
if (!available) Positions("no-clear-slot");
|
||||
if (!Check(available, "bounded clear prison slot exists for next test character")) return -1;
|
||||
IEntity actor = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, position);
|
||||
if (!Check(actor != null, "physical test prisoner created")) return -1;
|
||||
m_Mode.Own(actor);
|
||||
m_Reservations.Set(m_Actors.Count(), position);
|
||||
m_Actors.Insert(actor);
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
if (!Check(damage != null, "test prisoner damage component exists")) return -1;
|
||||
damage.EnableDamageHandling(false);
|
||||
m_Wait = now + 500;
|
||||
return 0;
|
||||
}
|
||||
Positions("all-spawned");
|
||||
m_Step = 2; m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (m_Checked == 0) Positions("settled");
|
||||
if (!Kit(m_Actors[m_Checked], m_Checked)) return -1;
|
||||
m_Checked++;
|
||||
if (m_Checked < 12) return 0;
|
||||
m_Cache = m_Mode.Sites().Find("everon-supply-1");
|
||||
if (!Check(m_Cache && m_Cache.Container, "authored clue source remains available")) return -1;
|
||||
m_Source = InventoryStorageManagerComponent.Cast(m_Cache.Container.FindComponent(InventoryStorageManagerComponent));
|
||||
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Player.FindComponent(SCR_InventoryStorageManagerComponent));
|
||||
if (!Check(m_Source && m_Inventory, "source and player inventory managers are usable")) return -1;
|
||||
array<IEntity> stock = {};
|
||||
m_Source.GetItems(stock);
|
||||
foreach (IEntity item : stock)
|
||||
{
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(item.FindComponent(ERR_DocumentComponent));
|
||||
if (document && document.TargetId() == "everon-radio-1") { m_Document = item; break; }
|
||||
}
|
||||
if (!Check(m_Document && m_Inventory, "real finite radio document and player storage found")) return -1;
|
||||
vector transform[4]; m_Player.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(m_Cache.Container.GetOrigin() + "0 0 2");
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(transform);
|
||||
m_Step = 3; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
BaseInventoryStorageComponent destination = m_Inventory.FindStorageForItem(m_Document);
|
||||
if (!Check(destination && m_Source.TryMoveItemToStorage(m_Document, destination), "minimal starter clothing accepts a real authored clue transfer")) return -1;
|
||||
m_Step = 4; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (!Check(Contains(m_Inventory, m_Document) && !Contains(m_Source, m_Document) && m_Mode.Phase() == ERR_Phase.PRISON, "document physically moved without duplication while prison phase remains active")) return -1;
|
||||
BaseInventoryStorageComponent destination = BaseInventoryStorageComponent.Cast(m_Cache.Container.FindComponent(BaseInventoryStorageComponent));
|
||||
if (!Check(destination && m_Inventory.TryMoveItemToStorage(m_Document, destination), "borrowed document returns to finite source")) return -1;
|
||||
m_Step = 5; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(Contains(m_Source, m_Document) && !Contains(m_Inventory, m_Document), "borrowed clue restored exactly once")) return -1;
|
||||
Stop();
|
||||
ChimeraCharacter.Cast(m_Player).Teleport(m_Original);
|
||||
m_Step = 6; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
for (int i = 1; i < m_Actors.Count(); i++)
|
||||
if (!Check(!m_Actors[i], "disposable prisoner removed after native cleanup")) return -1;
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player));
|
||||
if (!Check(ERR_Actors.Player(m_Player) && ERR_Actors.Alive(m_Player) && life && life.UsedLife, "real prisoner and spent life preserved")) return -1;
|
||||
m_Step = 7;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#ifdef WORKBENCH
|
||||
// Synchronous roster-body fault injection against the real admitted controller.
|
||||
// No disconnect, possession, account change, network event or native rebind is simulated.
|
||||
class ERR_NativeProgressionIdentityProbe
|
||||
{
|
||||
protected static ERR_GameMode s_Mode;
|
||||
protected static IEntity s_Actor;
|
||||
protected static bool s_DocumentAttempted;
|
||||
protected static bool s_DocumentPassed;
|
||||
protected static bool s_Failed;
|
||||
static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: progression identity " + detail); return true; }
|
||||
s_Failed = true;
|
||||
Print("ERR SMOKE FAIL: progression identity " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
static bool Failed() { return s_Failed; }
|
||||
static bool DocumentPassed() { return s_DocumentPassed; }
|
||||
static void Stop() { s_Mode = null; s_Actor = null; }
|
||||
static void BeginRound() { Stop(); s_DocumentAttempted = false; s_DocumentPassed = false; s_Failed = false; }
|
||||
static bool Begin(ERR_GameMode mode, IEntity actor)
|
||||
{
|
||||
BeginRound();
|
||||
string enabled;
|
||||
if (!Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return false;
|
||||
if (!mode || !actor || !mode.Players()) return false;
|
||||
ERR_PlayerLife life = mode.Players().CurrentActor(actor);
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (!Check(life && controller && controller.GetControlledEntity() == actor && mode.Phase() == ERR_Phase.PRISON && ERR_Actors.CanFight(actor) && SCR_FactionManager.SGetFaction(actor) == mode.PlayerFaction() && vector.DistanceXZ(actor.GetOrigin(), mode.PrisonPosition()) < 5, "actual local conscious prisoner is canonical and inside lock range")) return false;
|
||||
IEntity savedActor = life.Actor;
|
||||
UUID savedIdentity = life.AccountId;
|
||||
int savedConnection = life.ConnectionId;
|
||||
bool savedConnected = life.Connected;
|
||||
bool savedUsedLife = life.UsedLife;
|
||||
// No callback, assertion, early return or yield between mutation and restore.
|
||||
life.Actor = null;
|
||||
bool nativeRosterRefused = mode.Players().CurrentActor(actor) == null;
|
||||
bool refused = !mode.UnlockPrison(actor);
|
||||
life.Actor = savedActor;
|
||||
if (!Check(nativeRosterRefused && refused && mode.Phase() == ERR_Phase.PRISON, "synchronous body-mismatch fault injection rejects normal prison unlock without progression")) return false;
|
||||
if (!Check(mode.Players().CurrentActor(actor) == life && controller.GetControlledEntity() == savedActor && life.AccountId == savedIdentity && life.ConnectionId == savedConnection && life.Connected == savedConnected && life.UsedLife == savedUsedLife, "exact actor, account, connection and used-life state restored before assertions")) return false;
|
||||
s_Mode = mode; s_Actor = actor;
|
||||
Print("ERR IDENTITY FIXTURE LIMIT: synchronous roster Actor=null fault injection, immediately restored; no actual possession, reconnect, foreign faction or multiplayer test");
|
||||
return true;
|
||||
}
|
||||
static bool WantsDocument(ERR_GameMode mode, IEntity actor)
|
||||
{
|
||||
return !s_Failed && !s_DocumentAttempted && mode && mode == s_Mode && actor == s_Actor;
|
||||
}
|
||||
static void AttemptDocument() { s_DocumentAttempted = true; }
|
||||
static void CompleteDocument(bool success) { s_DocumentPassed = success; }
|
||||
}
|
||||
|
||||
modded class ERR_DiscoveryService
|
||||
{
|
||||
protected override void Recover(IEntity player)
|
||||
{
|
||||
if (!ERR_NativeProgressionIdentityProbe.WantsDocument(m_Mode, player)) { super.Recover(player); return; }
|
||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent));
|
||||
if (!inventory) { super.Recover(player); return; }
|
||||
array<IEntity> folders = {}; Folders(inventory, folders);
|
||||
array<ERR_LocationComponent> locations = {}; m_Mode.Locations(locations);
|
||||
IEntity candidate;
|
||||
ERR_LocationComponent target;
|
||||
foreach (IEntity folder : folders)
|
||||
{
|
||||
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(folder.FindComponent(InventoryItemComponent));
|
||||
ERR_DocumentComponent document = ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent));
|
||||
if (!item || !item.GetParentSlot() || !item.GetParentSlot().GetStorage() || !item.GetParentSlot().GetStorage().Contains(folder) || !document) continue;
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
if (location && location.Id() == document.TargetId() && location.Knowledge() < 2) { candidate = folder; target = location; break; }
|
||||
if (candidate) break;
|
||||
}
|
||||
if (!candidate) { super.Recover(player); return; }
|
||||
ERR_NativeProgressionIdentityProbe.AttemptDocument();
|
||||
ERR_PlayerLife life = m_Mode.Players().CurrentActor(player);
|
||||
PlayerController controller = GetGame().GetPlayerController();
|
||||
if (!ERR_NativeProgressionIdentityProbe.Check(life && controller && controller.GetControlledEntity() == player && ERR_Actors.Alive(player) && SCR_FactionManager.SGetFaction(player) == m_Mode.PlayerFaction(), "real held unspent targeted document has an eligible unidentified site and canonical carrier")) return;
|
||||
array<int> knowledge = {};
|
||||
foreach (ERR_LocationComponent location : locations) knowledge.Insert(location.Knowledge());
|
||||
int spent = m_Spent.Count();
|
||||
int previousKnowledge = target.Knowledge();
|
||||
IEntity savedActor = life.Actor;
|
||||
UUID savedIdentity = life.AccountId;
|
||||
int savedConnection = life.ConnectionId;
|
||||
bool savedConnected = life.Connected;
|
||||
bool savedUsedLife = life.UsedLife;
|
||||
// Call the actual production implementation directly: no recursion into this
|
||||
// hook. Restore synchronously before any check, positive call or return.
|
||||
life.Actor = null;
|
||||
super.Recover(player);
|
||||
life.Actor = savedActor;
|
||||
bool unchanged = m_Spent.Count() == spent && !m_Spent.Contains(candidate);
|
||||
for (int index = 0; index < locations.Count(); index++)
|
||||
if (!locations[index] || locations[index].Knowledge() != knowledge[index]) unchanged = false;
|
||||
if (!ERR_NativeProgressionIdentityProbe.Check(unchanged, "synchronous body-mismatch fault injection neither spends the held document nor changes any discovery knowledge")) return;
|
||||
if (!ERR_NativeProgressionIdentityProbe.Check(m_Mode.Players().CurrentActor(player) == life && controller.GetControlledEntity() == savedActor && life.AccountId == savedIdentity && life.ConnectionId == savedConnection && life.Connected == savedConnected && life.UsedLife == savedUsedLife, "document rejection restores exact canonical actor and unchanged used-life identity before normal recovery")) return;
|
||||
super.Recover(player);
|
||||
bool recovered = m_Spent.Contains(candidate) && m_Spent.Count() > spent && target.Knowledge() > previousKnowledge;
|
||||
ERR_NativeProgressionIdentityProbe.CompleteDocument(ERR_NativeProgressionIdentityProbe.Check(recovered, "immediately restored ordinary recovery spends the same actual document and advances its target"));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+367
@@ -0,0 +1,367 @@
|
||||
#ifdef WORKBENCH
|
||||
// Only connection membership is simulated for the second character. Native
|
||||
// actor mutations settle on later frames; isolated service timestamps stay exact.
|
||||
class ERR_NativeRadioSample : ERR_RadioDefenceService
|
||||
{
|
||||
ref array<IEntity> Members = {};
|
||||
int Connections = 2;
|
||||
void ERR_NativeRadioSample(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected override bool IsPlayer(IEntity actor) { return Members.Contains(actor); }
|
||||
protected override int ConnectedPlayers() { return Connections; }
|
||||
protected override void Players(out array<IEntity> players) { players.Copy(Members); }
|
||||
}
|
||||
class ERR_NativeRadioDefenceProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected IEntity m_Second;
|
||||
protected ERR_LocationComponent m_Radio;
|
||||
protected FactionAffiliationComponent m_Affiliation;
|
||||
protected ref ERR_NativeRadioSample m_Service;
|
||||
protected ref ERR_RadioDefenceService m_Authoritative;
|
||||
protected int m_Starts;
|
||||
protected int m_Completions;
|
||||
protected int m_Changes;
|
||||
protected int m_OriginalDuration;
|
||||
protected vector m_Original[4];
|
||||
protected bool m_OriginalUnconscious;
|
||||
protected bool m_Snapshot;
|
||||
protected bool m_SettingsChanged;
|
||||
protected bool m_Restored;
|
||||
protected bool m_RestoreSucceeded;
|
||||
protected int m_Step;
|
||||
protected int m_Result;
|
||||
protected float m_Wait;
|
||||
protected float m_StateDeadline;
|
||||
protected float m_NextStateDiagnostic;
|
||||
|
||||
void ERR_NativeRadioDefenceProbe(ERR_GameMode mode, IEntity player, ERR_LocationComponent radio)
|
||||
{
|
||||
m_Mode = mode; m_Player = player; m_Radio = radio;
|
||||
}
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: radio service " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: radio service " + detail, LogLevel.ERROR);
|
||||
m_Result = -1;
|
||||
Restore();
|
||||
return false;
|
||||
}
|
||||
protected void Started(ERR_LocationComponent location) { m_Starts++; }
|
||||
protected void Changed(ERR_RadioDefenceService service) { m_Changes++; }
|
||||
protected void Completed(ERR_LocationComponent location)
|
||||
{
|
||||
m_Completions++;
|
||||
m_Service.Tick(1000000); // Re-entrant observer cannot complete twice.
|
||||
}
|
||||
protected void Move(IEntity actor, vector position)
|
||||
{
|
||||
vector transform[4]; actor.GetWorldTransform(transform);
|
||||
transform[3] = ERR_Actors.Ground(position);
|
||||
ChimeraCharacter.Cast(actor).Teleport(transform);
|
||||
}
|
||||
protected void Conscious(IEntity actor, bool value)
|
||||
{
|
||||
CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent)).SetUnconscious(!value);
|
||||
}
|
||||
protected float Distance(string label)
|
||||
{
|
||||
float distance = vector.DistanceXZ(m_Player.GetOrigin(), m_Radio.Position());
|
||||
PrintFormat("ERR RADIO POSITION: %1 actor=%2 radio=%3 distanceXZ=%4", label, m_Player.GetOrigin(), m_Radio.Position(), distance);
|
||||
return distance;
|
||||
}
|
||||
protected void WaitFor(int step, float now)
|
||||
{
|
||||
m_Step = step;
|
||||
m_Wait = now + 750;
|
||||
m_StateDeadline = now + 12000;
|
||||
m_NextStateDiagnostic = now;
|
||||
}
|
||||
protected void RecoveryDiagnostic(IEntity actor, string label)
|
||||
{
|
||||
CharacterControllerComponent controller;
|
||||
if (actor) controller = CharacterControllerComponent.Cast(actor.FindComponent(CharacterControllerComponent));
|
||||
int unconscious = -1;
|
||||
int lifeState = -1;
|
||||
if (controller)
|
||||
{
|
||||
unconscious = controller.IsUnconscious();
|
||||
lifeState = controller.GetLifeState();
|
||||
}
|
||||
PrintFormat("ERR RADIO RECOVERY: %1 step=%2 alive=%3 canFight=%4 unconscious=%5 lifeState=%6 origin=%7", label, m_Step, ERR_Actors.Alive(actor), ERR_Actors.CanFight(actor), unconscious, lifeState, actor.GetOrigin());
|
||||
}
|
||||
// Native recovery includes controller/ragdoll state transitions beyond one
|
||||
// frame. Poll the observed state without reissuing SetUnconscious or altering
|
||||
// health. Dead/missing actors fail immediately; delayed recovery has a bound.
|
||||
protected bool Recovered(IEntity actor, float now, string label)
|
||||
{
|
||||
if (!actor || !ERR_Actors.Alive(actor))
|
||||
{
|
||||
Check(false, label + " lost its living actor during recovery");
|
||||
return false;
|
||||
}
|
||||
if (ERR_Actors.CanFight(actor))
|
||||
{
|
||||
RecoveryDiagnostic(actor, label + " ready");
|
||||
return true;
|
||||
}
|
||||
if (now >= m_NextStateDiagnostic)
|
||||
{
|
||||
RecoveryDiagnostic(actor, label + " waiting");
|
||||
m_NextStateDiagnostic = now + 1000;
|
||||
}
|
||||
if (now >= m_StateDeadline)
|
||||
{
|
||||
Check(false, label + " did not regain conscious native state within twelve seconds");
|
||||
return false;
|
||||
}
|
||||
m_Wait = now + 250;
|
||||
return false;
|
||||
}
|
||||
protected void AuthorizationDiagnostic()
|
||||
{
|
||||
int id = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player);
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
||||
bool connected = life && life.Connected;
|
||||
bool usedLife = life && life.UsedLife;
|
||||
bool sameActor = life && life.Actor == m_Player;
|
||||
bool friendly = m_Mode.PlayerFaction() && SCR_FactionManager.SGetFaction(m_Player) == m_Mode.PlayerFaction();
|
||||
PrintFormat("ERR RADIO AUTH: active=%1 phase=%2 registered=%3 kind=%4 knowledge=%5 serviceState=%6 distance=%7", m_Mode.Active(), m_Mode.Phase(), m_Mode.HasLocation(m_Radio), m_Radio.Kind(), m_Radio.Knowledge(), m_Authoritative.State(), vector.DistanceXZ(m_Player.GetOrigin(), m_Radio.Position()));
|
||||
PrintFormat("ERR RADIO AUTH: playerId=%1 connected=%2 usedLife=%3 sameActor=%4 friendly=%5 canFight=%6 canBegin=%7", id, connected, usedLife, sameActor, friendly, ERR_Actors.CanFight(m_Player), m_Authoritative.CanBegin(m_Player, m_Radio));
|
||||
}
|
||||
protected void Restore()
|
||||
{
|
||||
if (m_Restored) return;
|
||||
m_Restored = true;
|
||||
m_RestoreSucceeded = true;
|
||||
if (m_Service)
|
||||
{
|
||||
m_Service.OnStarted().Remove(Started);
|
||||
m_Service.OnProgressChanged().Remove(Changed);
|
||||
m_Service.OnCompleted().Remove(Completed);
|
||||
m_Service.Stop();
|
||||
}
|
||||
if (m_Authoritative) m_Authoritative.Stop();
|
||||
if (!GetGame() || !Replication.IsServer()) return;
|
||||
if (m_SettingsChanged && m_Mode && m_Mode.Settings())
|
||||
m_RestoreSucceeded = m_Mode.Settings().Set("radioDefenceSeconds", m_OriginalDuration);
|
||||
if (m_Snapshot && m_Player)
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||
ChimeraCharacter character = ChimeraCharacter.Cast(m_Player);
|
||||
if (controller && character && ERR_Actors.Alive(m_Player))
|
||||
{
|
||||
controller.SetUnconscious(m_OriginalUnconscious);
|
||||
character.Teleport(m_Original);
|
||||
}
|
||||
else m_RestoreSucceeded = false;
|
||||
}
|
||||
ERR_Actors.DeleteSpawned(m_Second);
|
||||
m_Second = null;
|
||||
}
|
||||
// The parent must call this on cancellation/fixture timeout as well as failure.
|
||||
// No fixture-owned callqueue is installed; no later mutation runs after Stop.
|
||||
void Stop()
|
||||
{
|
||||
if (m_Result == 0) m_Result = -1;
|
||||
Restore();
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Result != 0) return m_Result;
|
||||
if (now < m_Wait) return 0;
|
||||
if (!Replication.IsServer() || !m_Mode || !m_Player || !m_Radio || !m_Mode.Settings())
|
||||
{
|
||||
Check(false, "async fixture lost authoritative mission/player/radio");
|
||||
return -1;
|
||||
}
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Radio.Knowledge() == 2 && ERR_Actors.Player(m_Player), "identified radio and real controlled prisoner available")) return -1;
|
||||
m_OriginalDuration = m_Mode.Settings().Get("radioDefenceSeconds");
|
||||
m_Player.GetWorldTransform(m_Original);
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||
if (!Check(controller && ChimeraCharacter.Cast(m_Player), "controlled prisoner supports native state restoration")) return -1;
|
||||
m_OriginalUnconscious = controller.IsUnconscious();
|
||||
m_Snapshot = true;
|
||||
if (!Check(m_Mode.Settings().Set("radioDefenceSeconds", 30), "isolated thirty-second duration saved")) return -1;
|
||||
m_SettingsChanged = true;
|
||||
m_Second = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, ERR_Actors.Ground(m_Radio.Position() + "-2 0 0"));
|
||||
if (!Check(m_Second != null, "second physical defender spawned")) return -1;
|
||||
m_Mode.Own(m_Second);
|
||||
DamageManagerComponent.Cast(m_Second.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
WaitFor(1, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
if (!Check(ERR_Actors.CanFight(m_Second), "second physical defender settles with simulated connection membership")) return -1;
|
||||
m_Authoritative = new ERR_RadioDefenceService(m_Mode);
|
||||
if (!Check(!m_Authoritative.Begin(m_Second, m_Radio), "ordinary AI cannot tune even while alive and nearby")) return -1;
|
||||
if (!Check(!m_Authoritative.Begin(m_Player, null), "missing radio rejected")) return -1;
|
||||
Move(m_Player, m_Radio.Position() + "6 0 0");
|
||||
WaitFor(2, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!Check(Distance("outside tuning range after deferred teleport") > 5 && !m_Authoritative.Begin(m_Player, m_Radio), "tuning outside five metres rejected")) return -1;
|
||||
Move(m_Player, m_Radio.Position() + "2 0 0");
|
||||
Conscious(m_Player, false);
|
||||
WaitFor(3, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||
if (!Check(Distance("unconscious tuning range after deferred teleport") <= 5 && controller.IsUnconscious() && !m_Authoritative.Begin(m_Player, m_Radio), "unconscious prisoner cannot tune")) return -1;
|
||||
Conscious(m_Player, true);
|
||||
WaitFor(4, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (!Recovered(m_Player, now, "tuning prisoner")) return m_Result;
|
||||
AuthorizationDiagnostic();
|
||||
m_Service = new ERR_NativeRadioSample(m_Mode);
|
||||
if (!Check(ERR_Actors.CanFight(m_Player) && m_Authoritative.CanBegin(m_Player, m_Radio), "registered friendly conscious prisoner remains authorized")) return -1;
|
||||
if (!Check(!m_Service.Begin(m_Player, m_Radio), "unregistered defender cannot start the isolated service")) return -1;
|
||||
m_Service.Members.Insert(m_Player); m_Service.Members.Insert(m_Second);
|
||||
m_Affiliation = FactionAffiliationComponent.Cast(m_Second.FindComponent(FactionAffiliationComponent));
|
||||
if (!Check(m_Affiliation != null, "physical defender has native faction affiliation")) return -1;
|
||||
m_Affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
WaitFor(5, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 5)
|
||||
{
|
||||
if (!Check(SCR_FactionManager.SGetFaction(m_Second) == m_Mode.EnemyFaction() && !m_Service.Begin(m_Second, m_Radio), "hostile controlled defender cannot tune")) return -1;
|
||||
m_Affiliation.SetAffiliatedFaction(m_Mode.PlayerFaction());
|
||||
WaitFor(6, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 6)
|
||||
{
|
||||
m_Service.OnStarted().Insert(Started);
|
||||
m_Service.OnProgressChanged().Insert(Changed);
|
||||
m_Service.OnCompleted().Insert(Completed);
|
||||
if (!Check(m_Service.Begin(m_Player, m_Radio) && !m_Service.Begin(m_Player, m_Radio) && m_Starts == 1, "start event emitted once and duplicate start rejected")) return -1;
|
||||
m_Service.Tick(1000);
|
||||
if (!Check(m_Service.DefenderCount() == 2 && m_Service.Since() == 1000 && m_Service.Occupied(), "two conscious physical defenders share one timer")) return -1;
|
||||
m_Affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||
WaitFor(7, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 7)
|
||||
{
|
||||
m_Service.Tick(1500);
|
||||
if (!Check(!m_Service.CanDefend(m_Second) && m_Service.DefenderCount() == 1 && m_Service.Since() == 1000, "hostile actor cannot defend even with simulated connection membership")) return -1;
|
||||
m_Affiliation.SetAffiliatedFaction(m_Mode.PlayerFaction());
|
||||
WaitFor(8, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 8)
|
||||
{
|
||||
m_Service.Tick(1600);
|
||||
int changed = m_Changes;
|
||||
m_Service.Tick(6000);
|
||||
if (!Check(m_Service.SecondsRemaining(6000) == 25 && m_Changes == changed, "elapsed countdown does not emit redundant progress events")) return -1;
|
||||
Conscious(m_Player, false);
|
||||
WaitFor(9, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 9)
|
||||
{
|
||||
m_Service.Tick(7000);
|
||||
if (!Check(m_Service.DefenderCount() == 1 && m_Service.Since() == 1000, "second defender preserves progress while first is unconscious")) return -1;
|
||||
Conscious(m_Second, false);
|
||||
WaitFor(10, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 10)
|
||||
{
|
||||
m_Service.Tick(8000);
|
||||
if (!Check(!m_Service.Occupied() && m_Service.Since() == -1 && m_Service.SecondsRemaining(8000) == 30, "all unconscious defenders reset accumulated time")) return -1;
|
||||
Conscious(m_Player, true);
|
||||
WaitFor(11, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 11)
|
||||
{
|
||||
if (!Recovered(m_Player, now, "first defender")) return m_Result;
|
||||
m_Service.Tick(9000);
|
||||
if (!Check(m_Service.DefenderCount() == 1 && m_Service.Since() == 9000, "conscious recovery starts a fresh continuous interval")) return -1;
|
||||
Conscious(m_Second, true);
|
||||
WaitFor(12, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 12)
|
||||
{
|
||||
if (!Recovered(m_Second, now, "second defender")) return m_Result;
|
||||
m_Service.Tick(10000);
|
||||
m_Service.Connections = 0; m_Service.Tick(11000);
|
||||
if (!Check(m_Service.DefenderCount() == 0 && !m_Service.Occupied(), "empty server resets despite remaining character bodies")) return -1;
|
||||
m_Service.Connections = 2; m_Service.Tick(12000);
|
||||
Move(m_Player, m_Radio.Position() + "60.5 0 0");
|
||||
WaitFor(13, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 13)
|
||||
{
|
||||
m_Service.Tick(13000);
|
||||
if (!Check(Distance("outside defence radius after deferred teleport") >= 60 && !m_Service.CanDefend(m_Player) && m_Service.DefenderCount() == 1 && m_Service.Since() == 12000, "outside sixty metres excludes first defender without resetting second")) return -1;
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Second.FindComponent(DamageManagerComponent));
|
||||
damage.EnableDamageHandling(true);
|
||||
SCR_CharacterDamageManagerComponent.Cast(damage).Kill(Instigator.CreateInstigator(m_Second));
|
||||
WaitFor(14, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 14)
|
||||
{
|
||||
m_Service.Tick(14000);
|
||||
if (!Check(!ERR_Actors.Alive(m_Second) && !m_Service.CanDefend(m_Second) && !m_Service.Occupied(), "dead defender cannot maintain transmission")) return -1;
|
||||
Move(m_Player, m_Radio.Position() + "2 0 0");
|
||||
WaitFor(15, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 15)
|
||||
{
|
||||
if (!Check(Distance("returned to radio after deferred teleport") <= 5 && ERR_Actors.CanFight(m_Player), "survivor returns physically before final transmission samples")) return -1;
|
||||
m_Service.Tick(15000);
|
||||
if (!Check(m_Mode.Settings().Set("radioDefenceSeconds", 45), "live duration increase saved")) return -1;
|
||||
m_Service.Tick(45000);
|
||||
if (!Check(m_Service.State() == ERR_RadioDefenceState.DEFENDING && m_Service.Since() == 15000 && m_Service.SecondsRemaining(45000) == 15, "live duration increase preserves accumulated occupancy")) return -1;
|
||||
if (!Check(m_Mode.Settings().Set("radioDefenceSeconds", 30), "live duration reduction saved")) return -1;
|
||||
m_Service.Tick(45000);
|
||||
if (!Check(m_Service.State() == ERR_RadioDefenceState.COMPLETED && m_Completions == 1 && m_Service.SecondsRemaining(45000) == 0, "live duration reduction completes at exact continuous boundary")) return -1;
|
||||
m_Service.Tick(46000); m_Service.Stop(); m_Service.Tick(47000);
|
||||
if (!Check(m_Completions == 1 && !m_Service.Begin(m_Player, m_Radio), "completed service cannot restart or emit duplicate completion after stop")) return -1;
|
||||
ERR_NativeRadioSample stopped = new ERR_NativeRadioSample(m_Mode);
|
||||
stopped.Members.Insert(m_Player); stopped.OnCompleted().Insert(Completed);
|
||||
if (!Check(stopped.Begin(m_Player, m_Radio), "isolated cancellation service begins")) return -1;
|
||||
stopped.Tick(1000); stopped.Stop(); stopped.Stop(); stopped.Tick(60000);
|
||||
if (!Check(stopped.State() == ERR_RadioDefenceState.STOPPED && !stopped.Occupied() && m_Completions == 1 && !stopped.Begin(m_Player, m_Radio), "stop cancels progress and future starts without completing")) return -1;
|
||||
ERR_NativeRadioSample terminal = new ERR_NativeRadioSample(m_Mode);
|
||||
terminal.Members.Insert(m_Player); terminal.OnCompleted().Insert(Completed);
|
||||
if (!Check(terminal.Begin(m_Player, m_Radio), "isolated terminal-phase service begins")) return -1;
|
||||
terminal.Tick(1000); terminal.PhaseChanged(ERR_Phase.RADIO_DEFENCE, ERR_Phase.DEFEAT); terminal.Tick(60000);
|
||||
if (!Check(terminal.State() == ERR_RadioDefenceState.STOPPED && m_Completions == 1, "terminal phase event prevents later completion")) return -1;
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Mode.RadioDefence().State() == ERR_RadioDefenceState.IDLE, "isolated event tests leave live objective ready for normal tuning")) return -1;
|
||||
Restore();
|
||||
WaitFor(90, now);
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 90)
|
||||
{
|
||||
if (!m_OriginalUnconscious && !Recovered(m_Player, now, "restored prisoner")) return m_Result;
|
||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||
PrintFormat("ERR RADIO RESTORE: actual=%1 original=%2 distance=%3", m_Player.GetOrigin(), m_Original[3], vector.Distance(m_Player.GetOrigin(), m_Original[3]));
|
||||
if (!Check(m_RestoreSucceeded && m_Mode.Settings().Get("radioDefenceSeconds") == m_OriginalDuration && controller.IsUnconscious() == m_OriginalUnconscious && vector.Distance(m_Player.GetOrigin(), m_Original[3]) < 1.5, "isolated test restores live duration and physical player state after a world frame")) return -1;
|
||||
m_Result = 1;
|
||||
return 1;
|
||||
}
|
||||
Check(false, "unknown asynchronous fixture step");
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeRecoveryProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected IEntity m_Player;
|
||||
protected CharacterControllerComponent m_Controller;
|
||||
protected float m_Wait;
|
||||
protected float m_RecoveryDeadline;
|
||||
protected bool m_Stopped;
|
||||
protected bool m_ChangedConsciousness;
|
||||
protected bool m_InitiallyUnconscious;
|
||||
protected int m_Step;
|
||||
protected int m_Changes;
|
||||
void ERR_NativeRecoveryProbe(ERR_GameMode mode, IEntity player)
|
||||
{
|
||||
m_Mode = mode; m_Player = player;
|
||||
m_Controller = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
|
||||
if (m_Controller) m_InitiallyUnconscious = m_Controller.IsUnconscious();
|
||||
mode.Recovery().OnChanged().Insert(Changed);
|
||||
mode.OnStop().Insert(Stop);
|
||||
}
|
||||
protected void Changed(string message) { m_Changes++; }
|
||||
void Stop()
|
||||
{
|
||||
if (m_Stopped) return;
|
||||
m_Stopped = true;
|
||||
if (m_Mode) m_Mode.OnStop().Remove(Stop);
|
||||
if (m_Mode && m_Mode.Recovery()) m_Mode.Recovery().OnChanged().Remove(Changed);
|
||||
if (m_ChangedConsciousness && m_Controller && ERR_Actors.Alive(m_Player)) m_Controller.SetUnconscious(m_InitiallyUnconscious);
|
||||
}
|
||||
protected bool Check(bool ok, string detail)
|
||||
{
|
||||
if (ok) { Print("ERR SMOKE PASS: recovery " + detail); return true; }
|
||||
Stop();
|
||||
Print("ERR SMOKE FAIL: recovery " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (m_Stopped) return -1;
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(m_Controller && m_Mode.Recovery().Conscious() == 1 && m_Mode.RecoveryStatus().IsEmpty(), "real conscious prisoner is available without recovery warning")) return -1;
|
||||
array<string> replies = {};
|
||||
m_Mode.Admin().Execute(m_Mode, false, "mission abandon", replies);
|
||||
if (!Check(replies.Count() == 1 && replies[0].Contains("Administrator") && m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "unauthorized abandonment leaves round and result intact")) return -1;
|
||||
m_Mode.Admin().Execute(m_Mode, true, "mission abandon now", replies);
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "malformed abandonment does not end a round")) return -1;
|
||||
m_ChangedConsciousness = true;
|
||||
m_Controller.SetUnconscious(true);
|
||||
m_Step = 1; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Recovery().Incapacitated() == 1 && m_Mode.Recovery().Conscious() == 0, "native unconscious prisoner is counted as incapacitated, not dead")) return -1;
|
||||
if (!Check(m_Mode.RecoveryStatus().Contains("No conscious prisoner") && m_Mode.Status().Contains("mission abandon") && ERR_ObjectiveText.Description(m_Mode).Contains(m_Mode.RecoveryStatus()), "server status and native objective copy explain the recovery state")) return -1;
|
||||
if (!Check(m_Changes == 1, "recovery state publishes exactly one change")) return -1;
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(m_Changes == 1, "unchanged sampling emits no duplicate recovery event")) return -1;
|
||||
m_Step = 2; m_Wait = now + 35000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Players().HasSurvivor(), "all-incapacitated player survives beyond ordinary defeat grace")) return -1;
|
||||
m_Controller.SetUnconscious(false);
|
||||
m_RecoveryDeadline = now + 12000;
|
||||
m_Step = 3; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!ERR_Actors.CanFight(m_Player) && now < m_RecoveryDeadline) return 0;
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(ERR_Actors.CanFight(m_Player) && m_Mode.RecoveryStatus().IsEmpty() && m_Mode.Recovery().Conscious() == 1 && m_Changes == 2, "native recovery clears the warning once")) return -1;
|
||||
int id = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player);
|
||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
||||
if (!Check(life && life.UsedLife && life.Actor == m_Player, "recovery preserves original actor and spent life")) return -1;
|
||||
m_Controller.SetUnconscious(true);
|
||||
m_Step = 4; m_Wait = now + 1000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
m_Mode.Recovery().Tick();
|
||||
if (!Check(!m_Mode.RecoveryStatus().IsEmpty() && m_Changes == 3, "second incapacitation restores warning before abandonment")) return -1;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
#ifdef WORKBENCH
|
||||
// Local controller and native presentation provider; no network clients are
|
||||
// simulated. The extraction snapshot was produced by real occupied seats.
|
||||
class ERR_NativeResultProbe
|
||||
{
|
||||
protected static Widget s_Content;
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: result " + detail); return true; }
|
||||
if (s_Content) s_Content.RemoveFromHierarchy();
|
||||
s_Content = null;
|
||||
Print("ERR SMOKE FAIL: result " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected static bool NativeText(string title, string debrief)
|
||||
{
|
||||
if (!s_Content) return false;
|
||||
TextWidget titleWidget = TextWidget.Cast(s_Content.FindAnyWidget("GameOver_State"));
|
||||
TextWidget debriefWidget = TextWidget.Cast(s_Content.FindAnyWidget("GameOver_Description"));
|
||||
return titleWidget && debriefWidget && titleWidget.GetText() == title && debriefWidget.GetText() == debrief;
|
||||
}
|
||||
static bool Run(ERR_ExtractionService extraction, UUID escapedAccount, UUID missedAccount)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!Check(controller && extraction && extraction.State() == ERR_ExtractionState.ESCAPED, "local controller and physical extraction snapshot exist")) return false;
|
||||
int playerId = controller.GetPlayerId();
|
||||
ERR_EscapeGameOverScreenInfo info = new ERR_EscapeGameOverScreenInfo();
|
||||
array<int> staleIds = {playerId};
|
||||
SCR_GameModeEndData staleData = SCR_GameModeEndData.Create(EGameOverTypes.VICTORY, staleIds);
|
||||
array<int> copiedIds = {};
|
||||
staleData.GetWinnerIds(copiedIds);
|
||||
controller.ERR_ResetEscapeResult();
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.UNKNOWN && info.GetTitle(playerId, copiedIds) == "The helicopter escaped", "unknown controller ignores stale native end-data winner ID")) return false;
|
||||
ResourceName layout = "{C4507D07F3C54831}UI/layouts/HUD/GameOver/EndScreen/EndScreenContent_NoImage.layout";
|
||||
s_Content = GetGame().GetWorkspace().CreateWidgets(layout);
|
||||
if (!Check(s_Content != null, "native result content widgets created")) return false;
|
||||
s_Content.SetVisible(false);
|
||||
SCR_GameOverScreenContentUIComponent contentHandler = SCR_GameOverScreenContentUIComponent.Cast(s_Content.FindHandler(SCR_GameOverScreenContentUIComponent));
|
||||
if (!Check(contentHandler != null, "native result content handler attached")) return false;
|
||||
SCR_GameOverScreenUIContentData content = new SCR_GameOverScreenUIContentData(layout, info, staleData, "STALE WINNER", string.Empty, "STALE DEBRIEF", ResourceName.Empty, ResourceName.Empty, null, string.Empty, string.Empty, string.Empty);
|
||||
contentHandler.InitContent(content);
|
||||
if (!Check(NativeText("The helicopter escaped", "Your personal escape result is not available."), "opened native content ignores a stale winner while personal result is unknown")) return false;
|
||||
ERR_PlayerRoster roster = new ERR_PlayerRoster();
|
||||
if (!Check(roster.Bind(playerId, escapedAccount), "isolated presentation account binds to local controller ID")) return false;
|
||||
roster.Spawned(playerId, controller.GetControlledEntity());
|
||||
controller.ERR_PublishEscapeResult(roster, extraction);
|
||||
array<int> noWinners = {};
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.ESCAPED && info.GetTitle(playerId, noWinners) == "You escaped Everon", "identity snapshot publishes victory despite missing end-data ID")) return false;
|
||||
if (!Check(NativeText("You escaped Everon", "You crossed the extraction boundary aboard the helicopter."), "already-open native content refreshes when personal victory arrives")) return false;
|
||||
if (!Check(roster.Bind(playerId, missedAccount), "local presentation connection ID is recycled to non-escapee")) return false;
|
||||
roster.Spawned(playerId, controller.GetControlledEntity());
|
||||
controller.ERR_PublishEscapeResult(roster, extraction);
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.MISSED && info.GetTitle(playerId, copiedIds) == "The helicopter escaped" && info.GetDebriefing(playerId, copiedIds) == "You did not escape with the helicopter.", "recycled winner ID cannot grant escape or misdescribe dead occupants")) return false;
|
||||
if (!Check(NativeText("The helicopter escaped", "You did not escape with the helicopter."), "native text refresh removes the previous account's win")) return false;
|
||||
if (!Check(roster.Bind(playerId, escapedAccount), "escaped presentation account returns after simulated rebind")) return false;
|
||||
controller.ERR_PublishEscapeResult(roster, extraction);
|
||||
if (!Check(info.GetTitle(playerId, noWinners) == "You escaped Everon", "presentation follows returning account rather than copied end data")) return false;
|
||||
controller.ERR_ResetEscapeResult();
|
||||
if (!Check(controller.ERR_EscapeResult() == ERR_PersonalEscapeResult.UNKNOWN && info.GetTitle(playerId, copiedIds) == "The helicopter escaped", "round reset clears personal result despite stale winner ID")) return false;
|
||||
if (!Check(NativeText("The helicopter escaped", "Your personal escape result is not available."), "round reset clears the open native result text")) return false;
|
||||
s_Content.RemoveFromHierarchy();
|
||||
s_Content = null;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
controller.ERR_PublishEscapeResult(mode.Players(), mode.Extraction());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
#ifdef WORKBENCH
|
||||
// Read-only spatial survey of new authored site candidates. No world edits.
|
||||
class ERR_NativeSiteSurvey
|
||||
{
|
||||
protected bool m_Blocked;
|
||||
protected string m_Obstacle;
|
||||
protected bool Obstacle(IEntity entity)
|
||||
{
|
||||
if (entity && entity.GetVObject())
|
||||
{
|
||||
m_Blocked = true;
|
||||
m_Obstacle = entity.ToString();
|
||||
if (entity.GetPrefabData()) m_Obstacle = entity.GetPrefabData().GetPrefabName();
|
||||
}
|
||||
return !m_Blocked;
|
||||
}
|
||||
void Area(string name, vector center)
|
||||
{
|
||||
int found, slopes, blocked;
|
||||
float best = 10000;
|
||||
for (int x = -80; x <= 80; x += 20)
|
||||
for (int z = -80; z <= 80; z += 20)
|
||||
{
|
||||
vector position = ERR_Actors.Ground(center + Vector(x, 0, z));
|
||||
float low = 10000;
|
||||
float high = -10000;
|
||||
for (int dx = -2; dx <= 2; dx += 2)
|
||||
for (int dz = -2; dz <= 2; dz += 2)
|
||||
{
|
||||
float height = GetGame().GetWorld().GetSurfaceY(position[0] + dx, position[2] + dz);
|
||||
low = Math.Min(low, height);
|
||||
high = Math.Max(high, height);
|
||||
}
|
||||
best = Math.Min(best, high-low);
|
||||
if (low < 1 || high - low > 1.2) { slopes++; continue; }
|
||||
m_Blocked = false;
|
||||
GetGame().GetWorld().QueryEntitiesByAABB(Vector(position[0]-1.3, low+0.1, position[2]-1.3), Vector(position[0]+1.3, high+2, position[2]+1.3), Obstacle);
|
||||
if (m_Blocked)
|
||||
{
|
||||
blocked++;
|
||||
if (blocked <= 2) PrintFormat("ERR SITE OBSTACLE: area=%1 position=%2 object=%3", name, position, m_Obstacle);
|
||||
continue;
|
||||
}
|
||||
found++;
|
||||
PrintFormat("ERR SITE CANDIDATE: area=%1 position=%2 elevationRange=%3", name, position, high-low);
|
||||
}
|
||||
PrintFormat("ERR SITE SURVEY COUNT: area=%1 clear=%2 slopes=%3 blocked=%4 bestSlope=%5", name, found, slopes, blocked, best);
|
||||
}
|
||||
void Run()
|
||||
{
|
||||
Area("west-supplies", "5900 0 7200");
|
||||
Area("medical", "6320 0 7440");
|
||||
Area("outpost", "6740 0 7320");
|
||||
Print("ERR SMOKE PASS: candidate survey completed without authoring world changes");
|
||||
Print("ERR SMOKE COMPLETE: read-only site survey");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
// Opt-in local diagnostic only. It does not bypass validation or alter cleanup.
|
||||
#ifdef WORKBENCH
|
||||
modded class SCR_BaseItemSupportStationComponent
|
||||
{
|
||||
protected static int s_ERRStationTeardownLines;
|
||||
protected static int s_ERRStationInitLines;
|
||||
protected bool m_ERRStationInitSeen;
|
||||
protected bool m_ERRStationInitModePresent;
|
||||
protected bool m_ERRStationInitMainWorld;
|
||||
protected bool m_ERRStationInitSiteOwned;
|
||||
protected bool m_ERRStationInitInInventory;
|
||||
protected ResourceName m_ERRStationInitParentPrefab;
|
||||
protected bool m_ERRStationInDelete;
|
||||
protected IEntity m_ERRStationDeleteOwner;
|
||||
|
||||
protected override void DelayedInit(IEntity owner)
|
||||
{
|
||||
// Preserve initialization and every native validation before observation.
|
||||
super.DelayedInit(owner);
|
||||
string enabled;
|
||||
if (!System.GetCLIParam("errStationTeardownDiag", enabled) || enabled != "1" || !owner) return;
|
||||
m_ERRStationInitSeen = true;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
m_ERRStationInitModePresent = mode != null;
|
||||
if (GetGame()) m_ERRStationInitMainWorld = owner.GetWorld() == GetGame().GetWorld();
|
||||
ERR_SiteService sites;
|
||||
if (mode) sites = mode.Sites();
|
||||
m_ERRStationInitSiteOwned = sites && sites.Owns(owner);
|
||||
InventoryItemComponent item = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent));
|
||||
m_ERRStationInitInInventory = item && item.GetParentSlot();
|
||||
IEntity parent = owner.GetParent();
|
||||
if (parent && parent.GetPrefabData()) m_ERRStationInitParentPrefab = parent.GetPrefabData().GetPrefabName();
|
||||
if (s_ERRStationInitLines >= 64) return;
|
||||
s_ERRStationInitLines++;
|
||||
ResourceName prefab;
|
||||
if (owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
|
||||
PrintFormat("ERR STATION INIT OWNERSHIP: n=%1 prefab=%2 modePresent=%3 mainWorld=%4 siteOwned=%5 inInventory=%6 parentPrefab=%7", s_ERRStationInitLines, prefab, m_ERRStationInitModePresent, m_ERRStationInitMainWorld, m_ERRStationInitSiteOwned, m_ERRStationInitInInventory, m_ERRStationInitParentPrefab);
|
||||
}
|
||||
|
||||
override void OnDelete(IEntity owner)
|
||||
{
|
||||
string enabled;
|
||||
bool diagnose = System.GetCLIParam("errStationTeardownDiag", enabled) && enabled == "1";
|
||||
if (diagnose)
|
||||
{
|
||||
m_ERRStationInDelete = true;
|
||||
m_ERRStationDeleteOwner = owner;
|
||||
}
|
||||
|
||||
// Preserve vanilla OnDelete, including its call to InitValidSetup.
|
||||
super.OnDelete(owner);
|
||||
|
||||
m_ERRStationInDelete = false;
|
||||
m_ERRStationDeleteOwner = null;
|
||||
}
|
||||
|
||||
protected override bool InitValidSetup()
|
||||
{
|
||||
if (m_ERRStationInDelete && s_ERRStationTeardownLines < 64)
|
||||
{
|
||||
s_ERRStationTeardownLines++;
|
||||
IEntity owner = m_ERRStationDeleteOwner;
|
||||
string ownerName = "<deleted>";
|
||||
ResourceName prefab;
|
||||
if (owner)
|
||||
{
|
||||
ownerName = owner.GetName();
|
||||
if (owner.GetPrefabData()) prefab = owner.GetPrefabData().GetPrefabName();
|
||||
}
|
||||
bool globalCatalogPresent = SCR_EntityCatalogManagerComponent.GetInstance() != null;
|
||||
SCR_BaseGameMode baseMode;
|
||||
if (GetGame()) baseMode = SCR_BaseGameMode.Cast(GetGame().GetGameMode());
|
||||
int gameState = -1;
|
||||
int phase = -1;
|
||||
bool hasEndData;
|
||||
if (baseMode)
|
||||
{
|
||||
gameState = baseMode.GetState();
|
||||
hasEndData = baseMode.GetEndGameData() != null;
|
||||
ERR_GameMode mode = ERR_GameMode.Cast(baseMode);
|
||||
if (mode) phase = mode.Phase();
|
||||
}
|
||||
string detail = string.Format("ERR STATION TEARDOWN DIAG: n=%1 owner=%2 prefab=%3 cachedCatalog=%4 globalCatalog=%5", s_ERRStationTeardownLines, ownerName, prefab, m_EntityCatalogManager != null, globalCatalogPresent);
|
||||
detail += string.Format(" modePresent=%1 gameState=%2 errPhase=%3 endData=%4 call=OnDelete/InitValidSetup", baseMode != null, gameState, phase, hasEndData);
|
||||
detail += string.Format(" initSeen=%1 initMode=%2 initMainWorld=%3 initSiteOwned=%4 initInInventory=%5 initParentPrefab=%6", m_ERRStationInitSeen, m_ERRStationInitModePresent, m_ERRStationInitMainWorld, m_ERRStationInitSiteOwned, m_ERRStationInitInInventory, m_ERRStationInitParentPrefab);
|
||||
bool deleteMainWorld;
|
||||
bool deleteSiteOwned;
|
||||
bool deleteInInventory;
|
||||
ResourceName deleteParentPrefab;
|
||||
if (owner)
|
||||
{
|
||||
if (GetGame()) deleteMainWorld = owner.GetWorld() == GetGame().GetWorld();
|
||||
ERR_GameMode deleteMode = ERR_GameMode.Cast(baseMode);
|
||||
ERR_SiteService deleteSites;
|
||||
if (deleteMode) deleteSites = deleteMode.Sites();
|
||||
deleteSiteOwned = deleteSites && deleteSites.Owns(owner);
|
||||
InventoryItemComponent deleteItem = InventoryItemComponent.Cast(owner.FindComponent(InventoryItemComponent));
|
||||
deleteInInventory = deleteItem && deleteItem.GetParentSlot();
|
||||
IEntity deleteParent = owner.GetParent();
|
||||
if (deleteParent && deleteParent.GetPrefabData()) deleteParentPrefab = deleteParent.GetPrefabData().GetPrefabName();
|
||||
}
|
||||
detail += string.Format(" deleteMainWorld=%1 deleteSiteOwned=%2 deleteInInventory=%3 deleteParentPrefab=%4", deleteMainWorld, deleteSiteOwned, deleteInInventory, deleteParentPrefab);
|
||||
Print(detail);
|
||||
if (s_ERRStationTeardownLines == 64) Print("ERR STATION TEARDOWN DIAG: 64-line diagnostic limit reached; vanilla validation and cleanup continue unchanged.");
|
||||
}
|
||||
|
||||
// Retain every original validation result and error message.
|
||||
return super.InitValidSetup();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
#ifdef WORKBENCH
|
||||
class ERR_NativeSettingsReader : ERR_Settings
|
||||
{
|
||||
bool Probe(string path, out map<string, int> values) { return Read(path, values); }
|
||||
}
|
||||
// Runs inside the disposable native test profile. No original settings touched.
|
||||
class ERR_NativeTimeProbe
|
||||
{
|
||||
protected static float s_EngineTime;
|
||||
protected static float s_ClockHours;
|
||||
protected static int s_Multiplier;
|
||||
protected static bool s_Started;
|
||||
static bool VerifyProgression(ERR_GameMode mode)
|
||||
{
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
TimeAndWeatherManagerEntity manager = world.GetTimeAndWeatherManager();
|
||||
if (!Check(s_Started && manager && manager.GetIsDayAutoAdvanced(), "clock advancement remains enabled")) return false;
|
||||
float elapsed = manager.GetEngineTime() - s_EngineTime;
|
||||
if (!Check(elapsed >= 10, "progression sampled across at least ten engine seconds")) return false;
|
||||
float actual = manager.GetTimeOfTheDay() - s_ClockHours;
|
||||
if (actual < 0) actual += 24;
|
||||
float expected = elapsed * s_Multiplier / 3600.0;
|
||||
return Check(Math.AbsFloat(actual - expected) < 0.001, "native clock advances at configured rate without periodic resets");
|
||||
}
|
||||
protected static bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: mission time " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: mission time " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected static bool WriteFixture(string path, string omit = "", bool clock = false, float minutes = 840, bool speed = false, float multiplier = 1)
|
||||
{
|
||||
JsonSaveContext json = new JsonSaveContext();
|
||||
json.EnableTypeDiscriminator(false);
|
||||
if (omit != "reportIntervalSeconds") json.WriteValue("reportIntervalSeconds", 20);
|
||||
json.WriteValue("supportSquadLimit", 2);
|
||||
json.WriteValue("responseRadiusMetres", 1500);
|
||||
json.WriteValue("aiIntelChancePercent", 0);
|
||||
json.WriteValue("intelRevealChancePercent", 100);
|
||||
json.WriteValue("radioDefenceSeconds", 120);
|
||||
if (clock) json.WriteValue("startTimeMinutes", minutes);
|
||||
if (speed) json.WriteValue("timeMultiplier", multiplier);
|
||||
return json.SaveToFile(path);
|
||||
}
|
||||
protected static bool SettingsMigration(string path)
|
||||
{
|
||||
if (!Check(WriteFixture(path), "legacy fixture created")) return false;
|
||||
ERR_Settings legacy = new ERR_Settings(path);
|
||||
if (!Check(legacy.Get("reportIntervalSeconds") == 20 && legacy.Get("radioDefenceSeconds") == 120 && legacy.Get("aiIntelChancePercent") == 0 && legacy.Get("intelRevealChancePercent") == 100, "existing six-key settings preserved")) return false;
|
||||
if (!Check(legacy.Get("startTimeMinutes") == 840 && legacy.Get("timeMultiplier") == 1, "only absent time settings receive defaults")) return false;
|
||||
JsonLoadContext unchanged = new JsonLoadContext();
|
||||
if (!Check(unchanged.LoadFromFile(path) && !unchanged.DoesKeyExist("startTimeMinutes") && !unchanged.DoesKeyExist("timeMultiplier"), "loading does not rewrite legacy file")) return false;
|
||||
if (!Check(legacy.Set("startTimeMinutes", 375) && legacy.Set("timeMultiplier", 4), "explicit save upgrades valid legacy settings")) return false;
|
||||
ERR_Settings reloaded = new ERR_Settings(path);
|
||||
if (!Check(reloaded.Get("startTimeMinutes") == 375 && reloaded.Get("timeMultiplier") == 4 && reloaded.Get("responseRadiusMetres") == 1500 && reloaded.Get("radioDefenceSeconds") == 120, "new keys and old values survive reload")) return false;
|
||||
ERR_NativeSettingsReader reader = new ERR_NativeSettingsReader();
|
||||
map<string, int> values;
|
||||
if (!WriteFixture(path, "", true, 1440)) return false;
|
||||
if (!Check(!reader.Probe(path, values), "present out-of-range time cannot silently default")) return false;
|
||||
if (!WriteFixture(path, "", true, 375.5)) return false;
|
||||
if (!Check(!reader.Probe(path, values), "fractional clock setting rejected")) return false;
|
||||
if (!WriteFixture(path, "", false, 0, true, 0)) return false;
|
||||
if (!Check(!reader.Probe(path, values), "zero multiplier rejected")) return false;
|
||||
if (!WriteFixture(path, "reportIntervalSeconds")) return false;
|
||||
if (!Check(!reader.Probe(path, values), "missing legacy setting remains invalid")) return false;
|
||||
if (!WriteFixture(path, "", true, 375)) return false;
|
||||
if (!Check(reader.Probe(path, values) && values.Get("startTimeMinutes") == 375 && values.Get("timeMultiplier") == 1, "partially upgraded file defaults only missing new field")) return false;
|
||||
if (!WriteFixture(path, "", false, 0, true, 25)) return false;
|
||||
ERR_Settings recovered = new ERR_Settings(path);
|
||||
if (!Check(recovered.Get("startTimeMinutes") == 375 && recovered.Get("timeMultiplier") == 1 && recovered.Get("reportIntervalSeconds") == 20, "valid saved backup recovers invalid primary")) return false;
|
||||
return true;
|
||||
}
|
||||
static bool Run(ERR_GameMode mode)
|
||||
{
|
||||
int minutes;
|
||||
if (!Check(ERR_TimeService.ParseClock("00:00", minutes) && minutes == 0 && ERR_TimeService.ParseClock("23:59", minutes) && minutes == 1439 && ERR_TimeService.Clock(375) == "06:15", "clock parsing includes midnight and final minute")) return false;
|
||||
if (!Check(!ERR_TimeService.ParseClock("24:00", minutes) && !ERR_TimeService.ParseClock("12:60", minutes) && !ERR_TimeService.ParseClock("1:00", minutes) && !ERR_TimeService.ParseClock("ab:cd", minutes) && !ERR_TimeService.ParseClock("06:15junk", minutes), "malformed clock commands rejected")) return false;
|
||||
string path = "$profile:erri-native-time-probe.json";
|
||||
if (!Check(!FileIO.FileExists(path) && !FileIO.FileExists(path + ".bak") && !FileIO.FileExists(path + ".tmp"), "isolated fixture paths unused")) return false;
|
||||
bool migrated = SettingsMigration(path);
|
||||
FileIO.DeleteFile(path); FileIO.DeleteFile(path + ".bak"); FileIO.DeleteFile(path + ".tmp");
|
||||
if (!migrated) return false;
|
||||
if (!Check(mode.Time() && mode.Time().Started(), "server starts world clock before prison phase")) return false;
|
||||
ChimeraWorld world = ChimeraWorld.CastFrom(mode.GetWorld());
|
||||
TimeAndWeatherManagerEntity manager = world.GetTimeAndWeatherManager();
|
||||
if (!Check(manager && manager.GetIsDayAutoAdvanced() && Math.AbsFloat(manager.GetDayDuration() - ERR_TimeService.DayDuration(mode.Time().Multiplier())) < 0.1, "engine progression matches applied multiplier")) return false;
|
||||
int originalTime = mode.Settings().Get("startTimeMinutes");
|
||||
int originalSpeed = mode.Settings().Get("timeMultiplier");
|
||||
array<string> replies = {};
|
||||
mode.Admin().Execute(mode, false, "mission time set 06:15", replies);
|
||||
if (!Check(mode.Settings().Get("startTimeMinutes") == originalTime && replies[0] == "Administrator access required.", "unauthorized clock mutation rejected")) return false;
|
||||
mode.Admin().Execute(mode, true, "mission time set 24:00", replies);
|
||||
if (!Check(mode.Settings().Get("startTimeMinutes") == originalTime && replies[0].Contains("Invalid"), "invalid clock command preserves setting")) return false;
|
||||
float currentHours = manager.GetTimeOfTheDay();
|
||||
float currentDuration = manager.GetDayDuration();
|
||||
int appliedStart = mode.Time().StartMinutes();
|
||||
int appliedSpeed = mode.Time().Multiplier();
|
||||
mode.Admin().Execute(mode, true, "mission time set 06:15", replies);
|
||||
bool savedTime = mode.Settings().Get("startTimeMinutes") == 375 && replies[0].Contains("next round");
|
||||
mode.Admin().Execute(mode, true, "mission speed set 4", replies);
|
||||
bool savedSpeed = mode.Settings().Get("timeMultiplier") == 4 && replies[0].Contains("next round");
|
||||
bool idempotent = mode.Time().Start(mode) && Math.AbsFloat(manager.GetTimeOfTheDay() - currentHours) < 0.01 && manager.GetDayDuration() == currentDuration && mode.Time().StartMinutes() == appliedStart && mode.Time().Multiplier() == appliedSpeed;
|
||||
mode.Admin().Execute(mode, true, "mission time show", replies);
|
||||
bool shown = replies.Count() == 2 && replies[0].Contains("Clock") && replies[1] == "Next round: 06:15, 4x progression.";
|
||||
bool restoredTime = mode.Settings().Set("startTimeMinutes", originalTime);
|
||||
bool restoredSpeed = mode.Settings().Set("timeMultiplier", originalSpeed);
|
||||
if (!Check(savedTime && savedSpeed && restoredTime && restoredSpeed, "authorized settings persist and test restores originals")) return false;
|
||||
if (!Check(idempotent, "saved changes and repeated start leave current clock and speed intact")) return false;
|
||||
if (!Check(shown, "admin sees current clock separately from next round settings")) return false;
|
||||
s_EngineTime = manager.GetEngineTime();
|
||||
s_ClockHours = manager.GetTimeOfTheDay();
|
||||
s_Multiplier = mode.Time().Multiplier();
|
||||
s_Started = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
#ifdef WORKBENCH
|
||||
// Explicit destructive test of successor-owned transport in the disposable
|
||||
// scenario. Scripted occupants and engine input are not manual driving or MP.
|
||||
class ERR_NativeTransportProbe
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected ERR_SiteRuntime m_Site;
|
||||
protected Vehicle m_Vehicle;
|
||||
protected CarControllerComponent m_Controller;
|
||||
protected SCR_FuelManagerComponent m_Fuel;
|
||||
protected ref array<IEntity> m_Actors = {};
|
||||
protected BaseCompartmentSlot m_Driver;
|
||||
protected BaseCompartmentSlot m_Passenger;
|
||||
protected int m_Step;
|
||||
protected float m_Wait;
|
||||
void ERR_NativeTransportProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||
protected bool Check(bool condition, string detail)
|
||||
{
|
||||
if (condition) { Print("ERR SMOKE PASS: transport " + detail); return true; }
|
||||
Print("ERR SMOKE FAIL: transport " + detail, LogLevel.ERROR);
|
||||
return false;
|
||||
}
|
||||
protected bool DefinitionChecks()
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.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;
|
||||
ERR_SiteTransport transport = site.Transport;
|
||||
vector offset = transport.Offset;
|
||||
transport.Offset = "61 0 0";
|
||||
if (!Check(!definition.Valid(), "unbounded parking offset rejected")) return false;
|
||||
transport.Offset = "0 1 0";
|
||||
if (!Check(!definition.Valid(), "floating parking offset rejected")) return false;
|
||||
transport.Offset = offset;
|
||||
float radius = transport.ClearanceRadius;
|
||||
transport.ClearanceRadius = 1;
|
||||
if (!Check(!definition.Valid(), "undersized parking footprint rejected")) return false;
|
||||
transport.ClearanceRadius = 9;
|
||||
if (!Check(!definition.Valid(), "unbounded parking footprint rejected")) return false;
|
||||
transport.ClearanceRadius = radius;
|
||||
float range = transport.MaximumTerrainRange;
|
||||
transport.MaximumTerrainRange = 2;
|
||||
if (!Check(!definition.Valid(), "unsafe authoring slope allowance rejected")) return false;
|
||||
transport.MaximumTerrainRange = range;
|
||||
ERR_SiteDefinition radio = definition.Find("everon-radio-1");
|
||||
radio.Transport = transport;
|
||||
if (!Check(!definition.Valid(), "transport restricted to optional depot and outpost sites")) return false;
|
||||
radio.Transport = null;
|
||||
return Check(definition.Valid(), "valid authoring restored after isolated mutations");
|
||||
}
|
||||
protected IEntity Character(int index)
|
||||
{
|
||||
IEntity actor = ERR_Actors.Spawn(m_Mode.Factions().PrisonerPrefab, ERR_Actors.Ground(m_Vehicle.GetOrigin() + Vector(-8 - index * 2, 0, 0)));
|
||||
if (!actor) return null;
|
||||
m_Mode.Own(actor);
|
||||
m_Actors.Insert(actor);
|
||||
DamageManagerComponent damage = DamageManagerComponent.Cast(actor.FindComponent(DamageManagerComponent));
|
||||
if (damage) damage.EnableDamageHandling(false);
|
||||
return actor;
|
||||
}
|
||||
int Tick(float now)
|
||||
{
|
||||
if (now < m_Wait) return 0;
|
||||
if (m_Step == 0)
|
||||
{
|
||||
if (!DefinitionChecks()) return -1;
|
||||
m_Site = m_Mode.Sites().Find("everon-supply-2");
|
||||
if (!Check(m_Site && m_Site.Transport && m_Mode.Sites().TransportCount() == 1 && m_Mode.Sites().Owns(m_Site.Transport), "exactly one authored transport belongs to the finite site service")) return -1;
|
||||
m_Vehicle = m_Site.Transport;
|
||||
m_Controller = CarControllerComponent.Cast(m_Vehicle.FindComponent(CarControllerComponent));
|
||||
m_Fuel = SCR_FuelManagerComponent.Cast(m_Vehicle.FindComponent(SCR_FuelManagerComponent));
|
||||
if (!Check(m_Controller && m_Fuel && m_Fuel.HasFuel() && m_Controller.GetPersistentHandBrake(), "wheeled transport starts parked with vanilla finite fuel")) return -1;
|
||||
PrintFormat("ERR TRANSPORT FUEL: current=%1 maximum=%2", m_Fuel.GetTotalFuel(), m_Fuel.GetTotalMaxFuel());
|
||||
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
|
||||
array<BaseCompartmentSlot> seats = {};
|
||||
if (manager) manager.GetCompartments(seats);
|
||||
int usable;
|
||||
foreach (BaseCompartmentSlot slot : seats)
|
||||
{
|
||||
if (!slot) continue;
|
||||
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
|
||||
if (type == ECompartmentType.PILOT) { usable++; if (!m_Driver) m_Driver = slot; }
|
||||
if (type == ECompartmentType.CARGO) { usable++; if (!m_Passenger) m_Passenger = slot; }
|
||||
}
|
||||
PrintFormat("ERR TRANSPORT SEATS: usable driver/passenger positions=%1", usable);
|
||||
if (!Check(m_Driver && m_Passenger && m_Driver != m_Passenger, "native driver and passenger compartments are distinct")) return -1;
|
||||
DamageManagerComponent.Cast(m_Vehicle.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||
IEntity driver = Character(0);
|
||||
IEntity passenger = Character(1);
|
||||
if (!Check(driver && passenger, "isolated physical occupants created")) return -1;
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(driver.FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(access && access.GetInVehicle(m_Vehicle, m_Driver, true, -1, ECloseDoorAfterActions.INVALID, false), "native driver boarding request accepted")) return -1;
|
||||
m_Step = 1;
|
||||
m_Wait = now + 1500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 1)
|
||||
{
|
||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(m_Actors[1].FindComponent(CompartmentAccessComponent));
|
||||
if (!Check(access && access.GetInVehicle(m_Vehicle, m_Passenger, true, -1, ECloseDoorAfterActions.INVALID, false), "native passenger boarding request accepted")) return -1;
|
||||
m_Step = 2;
|
||||
m_Wait = now + 2500;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 2)
|
||||
{
|
||||
if (!Check(m_Driver.GetOccupant() == m_Actors[0] && m_Passenger.GetOccupant() == m_Actors[1] && CompartmentAccessComponent.GetVehicleIn(m_Actors[0]) == m_Vehicle && CompartmentAccessComponent.GetVehicleIn(m_Actors[1]) == m_Vehicle, "driver and passenger physically occupy their native seats")) return -1;
|
||||
if (!Check(m_Controller.IsEngineOn() || m_Controller.StartEngine(), "ordinary native engine start request accepted")) return -1;
|
||||
m_Step = 3;
|
||||
m_Wait = now + 6000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 3)
|
||||
{
|
||||
if (!Check(m_Controller.IsEngineOn(), "native wheeled engine starts with available fuel")) return -1;
|
||||
m_Fuel.SetTotalFuelPercentage(0);
|
||||
if (!Check(m_Mode.Sites().Prepare() == ERR_PrepareResult.READY && m_Site.Transport == m_Vehicle, "preparation queries preserve the original vehicle after deliberate fuel depletion")) return -1;
|
||||
m_Step = 4;
|
||||
m_Wait = now + 5000;
|
||||
return 0;
|
||||
}
|
||||
if (m_Step == 4)
|
||||
{
|
||||
if (!Check(!m_Fuel.HasFuel() && m_Fuel.GetTotalFuel() <= 0.001 && m_Mode.Sites().Prepare() == ERR_PrepareResult.READY && m_Mode.Sites().TransportCount() == 1 && m_Site.Transport == m_Vehicle, "empty transport stays empty and is not refilled or replaced")) return -1;
|
||||
foreach (IEntity actor : m_Actors) ERR_Actors.DeleteSpawned(actor);
|
||||
m_Actors.Clear();
|
||||
ERR_Actors.DeleteSpawned(m_Vehicle);
|
||||
m_Step = 5;
|
||||
m_Wait = now + 3000;
|
||||
return 0;
|
||||
}
|
||||
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"));
|
||||
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;
|
||||
m_Step = 6;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#ifdef WORKBENCH
|
||||
// Authoring-only survey: omit transport in the fixture's in-memory definition so
|
||||
// an invalid parking candidate cannot block inspecting nearby terrain/scenery.
|
||||
modded class ERR_SiteService
|
||||
{
|
||||
override bool Begin(ERR_ScenarioDefinition definition, ResourceName fieldMap)
|
||||
{
|
||||
string outcome;
|
||||
if (definition && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "transport-survey")
|
||||
foreach (ERR_SiteDefinition site : definition.Sites) site.Transport = null;
|
||||
return super.Begin(definition, fieldMap);
|
||||
}
|
||||
}
|
||||
class ERR_NativeTransportSurvey
|
||||
{
|
||||
static void Run(ERR_GameMode mode)
|
||||
{
|
||||
ERR_ScenarioDefinition definition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab("{BC310DC0CD27F523}Configs/ERR/EveronSites.conf"));
|
||||
ERR_SiteDefinition site = definition.Find("everon-supply-2");
|
||||
int clear;
|
||||
for (int x = -40; x <= 40; x += 8)
|
||||
for (int z = -40; z <= 40; z += 8)
|
||||
{
|
||||
vector offset = Vector(x, 0, z);
|
||||
if (offset.Length() < 8) continue;
|
||||
vector position = site.Position + offset;
|
||||
if (!mode.Sites().TransportClearance(position, site.Transport.ClearanceRadius, site.Transport.MaximumTerrainRange)) continue;
|
||||
clear++;
|
||||
PrintFormat("ERR PARKING CANDIDATE: offset=%1 position=%2", offset, ERR_Actors.Ground(position));
|
||||
}
|
||||
PrintFormat("ERR PARKING SURVEY: clear=%1; no scenery removed and no transport spawned", clear);
|
||||
Print("ERR SMOKE PASS: transport parking survey completed");
|
||||
Print("ERR SMOKE COMPLETE: bounded transport parking survey");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user