Advance standalone admission, finite artillery and native validation
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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
+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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
} }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
class ERR_UnlockPrisonAction : ScriptedUserAction
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Phase() == ERR_Phase.PRISON && ERR_Actors.CanFight(user);
|
||||
}
|
||||
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
|
||||
{
|
||||
override bool CanBePerformedScript(IEntity user)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
return mode && mode.Phase() > ERR_Phase.PRISON && 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));
|
||||
return mode && mode.Phase() == ERR_Phase.RADIO_SEARCH && location && location.Knowledge() == 2 && ERR_Actors.CanFight(user);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
+305
@@ -0,0 +1,305 @@
|
||||
// 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 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; }
|
||||
// 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);
|
||||
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);
|
||||
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();
|
||||
while (!m_Pending.IsEmpty()) Resolve(m_Pending[m_Pending.Count() - 1], false);
|
||||
}
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// 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 SCR_MapMarkerBase 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;
|
||||
if (!m_ERRMarker)
|
||||
{
|
||||
m_ERRMarker = new SCR_MapMarkerBase();
|
||||
m_ERRMarker.SetType(SCR_EMapMarkerType.PLACED_CUSTOM);
|
||||
vector p = Position();
|
||||
m_ERRMarker.SetWorldPos(Math.Round(p[0]), Math.Round(p[2]));
|
||||
m_ERRMarker.SetMarkerConfigID(0);
|
||||
manager.InsertStaticMarker(m_ERRMarker, true);
|
||||
}
|
||||
string label = "Unknown location";
|
||||
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();
|
||||
}
|
||||
m_ERRMarker.SetCustomText(label);
|
||||
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 (manager && m_ERRMarker) manager.RemoveStaticMarker(m_ERRMarker);
|
||||
m_ERRMarker = null;
|
||||
super.OnDelete(owner);
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// 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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
+419
@@ -0,0 +1,419 @@
|
||||
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;
|
||||
[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; }
|
||||
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() || !ERR_Actors.Player(player) || !ERR_Actors.CanFight(player)) 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_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);
|
||||
}
|
||||
}
|
||||
+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
|
||||
+738
@@ -0,0 +1,738 @@
|
||||
// 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_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 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();
|
||||
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()
|
||||
{
|
||||
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_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 == "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 (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 == 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_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;
|
||||
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(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
|
||||
+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
|
||||
+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
|
||||
+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
|
||||
+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
|
||||
+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
|
||||
+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();
|
||||
}
|
||||
SCR_EntityCatalogManagerComponent catalog = SCR_EntityCatalogManagerComponent.GetInstance();
|
||||
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, catalog != null);
|
||||
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
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Native task descriptions otherwise remain stale while their entry is open.
|
||||
// Own only this scenario's additional binding and missing native detach cleanup.
|
||||
modded class SCR_TaskListEntryDescriptionUIComponent
|
||||
{
|
||||
protected SCR_Task m_ERRObservedObjective;
|
||||
// Entity references become null when a task is deleted; work ownership must survive.
|
||||
protected bool m_ERROwnsObjectiveBinding;
|
||||
override void InitDescription(notnull SCR_Task task)
|
||||
{
|
||||
// Native InitDescription deletes assignee labels; cancel their delayed work first.
|
||||
ERR_UnbindObjectiveDetails();
|
||||
super.InitDescription(task);
|
||||
if (!ERR_GameMode.Get() || !task.GetTaskID().StartsWith("erri-mission-")) return;
|
||||
m_ERRObservedObjective = task;
|
||||
m_ERROwnsObjectiveBinding = true;
|
||||
task.GetOnDisplayDataChanged().Insert(ERR_RefreshObjectiveDetails);
|
||||
}
|
||||
protected void ERR_RefreshObjectiveDetails()
|
||||
{
|
||||
if (m_wRoot && m_Task && m_Task == m_ERRObservedObjective) UpdateTask();
|
||||
}
|
||||
protected override void UpdateAssignees(notnull SCR_Task task, SCR_TaskExecutor newAssignee = null, int requesterID = 0)
|
||||
{
|
||||
// Native assignment refresh also deletes the old labels before rebuilding them.
|
||||
if (m_ERROwnsObjectiveBinding && task == m_Task)
|
||||
ERR_CancelObjectiveOverflow();
|
||||
super.UpdateAssignees(task, newAssignee, requesterID);
|
||||
}
|
||||
protected void ERR_CancelObjectiveOverflow()
|
||||
{
|
||||
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(CheckOverflow);
|
||||
}
|
||||
protected void ERR_UnbindObjectiveDetails()
|
||||
{
|
||||
if (!m_ERROwnsObjectiveBinding) return;
|
||||
ERR_CancelObjectiveOverflow();
|
||||
if (m_ERRObservedObjective) m_ERRObservedObjective.GetOnDisplayDataChanged().Remove(ERR_RefreshObjectiveDetails);
|
||||
m_ERRObservedObjective = null;
|
||||
m_ERROwnsObjectiveBinding = false;
|
||||
}
|
||||
override void HandlerDeattached(Widget w)
|
||||
{
|
||||
// Vanilla detaches widget/manager listeners but leaves these task listeners.
|
||||
// Preserve that vanilla path for every task outside the successor scenario.
|
||||
if (m_ERROwnsObjectiveBinding)
|
||||
{
|
||||
// These five native invokers are static and survive task entity deletion.
|
||||
SCR_Task.GetOnTaskAssigneeAdded().Remove(UpdateAssignees);
|
||||
SCR_Task.GetOnTaskAssigneeRemoved().Remove(UpdateAssignees);
|
||||
SCR_Task.GetOnTaskUIVisibilityChanged().Remove(OnTaskUIVisibilityChanged);
|
||||
SCR_Task.GetOnTaskVisibilityChanged().Remove(OnTaskVisibilityChanged);
|
||||
SCR_Task.GetOnTaskOwnershipChanged().Remove(OnTaskOwnershipChanged);
|
||||
if (m_TaskComponent) m_TaskComponent.GetOnTaskVisualChanged().Remove(OnTaskVisualChanged);
|
||||
m_Task = null;
|
||||
}
|
||||
ERR_UnbindObjectiveDetails();
|
||||
super.HandlerDeattached(w);
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
// Shared player-facing copy; reads replicated mission state and never changes it.
|
||||
class ERR_ObjectiveText
|
||||
{
|
||||
static string Title(ERR_Phase phase)
|
||||
{
|
||||
if (phase == ERR_Phase.PRISON) return "Escape the prison";
|
||||
if (phase == ERR_Phase.RADIO_SEARCH) return "Locate the radio station";
|
||||
if (phase == ERR_Phase.RADIO_DEFENCE) return "Defend the radio transmission";
|
||||
if (phase == ERR_Phase.EXTRACTION) return "Escape Everon by helicopter";
|
||||
return "Escape Everon";
|
||||
}
|
||||
static string Extraction(float radius)
|
||||
{
|
||||
return string.Format("The map marks the initial helicopter pickup site. Board the helicopter and fly beyond %1 kilometres from the prison. Living prisoners aboard escape, including unconscious passengers. Dead or absent prisoners do not.", radius / 1000.0);
|
||||
}
|
||||
static string RadioFeedbackMessage(ERR_RadioFeedback feedback)
|
||||
{
|
||||
if (feedback == ERR_RadioFeedback.WAITING)
|
||||
return "Waiting for a conscious defender near the radio. Keep someone within 60 metres to start the transmission.";
|
||||
if (feedback == ERR_RadioFeedback.TRANSMITTING)
|
||||
return "Transmission in progress. Keep a conscious survivor within 60 metres of the radio. Leaving the area resets progress.";
|
||||
if (feedback == ERR_RadioFeedback.INTERRUPTED)
|
||||
return "Transmission interrupted. Progress has reset. Return a conscious survivor to within 60 metres of the radio to resume.";
|
||||
return "Keep a conscious survivor within 60 metres of the radio until the transmission finishes. Leaving the area resets the defence timer.";
|
||||
}
|
||||
static string Description(ERR_GameMode mode, bool progress = true)
|
||||
{
|
||||
string description = PhaseDescription(mode, progress);
|
||||
if (mode && mode.Active() && !mode.RecoveryStatus().IsEmpty()) description += "\n" + mode.RecoveryStatus();
|
||||
return description;
|
||||
}
|
||||
protected static string PhaseDescription(ERR_GameMode mode, bool progress)
|
||||
{
|
||||
if (!mode) return "";
|
||||
ERR_Phase phase = mode.Phase();
|
||||
if (phase == ERR_Phase.PREPARING)
|
||||
{
|
||||
if (!mode.PreparationError().IsEmpty()) return "The mission could not start. An administrator can use /esc status for the cause. Spawning is disabled until the configuration is corrected and the scenario restarted.";
|
||||
return "Preparing mission sites and equipment. You will spawn when the mission is ready.";
|
||||
}
|
||||
if (phase == ERR_Phase.PRISON)
|
||||
return "Search the supply box inside the prison for weapons and a field map. Pick the gate lock, then get clear of the guards.";
|
||||
if (phase == ERR_Phase.RADIO_SEARCH)
|
||||
{
|
||||
array<ERR_LocationComponent> locations = {};
|
||||
mode.Locations(locations);
|
||||
foreach (ERR_LocationComponent location : locations)
|
||||
if (location.Kind() == ERR_LocationKind.RADIO && location.Knowledge() == 2)
|
||||
return "The radio station is identified on your map. Reach the station and tune its radio to start the transmission.";
|
||||
return "Choose either marked supply cache. Each has equipment and enough radio documents to identify the station. If one cache is lost, use the other. If every document is lost, a close approach with a clear view can identify the station itself. Nearby medical and guard sites can be discovered by approaching with a clear view.";
|
||||
}
|
||||
if (phase == ERR_Phase.RADIO_DEFENCE)
|
||||
{
|
||||
if (!progress) return RadioFeedbackMessage(mode.RadioFeedback());
|
||||
int seconds = mode.DefenceSecondsRemaining();
|
||||
if (!mode.DefenceOccupied())
|
||||
return string.Format("Transmission interrupted. Keep a conscious survivor within 60 metres of the radio for %1 seconds. Leaving resets progress.", seconds);
|
||||
int displayed = Math.Ceil(seconds / 5.0) * 5;
|
||||
return string.Format("Transmission in progress: up to %1 seconds remaining. Keep a conscious survivor within 60 metres of the radio. Leaving resets progress.", displayed);
|
||||
}
|
||||
if (phase == ERR_Phase.EXTRACTION) return Extraction(mode.EscapeRadius());
|
||||
return "";
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
// Server-owned presentation adapter: mission rules decide progress, native tasks
|
||||
// replicate it. Companion addons neither create nor advance these objectives.
|
||||
class ERR_Objectives
|
||||
{
|
||||
protected ERR_GameMode m_Mode;
|
||||
protected SCR_Task m_Current;
|
||||
protected string m_LastDescription;
|
||||
protected bool m_Stopped;
|
||||
|
||||
void ERR_Objectives(ERR_GameMode mode) { m_Mode = mode; }
|
||||
SCR_Task Current() { return m_Current; }
|
||||
static string Id(ERR_Phase phase) { return "erri-mission-" + phase.ToString(); }
|
||||
|
||||
void PhaseChanged(ERR_Phase previous, ERR_Phase current)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped) return;
|
||||
SCR_TaskSystem tasks = SCR_TaskSystem.GetInstance();
|
||||
if (!tasks) { Print("Rereintroduced: native task system missing", LogLevel.ERROR); return; }
|
||||
if (m_Current)
|
||||
{
|
||||
SCR_ETaskState result = SCR_ETaskState.COMPLETED;
|
||||
if (current == ERR_Phase.DEFEAT) result = SCR_ETaskState.FAILED;
|
||||
tasks.SetTaskState(m_Current, result);
|
||||
}
|
||||
m_Current = null;
|
||||
if (ERR_MissionRules.Terminal(current)) return;
|
||||
string title = ERR_ObjectiveText.Title(current);
|
||||
if (title.IsEmpty()) return;
|
||||
m_Current = tasks.CreateTask("{1D0F815858EE24AD}Prefabs/Tasks/BaseTask.et", Id(current), title, Description());
|
||||
if (!m_Current) { Print("Rereintroduced: objective creation failed", LogLevel.ERROR); return; }
|
||||
// Location markers use the discovery registry. A task cannot expose an unknown site.
|
||||
tasks.SetTaskUIVisibility(m_Current, SCR_ETaskUIVisibility.LIST_ONLY);
|
||||
m_Mode.Own(m_Current);
|
||||
m_LastDescription = "";
|
||||
Tick(GetGame().GetWorld().GetWorldTime());
|
||||
}
|
||||
|
||||
protected string Description() { return ERR_ObjectiveText.Description(m_Mode); }
|
||||
|
||||
void Tick(float now)
|
||||
{
|
||||
if (!Replication.IsServer() || m_Stopped || !m_Current) return;
|
||||
string description = Description();
|
||||
if (description != m_LastDescription)
|
||||
{
|
||||
m_Current.SetTaskDescription(description);
|
||||
m_LastDescription = description;
|
||||
}
|
||||
SCR_TaskSystem tasks = SCR_TaskSystem.GetInstance();
|
||||
if (!tasks) return;
|
||||
array<int> players = {};
|
||||
GetGame().GetPlayerManager().GetPlayers(players);
|
||||
foreach (int playerId : players)
|
||||
{
|
||||
SCR_TaskExecutor executor = SCR_TaskSystem.TaskExecutorFromPlayerID(playerId);
|
||||
if (executor && !m_Current.IsTaskAssignedTo(executor)) tasks.AssignTask(m_Current, executor, true);
|
||||
}
|
||||
}
|
||||
void Stop() { m_Stopped = true; }
|
||||
}
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
enum ERR_ObserverReason
|
||||
{
|
||||
NONE,
|
||||
SPENT_LIFE,
|
||||
LATE_EXTRACTION,
|
||||
SPAWN_UNAVAILABLE
|
||||
}
|
||||
|
||||
// A local follow camera. It never possesses its target or offers free flight.
|
||||
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
||||
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||
{
|
||||
override bool EOnCameraInit() { return true; }
|
||||
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
||||
{
|
||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||
if (!controller) return;
|
||||
IEntity target = controller.ERR_ObservedEntity();
|
||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||
vector position = aim + "0 4 -6";
|
||||
IEntity exclude = target;
|
||||
if (target)
|
||||
{
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
|
||||
vector transform[4];
|
||||
if (vehicle)
|
||||
{
|
||||
vehicle.GetWorldTransform(transform);
|
||||
vector lower, upper;
|
||||
vehicle.GetWorldBounds(lower, upper);
|
||||
aim = (lower + upper) * 0.5;
|
||||
position = aim + "0 5 0" - transform[2] * 10;
|
||||
exclude = vehicle;
|
||||
}
|
||||
else
|
||||
{
|
||||
target.GetWorldTransform(transform);
|
||||
aim = transform[3] + "0 1.5 0";
|
||||
position = aim + "0 1.5 0" - transform[2] * 4;
|
||||
}
|
||||
}
|
||||
TraceParam trace = new TraceParam();
|
||||
trace.Start = aim;
|
||||
trace.End = position;
|
||||
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
|
||||
trace.LayerMask = TRACE_LAYER_CAMERA;
|
||||
trace.Exclude = exclude;
|
||||
float fraction = GetCameraEntity().GetWorld().TraceMove(trace, null);
|
||||
if (fraction < 1) position = vector.Lerp(aim, position, Math.Max(0.05, fraction - 0.05));
|
||||
Math3D.DirectionAndUpMatrix(vector.Direction(position, aim), "0 1 0", param.transform);
|
||||
param.transform[3] = position;
|
||||
param.isDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
[RplProp()]
|
||||
protected ERR_ObserverReason m_ERRObserverReason;
|
||||
[RplProp()]
|
||||
protected RplId m_ERRObservedId = RplId.Invalid();
|
||||
[RplProp()]
|
||||
protected vector m_ERRObserverAnchor;
|
||||
protected IEntity m_ERRServerObserved;
|
||||
protected IEntity m_ERRServerObservedVehicle;
|
||||
protected SCR_ManualCamera m_ERRObserverCamera;
|
||||
protected bool m_ERRObserverMonitor;
|
||||
protected bool m_ERRObserverReady;
|
||||
protected float m_ERRLastCycle = -1000;
|
||||
|
||||
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
||||
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
||||
IEntity ERR_ObservedEntity()
|
||||
{
|
||||
RplComponent component = RplComponent.Cast(Replication.FindItem(m_ERRObservedId));
|
||||
if (!component) return null;
|
||||
return component.GetEntity();
|
||||
}
|
||||
bool ERR_HasObserverCamera() { return m_ERRObserverCamera != null; }
|
||||
|
||||
void ERR_SetObserver(ERR_ObserverReason reason)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity())) return;
|
||||
m_ERRObserverReason = reason;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
|
||||
if (reason == ERR_ObserverReason.NONE) ERR_ReleaseObserved();
|
||||
Replication.BumpMe();
|
||||
ERR_UpdateObserverServer();
|
||||
}
|
||||
|
||||
protected void ERR_StreamObserved(IEntity entity, bool normalStreaming)
|
||||
{
|
||||
RplIdentity identity = GetRplIdentity();
|
||||
if (!identity.IsValid() || identity == RplIdentity.Local() || !entity) return;
|
||||
RplComponent component = RplComponent.Cast(entity.FindComponent(RplComponent));
|
||||
if (component) component.EnableStreamingConNode(identity, normalStreaming);
|
||||
}
|
||||
protected void ERR_ReleaseObserved()
|
||||
{
|
||||
ERR_StreamObserved(m_ERRServerObserved, true);
|
||||
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
|
||||
m_ERRServerObserved = null;
|
||||
m_ERRServerObservedVehicle = null;
|
||||
m_ERRObservedId = RplId.Invalid();
|
||||
}
|
||||
void ERR_UpdateObserverServer(int direction = 0)
|
||||
{
|
||||
if (!Replication.IsServer() || m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active() || ERR_Actors.Alive(GetControlledEntity()))
|
||||
{
|
||||
m_ERRObserverReason = ERR_ObserverReason.NONE;
|
||||
ERR_ReleaseObserved();
|
||||
Replication.BumpMe();
|
||||
return;
|
||||
}
|
||||
array<IEntity> survivors = {};
|
||||
ERR_Actors.Players(survivors);
|
||||
Faction friendly = mode.PlayerFaction();
|
||||
for (int i = survivors.Count() - 1; i >= 0; i--)
|
||||
if (SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||
int index = survivors.Find(m_ERRServerObserved);
|
||||
if (index < 0) index = 0;
|
||||
else index += direction;
|
||||
if (index < 0) index = survivors.Count() - 1;
|
||||
if (index >= survivors.Count()) index = 0;
|
||||
IEntity target;
|
||||
if (!survivors.IsEmpty()) target = survivors[index];
|
||||
if (target != m_ERRServerObserved)
|
||||
{
|
||||
ERR_ReleaseObserved();
|
||||
m_ERRServerObserved = target;
|
||||
if (target)
|
||||
{
|
||||
RplComponent component = RplComponent.Cast(target.FindComponent(RplComponent));
|
||||
if (component) m_ERRObservedId = component.Id();
|
||||
ERR_StreamObserved(target, false);
|
||||
}
|
||||
}
|
||||
if (target)
|
||||
{
|
||||
m_ERRObserverAnchor = target.GetOrigin();
|
||||
IEntity vehicle = CompartmentAccessComponent.GetVehicleIn(target);
|
||||
if (vehicle != m_ERRServerObservedVehicle)
|
||||
{
|
||||
ERR_StreamObserved(m_ERRServerObservedVehicle, true);
|
||||
m_ERRServerObservedVehicle = vehicle;
|
||||
ERR_StreamObserved(vehicle, false);
|
||||
}
|
||||
}
|
||||
Replication.BumpMe();
|
||||
}
|
||||
void ERR_CycleObserver(int direction)
|
||||
{
|
||||
if (this == GetGame().GetPlayerController()) Rpc(ERR_RequestObserverCycle, direction);
|
||||
}
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_RequestObserverCycle(int direction)
|
||||
{
|
||||
if (!Replication.IsServer() || (direction != -1 && direction != 1)) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (now - m_ERRLastCycle < 250) return;
|
||||
m_ERRLastCycle = now;
|
||||
ERR_UpdateObserverServer(direction);
|
||||
}
|
||||
|
||||
void ERR_StartObserverMonitor()
|
||||
{
|
||||
if (this != GetGame().GetPlayerController() || System.IsConsoleApp() || m_ERRObserverMonitor) return;
|
||||
m_ERRObserverMonitor = true;
|
||||
ERR_OnObserverMonitorStarted();
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateObserverClient, 250, true);
|
||||
}
|
||||
protected void ERR_OnObserverMonitorStarted() {}
|
||||
protected void ERR_UpdateObserverClient()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
bool observe = mode && mode.Active() && m_ERRObserverReason != ERR_ObserverReason.NONE && !ERR_Actors.Alive(GetControlledEntity());
|
||||
if (!observe)
|
||||
{
|
||||
ERR_CloseObserverCamera();
|
||||
return;
|
||||
}
|
||||
if (!m_ERRObserverCamera)
|
||||
{
|
||||
Resource resource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
|
||||
if (!resource || !resource.IsValid()) return;
|
||||
EntitySpawnParams params = new EntitySpawnParams();
|
||||
params.TransformMode = ETransformMode.WORLD;
|
||||
params.Transform[3] = m_ERRObserverAnchor + "0 5 -6";
|
||||
m_ERRObserverCamera = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefab(resource, GetGame().GetWorld(), params));
|
||||
if (!m_ERRObserverCamera) return;
|
||||
}
|
||||
CameraManager manager = GetGame().GetCameraManager();
|
||||
if (!manager || manager.CurrentCamera() != m_ERRObserverCamera) return;
|
||||
SCR_RespawnSystemComponent respawn = SCR_RespawnSystemComponent.GetInstance();
|
||||
if (respawn) respawn.DestroyLoadingPlaceholder();
|
||||
if (!m_ERRObserverReady)
|
||||
{
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (hints) hints.ShowCustom("You are spectating the surviving prisoners until the next round.", "Spectating", 12);
|
||||
m_ERRObserverReady = true;
|
||||
}
|
||||
}
|
||||
protected void ERR_CloseObserverCamera()
|
||||
{
|
||||
m_ERRObserverReady = false;
|
||||
if (!m_ERRObserverCamera) return;
|
||||
if (GetGame().GetPlayerController() == this) m_ERRObserverCamera.TrySwitchToControlledEntityCamera();
|
||||
m_ERRObserverCamera.Terminate();
|
||||
delete m_ERRObserverCamera;
|
||||
m_ERRObserverCamera = null;
|
||||
}
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame())
|
||||
{
|
||||
GetGame().GetCallqueue().Remove(ERR_UpdateObserverClient);
|
||||
ERR_CloseObserverCamera();
|
||||
if (Replication.IsServer()) ERR_ReleaseObserved();
|
||||
}
|
||||
}
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
// Explicit local diagnostic launcher. Normal games and Workbench never request
|
||||
// transitions from this file. No player, identity or audit state is fabricated.
|
||||
#ifndef WORKBENCH
|
||||
// Native Game is not Managed; this script-owned helper owns the callback.
|
||||
class ERR_OrdinaryScenarioLaunchRequest : Managed
|
||||
{
|
||||
protected string m_Project;
|
||||
protected ResourceName m_Header;
|
||||
protected string m_Addon;
|
||||
protected bool m_Queued;
|
||||
protected bool m_Finished;
|
||||
|
||||
void Queue(string project, ResourceName header, string addon)
|
||||
{
|
||||
if (m_Queued || m_Finished) return;
|
||||
m_Queued = true;
|
||||
m_Project = project;
|
||||
m_Header = header;
|
||||
m_Addon = addon;
|
||||
GetGame().GetCallqueue().CallLater(Attempt, 0, false);
|
||||
}
|
||||
|
||||
void Cancel()
|
||||
{
|
||||
m_Finished = true;
|
||||
if (GetGame() && GetGame().GetCallqueue()) GetGame().GetCallqueue().Remove(Attempt);
|
||||
}
|
||||
|
||||
protected void Attempt()
|
||||
{
|
||||
if (m_Finished) return;
|
||||
m_Finished = true;
|
||||
if (!GameStateTransitions.IsInsideMainMenu() || ERR_GameMode.Get() || GameStateTransitions.IsTransitionRequestedOrInProgress())
|
||||
{
|
||||
Print("ERR ORDINARY: header launch refused outside idle main menu", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
string addons;
|
||||
if (!System.GetCLIParam("addons", addons) || addons != m_Addon)
|
||||
{
|
||||
Print("ERR ORDINARY: header launch addon selection mismatch", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
ResourceName expected = m_Header;
|
||||
MissionHeader header = MissionHeader.ReadMissionHeader(expected);
|
||||
if (!header || header.GetWorldResourceName() != "{B6C516F86302C679}Worlds/ERR_Everon.ent" || header.GetWorldSystemsConfig() != "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf")
|
||||
{
|
||||
Print("ERR ORDINARY: diagnostic mission header is invalid", LogLevel.ERROR);
|
||||
return;
|
||||
}
|
||||
PrintFormat("ERR ORDINARY: requesting header project=%1", m_Project);
|
||||
if (!GameStateTransitions.RequestScenarioChangeTransition(expected, string.Empty, addons))
|
||||
Print("ERR ORDINARY: native header transition refused", LogLevel.ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
modded class ArmaReforgerScripted
|
||||
{
|
||||
protected ref ERR_OrdinaryScenarioLaunchRequest m_ERRDiagnosticLaunchRequest;
|
||||
protected bool m_ERRDiagnosticLaunchAttempted;
|
||||
protected string m_ERRDiagnosticProject;
|
||||
|
||||
protected ResourceName ERR_DiagnosticHeader(string project)
|
||||
{
|
||||
if (project == "core") return "{AD62322D5213C440}Missions/ERR_Everon_Base.conf";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
protected string ERR_DiagnosticAddon(string project)
|
||||
{
|
||||
if (project == "core") return "2528466DE220D91C";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
override bool OnGameStart()
|
||||
{
|
||||
bool started = super.OnGameStart();
|
||||
if (!started || !System.GetCLIParam("errOrdinaryScenario", m_ERRDiagnosticProject)) return started;
|
||||
ResourceName expected = ERR_DiagnosticHeader(m_ERRDiagnosticProject);
|
||||
if (expected.IsEmpty())
|
||||
{
|
||||
Print("ERR ORDINARY: invalid diagnostic project", LogLevel.ERROR);
|
||||
return started;
|
||||
}
|
||||
// A loaded successor is inspected only; never replace an active mission.
|
||||
if (ERR_GameMode.Get())
|
||||
{
|
||||
MissionHeader active = GetMissionHeader();
|
||||
ResourceName systems = "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf";
|
||||
if (!active || active.GetHeaderResourceName() != expected || active.GetWorldSystemsConfig() != systems || GetSystemsConfig() != systems)
|
||||
Print("ERR ORDINARY: active scenario header/systems mismatch", LogLevel.ERROR);
|
||||
else
|
||||
PrintFormat("ERR ORDINARY: header verified project=%1 systems=MissionSystems", m_ERRDiagnosticProject);
|
||||
return started;
|
||||
}
|
||||
if (m_ERRDiagnosticLaunchAttempted) return started;
|
||||
m_ERRDiagnosticLaunchAttempted = true;
|
||||
// OnGameStart runs while the native state is finishing initialization. The
|
||||
// one queued attempt requires that it actually reached the main menu.
|
||||
m_ERRDiagnosticLaunchRequest = new ERR_OrdinaryScenarioLaunchRequest();
|
||||
m_ERRDiagnosticLaunchRequest.Queue(m_ERRDiagnosticProject, expected, ERR_DiagnosticAddon(m_ERRDiagnosticProject));
|
||||
return started;
|
||||
}
|
||||
|
||||
|
||||
override protected void OnBeforeWorldCleanup()
|
||||
{
|
||||
if (m_ERRDiagnosticLaunchRequest) m_ERRDiagnosticLaunchRequest.Cancel();
|
||||
m_ERRDiagnosticLaunchRequest = null;
|
||||
super.OnBeforeWorldCleanup();
|
||||
}
|
||||
}
|
||||
|
||||
// Observe normal finalized admission; this never creates a body or identity.
|
||||
modded class ERR_GameMode
|
||||
{
|
||||
override void PlayerSpawned(int id, IEntity actor)
|
||||
{
|
||||
super.PlayerSpawned(id, actor);
|
||||
string project;
|
||||
if (!System.GetCLIParam("errOrdinaryScenario", project) || (project != "core" && project != "chat")) return;
|
||||
PlayerController local = GetGame().GetPlayerController();
|
||||
if (!local || local.GetPlayerId() != id || local.GetControlledEntity() != actor || !ERR_Actors.Alive(actor)) return;
|
||||
if (!Players().CurrentActor(actor) || Phase() != ERR_Phase.PRISON) return;
|
||||
PrintFormat("ERR ORDINARY: local participant verified project=%1 phase=prison", project);
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected ChatCommandInvoker m_ERRCommand;
|
||||
protected float m_ERRLastRequest = -1000;
|
||||
protected int m_ERRRegisterAttempts;
|
||||
|
||||
override void OnControlledEntityChanged(IEntity from, IEntity to)
|
||||
{
|
||||
super.OnControlledEntityChanged(from, to);
|
||||
if (this == GetGame().GetPlayerController() && ERR_GameMode.Get()) ERR_Register();
|
||||
}
|
||||
|
||||
protected void ERR_Register()
|
||||
{
|
||||
if (m_ERRCommand || m_ERRRegisterAttempts++ > 40) return;
|
||||
SCR_ChatPanelManager manager = SCR_ChatPanelManager.GetInstance();
|
||||
if (!manager) { GetGame().GetCallqueue().CallLater(ERR_Register, 250, false); return; }
|
||||
m_ERRCommand = manager.GetCommandInvoker("esc");
|
||||
if (m_ERRCommand) m_ERRCommand.Insert(ERR_OnCommand);
|
||||
}
|
||||
protected override void ERR_OnObserverMonitorStarted()
|
||||
{
|
||||
super.ERR_OnObserverMonitorStarted();
|
||||
ERR_Register();
|
||||
}
|
||||
protected void ERR_OnCommand(SCR_ChatPanel panel, string arguments)
|
||||
{
|
||||
string localCommand = arguments.Trim(); localCommand.ToLower();
|
||||
if (localCommand == "spectate next" || localCommand == "spectate previous")
|
||||
{
|
||||
int direction = 1;
|
||||
if (localCommand == "spectate previous") direction = -1;
|
||||
ERR_CycleObserver(direction);
|
||||
return;
|
||||
}
|
||||
Rpc(ERR_Request, arguments);
|
||||
}
|
||||
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Server)]
|
||||
protected void ERR_Request(string arguments)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!Replication.IsServer() || !mode) return;
|
||||
float now = GetGame().GetWorld().GetWorldTime();
|
||||
if (arguments.Length() > 160 || now - m_ERRLastRequest < 250) return;
|
||||
m_ERRLastRequest = now;
|
||||
arguments = arguments.Trim(); arguments.ToLower();
|
||||
array<string> replies = {};
|
||||
mode.Admin().Execute(mode, SCR_Global.IsAdmin(GetPlayerId()), arguments, replies);
|
||||
foreach (string reply : replies) ERR_Reply(reply);
|
||||
}
|
||||
protected void ERR_Reply(string text) { Rpc(ERR_Response, text); }
|
||||
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||
protected void ERR_Response(string text) { SCR_ChatComponent.RadioProtocolMessage(text); }
|
||||
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_Register);
|
||||
if (m_ERRCommand) m_ERRCommand.Remove(ERR_OnCommand);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
enum ERR_PersonalEscapeResult { UNKNOWN, ESCAPED, MISSED }
|
||||
|
||||
// Only the server resolves account identities. Clients receive their controller's
|
||||
// result, never a persistent account ID or a reused end-data connection ID.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
[RplProp(onRplName: "ERR_OnEscapeResultReplicated")]
|
||||
protected ERR_PersonalEscapeResult m_ERREscapeResult;
|
||||
[RplProp(onRplName: "ERR_OnEscapeResultReplicated")]
|
||||
protected RplId m_ERREscapeResultMission = RplId.Invalid();
|
||||
protected static ref ScriptInvoker s_ERREscapeResultChanged = new ScriptInvoker();
|
||||
|
||||
static ScriptInvoker ERR_OnEscapeResultChanged() { return s_ERREscapeResultChanged; }
|
||||
protected RplId ERR_CurrentResultMission()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode) return RplId.Invalid();
|
||||
RplComponent component = RplComponent.Cast(mode.FindComponent(RplComponent));
|
||||
if (!component) return RplId.Invalid();
|
||||
return component.Id();
|
||||
}
|
||||
ERR_PersonalEscapeResult ERR_EscapeResult()
|
||||
{
|
||||
RplId mission = ERR_CurrentResultMission();
|
||||
if (mission == RplId.Invalid() || mission != m_ERREscapeResultMission) return ERR_PersonalEscapeResult.UNKNOWN;
|
||||
return m_ERREscapeResult;
|
||||
}
|
||||
void ERR_ResetEscapeResult()
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
m_ERREscapeResult = ERR_PersonalEscapeResult.UNKNOWN;
|
||||
m_ERREscapeResultMission = ERR_CurrentResultMission();
|
||||
Replication.BumpMe();
|
||||
ERR_OnEscapeResultReplicated();
|
||||
}
|
||||
void ERR_PublishEscapeResult(ERR_PlayerRoster roster, ERR_ExtractionService extraction)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PersonalEscapeResult result = ERR_PersonalEscapeResult.UNKNOWN;
|
||||
ERR_PlayerLife life;
|
||||
if (roster) life = roster.Get(GetPlayerId());
|
||||
if (life && life.Connected && life.ConnectionId == GetPlayerId() && life.UsedLife && extraction && extraction.State() == ERR_ExtractionState.ESCAPED)
|
||||
{
|
||||
result = ERR_PersonalEscapeResult.MISSED;
|
||||
if (extraction.DidEscape(life.AccountId)) result = ERR_PersonalEscapeResult.ESCAPED;
|
||||
}
|
||||
m_ERREscapeResult = result;
|
||||
m_ERREscapeResultMission = ERR_CurrentResultMission();
|
||||
Replication.BumpMe();
|
||||
ERR_OnEscapeResultReplicated();
|
||||
}
|
||||
protected void ERR_OnEscapeResultReplicated()
|
||||
{
|
||||
if (GetGame() && this == GetGame().GetPlayerController()) s_ERREscapeResultChanged.Invoke();
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
// Local presentation reads replicated state; it cannot advance objectives.
|
||||
modded class SCR_PlayerController
|
||||
{
|
||||
protected int m_ERRGuidePhase = -1;
|
||||
protected bool m_ERRGuideStarted;
|
||||
protected string m_ERRGuideMessage;
|
||||
|
||||
override void OnControlledEntityChanged(IEntity from, IEntity to)
|
||||
{
|
||||
super.OnControlledEntityChanged(from, to);
|
||||
ERR_StartGuide();
|
||||
}
|
||||
protected override void ERR_OnObserverMonitorStarted()
|
||||
{
|
||||
super.ERR_OnObserverMonitorStarted();
|
||||
ERR_StartGuide();
|
||||
}
|
||||
protected void ERR_StartGuide()
|
||||
{
|
||||
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get() || m_ERRGuideStarted) return;
|
||||
m_ERRGuideStarted = true;
|
||||
GetGame().GetCallqueue().CallLater(ERR_UpdateGuide, 1000, true);
|
||||
}
|
||||
protected void ERR_UpdateGuide()
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode) return;
|
||||
if (mode.Phase() != ERR_Phase.PREPARING && !GetControlledEntity()) return;
|
||||
string message = ERR_ObjectiveText.Description(mode, false);
|
||||
if (m_ERRGuidePhase == mode.Phase() && m_ERRGuideMessage == message) return;
|
||||
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
|
||||
if (!hints) return;
|
||||
if (!message.IsEmpty()) hints.ShowCustom(message, ERR_ObjectiveText.Title(mode.Phase()), 15);
|
||||
m_ERRGuidePhase = mode.Phase();
|
||||
m_ERRGuideMessage = message;
|
||||
}
|
||||
void ~SCR_PlayerController()
|
||||
{
|
||||
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_UpdateGuide);
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// Round-owned identities survive controller IDs, death and reconnect attempts.
|
||||
class ERR_PlayerLifeRules
|
||||
{
|
||||
static bool CanEnter(bool usedLife, int phase)
|
||||
{
|
||||
return !usedLife && phase >= ERR_Phase.PRISON && phase <= ERR_Phase.RADIO_DEFENCE;
|
||||
}
|
||||
}
|
||||
|
||||
class ERR_PlayerLife
|
||||
{
|
||||
UUID AccountId;
|
||||
int ConnectionId;
|
||||
bool Connected;
|
||||
bool UsedLife;
|
||||
IEntity Actor;
|
||||
}
|
||||
|
||||
class ERR_PlayerRoster
|
||||
{
|
||||
protected ref map<UUID, ref ERR_PlayerLife> m_Lives = new map<UUID, ref ERR_PlayerLife>();
|
||||
protected ref map<int, UUID> m_Connections = new map<int, UUID>();
|
||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||
|
||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||
bool Bind(int playerId, UUID identity)
|
||||
{
|
||||
if (!Replication.IsServer() || playerId <= 0 || identity.IsNull()) return false;
|
||||
ERR_PlayerLife life;
|
||||
if (!m_Lives.Find(identity, life))
|
||||
{
|
||||
life = new ERR_PlayerLife();
|
||||
life.AccountId = identity;
|
||||
m_Lives.Insert(identity, life);
|
||||
}
|
||||
if (life.Connected && life.ConnectionId != playerId) return false;
|
||||
// A recycled transient ID cannot inherit the previous connection's record.
|
||||
UUID previous;
|
||||
if (m_Connections.Find(playerId, previous) && previous != identity) Disconnect(playerId);
|
||||
life.ConnectionId = playerId;
|
||||
life.Connected = true;
|
||||
m_Connections.Set(playerId, identity);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
return true;
|
||||
}
|
||||
ERR_PlayerLife Get(int playerId)
|
||||
{
|
||||
UUID identity;
|
||||
if (!m_Connections.Find(playerId, identity)) return null;
|
||||
return m_Lives.Get(identity);
|
||||
}
|
||||
// The roster identity was bound by the audited connection lifecycle. Consumers
|
||||
// validate its current body rather than requesting a second backend identity.
|
||||
ERR_PlayerLife CurrentActor(IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor || !GetGame() || !GetGame().GetPlayerManager()) return null;
|
||||
PlayerManager manager = GetGame().GetPlayerManager();
|
||||
int connection = manager.GetPlayerIdFromControlledEntity(actor);
|
||||
if (connection <= 0 || manager.GetPlayerControlledEntity(connection) != actor) return null;
|
||||
ERR_PlayerLife life = Get(connection);
|
||||
if (!life || life.AccountId.IsNull() || !life.Connected || !life.UsedLife || life.ConnectionId != connection || life.Actor != actor) return null;
|
||||
return life;
|
||||
}
|
||||
bool CanEnter(int playerId, int phase)
|
||||
{
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
return life && life.Connected && ERR_PlayerLifeRules.CanEnter(life.UsedLife, phase);
|
||||
}
|
||||
void Spawned(int playerId, IEntity actor)
|
||||
{
|
||||
if (!Replication.IsServer() || !actor) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (!life) return;
|
||||
life.Actor = actor;
|
||||
life.UsedLife = true;
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Disconnect(int playerId)
|
||||
{
|
||||
if (!Replication.IsServer()) return;
|
||||
ERR_PlayerLife life = Get(playerId);
|
||||
if (life) life.Connected = false;
|
||||
m_Connections.Remove(playerId);
|
||||
m_OnChanged.Invoke(playerId);
|
||||
}
|
||||
void Participants(out array<ref ERR_PlayerLife> lives)
|
||||
{
|
||||
lives.Clear();
|
||||
foreach (UUID identity, ERR_PlayerLife life : m_Lives)
|
||||
if (life.UsedLife) lives.Insert(life);
|
||||
}
|
||||
bool HasSurvivor()
|
||||
{
|
||||
// Native reconnect reservations retain the original actor until expiry.
|
||||
// A reserved survivor must not lose the round while their client reconnects.
|
||||
foreach (UUID identity, ERR_PlayerLife life : m_Lives)
|
||||
if (life.UsedLife && ERR_Actors.Alive(life.Actor)) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
modded class SCR_ReconnectComponent
|
||||
{
|
||||
override bool HandlePlayerDisconnect(int playerId, KickCauseCode cause)
|
||||
{
|
||||
ERR_GameMode mode = ERR_GameMode.Get();
|
||||
if (!mode || !mode.Active()) return super.HandlePlayerDisconnect(playerId, cause);
|
||||
UUID identity = SCR_PlayerIdentityUtils.GetPlayerIdentityId(playerId);
|
||||
if (identity.IsNull()) return super.HandlePlayerDisconnect(playerId, cause);
|
||||
// Voluntary and involuntary departures use the same vanilla reservation,
|
||||
// timeout, character handoff and expiry cleanup. No replacement life is issued.
|
||||
SCR_ReconnectData data = StoreData(playerId);
|
||||
if (!IsDataRelevant(data)) return false;
|
||||
m_mReconnectData.Set(identity, data);
|
||||
UpdateExpieryCheck();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user