Fix convoy coordination, AI combat and spectator recovery
This commit is contained in:
@@ -0,0 +1,763 @@
|
|||||||
|
BehaviorTree "{D0BDF32990D2C687}AI/BehaviorTrees/ERR/MortarRapidFire.bt" {
|
||||||
|
Name "AI/BehaviorTrees/ERR/MortarRapidFire.bt"
|
||||||
|
Nodes {
|
||||||
|
RootNode Root {
|
||||||
|
Name "Root"
|
||||||
|
EditorPos 2512 -464
|
||||||
|
Nodes {
|
||||||
|
Repeater Repeater {
|
||||||
|
Name "Repeater"
|
||||||
|
EditorPos 2512.222 -312.222
|
||||||
|
Nodes {
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 2507.166 -173.666
|
||||||
|
Nodes {
|
||||||
|
SCR_AIGetStaticArtilleryBehaviorParameters "SCR_AI Get Static Artillery Behavior Parameters" {
|
||||||
|
Name "SCR_AI Get Static Artillery Behavior Parameters"
|
||||||
|
EditorPos -1398.001 1037.668
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 0
|
||||||
|
PortName "ArtilleryEntity"
|
||||||
|
}
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 17
|
||||||
|
PortName "TargetPos"
|
||||||
|
}
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "InventoryEntity"
|
||||||
|
}
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 1
|
||||||
|
PortName "AmmoPrefab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos 413.238 1284.881
|
||||||
|
Nodes {
|
||||||
|
DecoTestVariable "Test Variable" {
|
||||||
|
Name "Test Variable"
|
||||||
|
EditorPos 165.588 1481.158
|
||||||
|
Nodes {
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos -131.571 2021.904
|
||||||
|
Nodes {
|
||||||
|
AITaskCharacterSetMovementSpeed "Character Set Movement Speed" {
|
||||||
|
Name "Character Set Movement Speed"
|
||||||
|
EditorPos -1542.333 2485
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
"Movement speed" RUN
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos -1077.214 2563.785
|
||||||
|
Nodes {
|
||||||
|
DecoIsInVehicle "Is In Vehicle" {
|
||||||
|
Name "Is In Vehicle"
|
||||||
|
EditorPos -1249.942 2695.149
|
||||||
|
Nodes {
|
||||||
|
AITaskGetInVehicle "Get In Vehicle" {
|
||||||
|
Name "Get In Vehicle"
|
||||||
|
EditorPos -1266.143 2863.728
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
PrecisionXZ 1
|
||||||
|
Position TURRET
|
||||||
|
InVehicle 21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
NegativeCondition 1
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos -1003.786 2694.214
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
Result SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos -198.12 2172.832
|
||||||
|
Nodes {
|
||||||
|
SCR_AIGetArtilleryAimDistanceCompensation "SCR_AI Get Artillery Aim Distance Compensation" {
|
||||||
|
Name "SCR_AI Get Artillery Aim Distance Compensation"
|
||||||
|
EditorPos -633.667 2389.631
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 16
|
||||||
|
PortName "ArtilleryEntity"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 19
|
||||||
|
PortName "AmmoPrefab"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 18
|
||||||
|
PortName "TargetPos"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 20
|
||||||
|
PortName "AimPos"
|
||||||
|
}
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 27
|
||||||
|
PortName "InitSpeedId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos -16.266 2433.212
|
||||||
|
Nodes {
|
||||||
|
SCR_AITalk "SCR_AI Talk" {
|
||||||
|
Name "SCR_AI Talk"
|
||||||
|
EditorPos -153.255 2556.307
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "SpeakerIn"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "TargetIn"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "LocationIn"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "EnumIn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
m_messageType REPORT_NEGATIVE
|
||||||
|
m_ePreset MANDATORY
|
||||||
|
}
|
||||||
|
SCR_AIFailAction "SCR_AI Fail Action" {
|
||||||
|
Name "SCR_AI Fail Action"
|
||||||
|
EditorPos -30.897 2555.629
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
m_eFailReason TARGET_UNREACHABLE
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 95.77 2555.712
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
Result RUNNING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
AITaskAim Aim {
|
||||||
|
Name "Aim"
|
||||||
|
EditorPos 353.709 2413.569
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
InAimAtPosition 22
|
||||||
|
PrecisionXZ 0.05
|
||||||
|
PrecisionY 0.05
|
||||||
|
}
|
||||||
|
AITaskSetVariable "Set Variable" {
|
||||||
|
Name "Set Variable"
|
||||||
|
EditorPos 697.464 2397.535
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
SetValue "true"
|
||||||
|
OutVariable 23
|
||||||
|
}
|
||||||
|
AITaskGetOutVehicle "Get Out Vehicle" {
|
||||||
|
Name "Get Out Vehicle"
|
||||||
|
EditorPos 910.25 2399.999
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
AITaskIdle Idle {
|
||||||
|
Name "Idle"
|
||||||
|
EditorPos 1101.667 2391.666
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
Period 0.75
|
||||||
|
PeriodRandom 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NegativeCondition 1
|
||||||
|
InVariable 24
|
||||||
|
TestType EqualsValue
|
||||||
|
TestValue "true"
|
||||||
|
}
|
||||||
|
DecoTestVariable "Test Variable" {
|
||||||
|
Name "Test Variable"
|
||||||
|
EditorPos 350.389 1454.611
|
||||||
|
Nodes {
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 349.948 1608.115
|
||||||
|
Result SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InVariable 24
|
||||||
|
TestType EqualsValue
|
||||||
|
TestValue "true"
|
||||||
|
}
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 637.139 1525.278
|
||||||
|
Nodes {
|
||||||
|
AITaskIdle Idle {
|
||||||
|
Name "Idle"
|
||||||
|
EditorPos 560 1643.75
|
||||||
|
Group "{619AE77BE5296DF2}"
|
||||||
|
PeriodRandom 0
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 692.833 1641.25
|
||||||
|
Group "{619AE77BE5296DF2}"
|
||||||
|
Result FAIL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77BE5296DF2}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos 2100.833 754.167
|
||||||
|
Nodes {
|
||||||
|
SCR_AIFindItemInInventory "SCR_AI Find Item In Inventory" {
|
||||||
|
Name "SCR_AI Find Item In Inventory"
|
||||||
|
EditorPos 1820.595 1035.833
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 13
|
||||||
|
PortName "PrefabResourceName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 14
|
||||||
|
PortName "ItemEntity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 2577.5 1823
|
||||||
|
Nodes {
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 1923.916 2246.194
|
||||||
|
Nodes {
|
||||||
|
AITaskCreatePosition "Create Position" {
|
||||||
|
Name "Create Position"
|
||||||
|
EditorPos 1717.386 2367.864
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
InEntity 3
|
||||||
|
OutPosition 2
|
||||||
|
}
|
||||||
|
SCR_AIFindArsenalWithPrefab "SCR_AI Find Arsenal With Prefab" {
|
||||||
|
Name "SCR_AI Find Arsenal With Prefab"
|
||||||
|
EditorPos 1830.487 2493.422
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 2
|
||||||
|
PortName "Pos"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 4
|
||||||
|
PortName "PrefabResourceName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 5
|
||||||
|
PortName "ArsenalEntity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
m_fSearchRadius 20
|
||||||
|
}
|
||||||
|
AITaskCharacterSetMovementSpeed "Character Set Movement Speed" {
|
||||||
|
Name "Character Set Movement Speed"
|
||||||
|
EditorPos 1993 2373.167
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
"Movement speed" RUN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos 2580.178 2035.203
|
||||||
|
Nodes {
|
||||||
|
AITaskMoveToEntity "Move To Entity" {
|
||||||
|
Name "Move To Entity"
|
||||||
|
EditorPos 2380.512 2174.286
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
PrecisionXZ 2
|
||||||
|
InEntity 9
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 2668.428 2175.453
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
Result SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
SCR_AITakeItemFromArsenal "SCR_AI Take Item From Arsenal" {
|
||||||
|
Name "SCR_AI Take Item From Arsenal"
|
||||||
|
EditorPos 2894.548 2307.881
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 6
|
||||||
|
PortName "ArsenalEntity"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 7
|
||||||
|
PortName "PrefabResourceName"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 8
|
||||||
|
PortName "ItemEntity"
|
||||||
|
}
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "CountItemsTaken"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
m_iMaxItemsToTake 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 2608.667 990
|
||||||
|
Nodes {
|
||||||
|
SCR_AITalk "SCR_AI Talk" {
|
||||||
|
Name "SCR_AI Talk"
|
||||||
|
EditorPos 2485.833 1116.944
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "SpeakerIn"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "TargetIn"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "LocationIn"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "EnumIn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C86E958CB}"
|
||||||
|
m_messageType REPORT_NO_AMMO
|
||||||
|
m_ePreset MANDATORY
|
||||||
|
}
|
||||||
|
SCR_AIFailAction "SCR_AI Fail Action" {
|
||||||
|
Name "SCR_AI Fail Action"
|
||||||
|
EditorPos 2605 1116.944
|
||||||
|
Group "{619AE77C86E958CB}"
|
||||||
|
m_eFailReason NO_AMMO
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 2736.166 1116.944
|
||||||
|
Group "{619AE77C86E958CB}"
|
||||||
|
Result RUNNING
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{619AE77C86E958CB}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SCR_AITakeGadgetInLeftHand "SCR_AI Take Gadget In Left Hand" {
|
||||||
|
Name "SCR_AI Take Gadget In Left Hand"
|
||||||
|
EditorPos 3229.69 912.619
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 15
|
||||||
|
PortName "ItemIn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port -1
|
||||||
|
PortName "ItemOut"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_eGadgetType NONE
|
||||||
|
}
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 4223.904 609.286
|
||||||
|
Nodes {
|
||||||
|
AITaskCharacterSetMovementSpeed "Character Set Movement Speed" {
|
||||||
|
Name "Character Set Movement Speed"
|
||||||
|
EditorPos 3828.833 809.333
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
"Movement speed" RUN
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos 4224.476 805.571
|
||||||
|
Nodes {
|
||||||
|
AITaskMoveToEntity "Move To Entity" {
|
||||||
|
Name "Move To Entity"
|
||||||
|
EditorPos 4009.333 942.667
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
PrecisionXZ 1.5
|
||||||
|
InEntity 10
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 4313.857 945.047
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
Result SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
Selector Selector {
|
||||||
|
Name "Selector"
|
||||||
|
EditorPos 5528.9 1018.859
|
||||||
|
Nodes {
|
||||||
|
Sequence Sequence {
|
||||||
|
Name "Sequence"
|
||||||
|
EditorPos 5132.62 1133.074
|
||||||
|
Nodes {
|
||||||
|
DecoTestVariable "Test Variable" {
|
||||||
|
Name "Test Variable"
|
||||||
|
EditorPos 4678.954 1280.82
|
||||||
|
Nodes {
|
||||||
|
SCR_AIConfigureArtilleryAmmo "SCR_AI Configure Artillery Ammo" {
|
||||||
|
Name "SCR_AI Configure Artillery Ammo"
|
||||||
|
EditorPos 4615.083 1454.909
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 12
|
||||||
|
PortName "AmmoEntity"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 28
|
||||||
|
PortName "InitSpeedId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
NegativeCondition 1
|
||||||
|
InVariable 28
|
||||||
|
TestType EqualsValue
|
||||||
|
TestValue "-1"
|
||||||
|
}
|
||||||
|
SCR_AIShootStaticArtillery "SCR_AI Shoot Static Artillery" {
|
||||||
|
Name "SCR_AI Shoot Static Artillery"
|
||||||
|
EditorPos 5066.497 1316.781
|
||||||
|
InPorts {
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 11
|
||||||
|
PortName "ArtilleryEntity"
|
||||||
|
}
|
||||||
|
SingleInputPortContainer SingleInputPortContainer {
|
||||||
|
Port 12
|
||||||
|
PortName "AmmoEntity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
AITaskIdle Idle {
|
||||||
|
Name "Idle"
|
||||||
|
EditorPos 5311.732 1318.751
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
Period 0
|
||||||
|
PeriodRandom 0
|
||||||
|
}
|
||||||
|
SCR_AIStaticArtilleryBehavior_ShootingLogic "SCR_AI Static Artillery Behavior_ Shooting Logic" {
|
||||||
|
Name "SCR_AI Static Artillery Behavior_ Shooting Logic"
|
||||||
|
EditorPos 5446.416 1323.941
|
||||||
|
OutPorts {
|
||||||
|
SingleOutputPortContainer SingleOutputPortContainer {
|
||||||
|
Port 25
|
||||||
|
PortName "AimingNeeded"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
m_iShotsBetweenAiming 5
|
||||||
|
}
|
||||||
|
DecoTestVariable "Test Variable" {
|
||||||
|
Name "Test Variable"
|
||||||
|
EditorPos 5875.542 1327.115
|
||||||
|
Nodes {
|
||||||
|
AITaskSetVariable "Set Variable" {
|
||||||
|
Name "Set Variable"
|
||||||
|
EditorPos 5853.542 1466.37
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
SetValue "false"
|
||||||
|
OutVariable 26
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
InVariable 25
|
||||||
|
TestType EqualsValue
|
||||||
|
TestValue "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
AITaskReturnState "Return State" {
|
||||||
|
Name "Return State"
|
||||||
|
EditorPos 5616.589 1132.723
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
Result SUCCESS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
AITaskIdle Idle {
|
||||||
|
Name "Idle"
|
||||||
|
EditorPos 5794.508 1010.276
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
Period 0
|
||||||
|
PeriodRandom 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RepeatTimes -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Variables {
|
||||||
|
BtVariableEntity artilleryEntity {
|
||||||
|
Flags 0
|
||||||
|
Desc "IEntity"
|
||||||
|
}
|
||||||
|
BtVariableResourceName ammoPrefab {
|
||||||
|
Flags 0
|
||||||
|
Desc "ResourceName"
|
||||||
|
}
|
||||||
|
BtVariableVector vArtilleryPos {
|
||||||
|
Flags 0
|
||||||
|
Desc "vector"
|
||||||
|
}
|
||||||
|
BtVariableEntity arsenalEntity {
|
||||||
|
Flags 0
|
||||||
|
Desc "IEntity"
|
||||||
|
}
|
||||||
|
BtVariableEntity artilleryAmmoEntity {
|
||||||
|
Flags 0
|
||||||
|
Desc "IEntity"
|
||||||
|
}
|
||||||
|
BtVariableVector vTargetPos {
|
||||||
|
Flags 0
|
||||||
|
Desc "vector"
|
||||||
|
}
|
||||||
|
BtVariableVector vAimPos {
|
||||||
|
Flags 0
|
||||||
|
Desc "vector"
|
||||||
|
}
|
||||||
|
BtVariableBool bTurretAimed {
|
||||||
|
Flags 0
|
||||||
|
Desc "bool"
|
||||||
|
}
|
||||||
|
BtVariableBool bDoAimingAgain {
|
||||||
|
Flags 0
|
||||||
|
Desc "bool"
|
||||||
|
}
|
||||||
|
BtVariableInteger iInitSpeedId {
|
||||||
|
Flags 0
|
||||||
|
Desc "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VariablesRefs {
|
||||||
|
VariableReference artilleryEntity {
|
||||||
|
VariableType 0
|
||||||
|
EditorPos -944 976
|
||||||
|
}
|
||||||
|
VariableReference ammoPrefab {
|
||||||
|
VariableType 1
|
||||||
|
EditorPos -944 1136
|
||||||
|
}
|
||||||
|
VariableReference vArtilleryPos {
|
||||||
|
VariableType 2
|
||||||
|
EditorPos 1616 2528
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference artilleryEntity {
|
||||||
|
VariableType 0
|
||||||
|
EditorPos 1520 2432
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference ammoPrefab {
|
||||||
|
VariableType 1
|
||||||
|
EditorPos 1552 2608
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference arsenalEntity {
|
||||||
|
VariableType 3
|
||||||
|
EditorPos 1920 2608
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference arsenalEntity {
|
||||||
|
VariableType 3
|
||||||
|
EditorPos 2688 2304
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference ammoPrefab {
|
||||||
|
VariableType 1
|
||||||
|
EditorPos 2624 2384
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference artilleryAmmoEntity {
|
||||||
|
VariableType 4
|
||||||
|
EditorPos 2896 2432
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference arsenalEntity {
|
||||||
|
VariableType 3
|
||||||
|
EditorPos 2192 2208
|
||||||
|
Group "{618C93F9D918DCD9}"
|
||||||
|
}
|
||||||
|
VariableReference artilleryEntity {
|
||||||
|
VariableType 0
|
||||||
|
EditorPos 3792 976
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
VariableReference artilleryEntity {
|
||||||
|
VariableType 0
|
||||||
|
EditorPos 4864 1312
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
VariableReference artilleryAmmoEntity {
|
||||||
|
VariableType 4
|
||||||
|
EditorPos 4336 1456
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
VariableReference ammoPrefab {
|
||||||
|
VariableType 1
|
||||||
|
EditorPos 1568 1072
|
||||||
|
}
|
||||||
|
VariableReference artilleryAmmoEntity {
|
||||||
|
VariableType 4
|
||||||
|
EditorPos 1824 1136
|
||||||
|
}
|
||||||
|
VariableReference artilleryAmmoEntity {
|
||||||
|
VariableType 4
|
||||||
|
EditorPos 2976 976
|
||||||
|
}
|
||||||
|
VariableReference artilleryEntity {
|
||||||
|
VariableType 0
|
||||||
|
EditorPos -864 2368
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference vTargetPos {
|
||||||
|
VariableType 5
|
||||||
|
EditorPos -944 1056
|
||||||
|
}
|
||||||
|
VariableReference vTargetPos {
|
||||||
|
VariableType 5
|
||||||
|
EditorPos -848 2528
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference ammoPrefab {
|
||||||
|
VariableType 1
|
||||||
|
EditorPos -928 2448
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference vAimPos {
|
||||||
|
VariableType 6
|
||||||
|
EditorPos -224 2352
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference artilleryEntity {
|
||||||
|
VariableType 0
|
||||||
|
EditorPos -1472 2896
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference vAimPos {
|
||||||
|
VariableType 6
|
||||||
|
EditorPos 192 2448
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference bTurretAimed {
|
||||||
|
VariableType 7
|
||||||
|
EditorPos 736 2512
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference bTurretAimed {
|
||||||
|
VariableType 7
|
||||||
|
EditorPos -64 1504
|
||||||
|
}
|
||||||
|
VariableReference bDoAimingAgain {
|
||||||
|
VariableType 8
|
||||||
|
EditorPos 5568 1424
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
VariableReference bTurretAimed {
|
||||||
|
VariableType 7
|
||||||
|
EditorPos 5856 1584
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
VariableReference iInitSpeedId {
|
||||||
|
VariableType 9
|
||||||
|
EditorPos -224 2448
|
||||||
|
Group "{619AE77C783232F7}"
|
||||||
|
}
|
||||||
|
VariableReference iInitSpeedId {
|
||||||
|
VariableType 9
|
||||||
|
EditorPos 4416 1536
|
||||||
|
Group "{618C93F9C535F544}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Groups {
|
||||||
|
BTGroup "{618C93F9C535F544}" {
|
||||||
|
Name "Shoot artillery"
|
||||||
|
Color 0.6 0.6 0.6 0.251
|
||||||
|
}
|
||||||
|
BTGroup "{618C93F9D918DCD9}" {
|
||||||
|
Name "Find ammo in arsenal"
|
||||||
|
Color 0.6 0.6 0.6 0.251
|
||||||
|
}
|
||||||
|
BTGroup "{619AE77C783232F7}" {
|
||||||
|
Name "Aim mortar"
|
||||||
|
Color 0.6 0.6 0.6 0.251
|
||||||
|
}
|
||||||
|
BTGroup "{619AE77C86E958CB}" {
|
||||||
|
Name "No ammo for artillery"
|
||||||
|
Color 0.6 0.6 0.6 0.251
|
||||||
|
}
|
||||||
|
BTGroup "{619AE77BE5296DF2}" {
|
||||||
|
Name "Unable to get in or aim artillery"
|
||||||
|
Color 0.6 0.6 0.6 0.251
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
MetaFileClass {
|
||||||
|
Name "{D0BDF32990D2C687}AI/BehaviorTrees/ERR/MortarRapidFire.bt"
|
||||||
|
Configurations {
|
||||||
|
BehaviorTreeResourceClass PC {}
|
||||||
|
BehaviorTreeResourceClass HEADLESS {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ ERR_ContentDefinition {
|
|||||||
Parent "rhs"
|
Parent "rhs"
|
||||||
DefaultEnabled 1
|
DefaultEnabled 1
|
||||||
NativeFactionKey "RHS_USAF"
|
NativeFactionKey "RHS_USAF"
|
||||||
|
ResponseSoldier "{844936B168DFAB8E}Prefabs/Characters/Factions/BLUFOR/RHS_USAF/RHS_USAF_MARSOC/RandomSoldiers/Character_RHS_USMC_MARSOC_Rifleman_Random.et"
|
||||||
|
ResponseMedic "{DBB8D1F13930220E}Prefabs/Characters/Factions/BLUFOR/RHS_USAF/RHS_USAF_MARSOC/RandomSoldiers/Character_RHS_USMC_MARSOC_Medic_Random.et"
|
||||||
GuardGroup "{19843E954790DF28}Prefabs/Groups/BLUFOR/RHS_USAF/RHS_USAF_USMC_MEF/Group_USAF_USMC_MEF_FireTeam.et"
|
GuardGroup "{19843E954790DF28}Prefabs/Groups/BLUFOR/RHS_USAF/RHS_USAF_USMC_MEF/Group_USAF_USMC_MEF_FireTeam.et"
|
||||||
}
|
}
|
||||||
ERR_ContentOption {
|
ERR_ContentOption {
|
||||||
@@ -20,6 +22,8 @@ ERR_ContentDefinition {
|
|||||||
Parent "rhs"
|
Parent "rhs"
|
||||||
DefaultEnabled 1
|
DefaultEnabled 1
|
||||||
NativeFactionKey "RHS_AFRF"
|
NativeFactionKey "RHS_AFRF"
|
||||||
|
ResponseSoldier "{B3C496CCFA72D25E}Prefabs/Characters/Factions/OPFOR/RHS_AFRF/MSV/Flora/Character_RHS_RF_MSV_Flora_SL.et"
|
||||||
|
ResponseMedic "{D9AF475A4E5DC52C}Prefabs/Characters/Factions/OPFOR/RHS_AFRF/MSV/Flora/Character_RHS_RF_MSV_Flora_Medic.et"
|
||||||
GuardGroup "{AB905EF799E0A0AE}Prefabs/Groups/OPFOR/RHS_AFRF/MSV/Flora/Group_RHS_RF_MSV_Flora_FireGroup.et"
|
GuardGroup "{AB905EF799E0A0AE}Prefabs/Groups/OPFOR/RHS_AFRF/MSV/Flora/Group_RHS_RF_MSV_Flora_FireGroup.et"
|
||||||
}
|
}
|
||||||
ERR_ContentOption {
|
ERR_ContentOption {
|
||||||
@@ -28,6 +32,8 @@ ERR_ContentDefinition {
|
|||||||
Parent "rhs"
|
Parent "rhs"
|
||||||
DefaultEnabled 1
|
DefaultEnabled 1
|
||||||
NativeFactionKey "RHS_ION"
|
NativeFactionKey "RHS_ION"
|
||||||
|
ResponseSoldier "{140001FC383DF9A2}Prefabs/Characters/Factions/INDFOR/RHS_ION_Coy/RandomSoldiers/Character_RHS_ION_Coy_Rifleman_Random.et"
|
||||||
|
ResponseMedic "{0FA8ACE19A772327}Prefabs/Characters/Factions/INDFOR/RHS_ION_Coy/RandomSoldiers/Character_RHS_ION_Coy_Medic_Random.et"
|
||||||
GuardGroup "{516C8C7400DF2C4D}Prefabs/Groups/INDFOR/RHS_ION/RHS_ION_COY/Group_RHS_ION_COY_StaticSecurityTeam.et"
|
GuardGroup "{516C8C7400DF2C4D}Prefabs/Groups/INDFOR/RHS_ION/RHS_ION_COY/Group_RHS_ION_COY_StaticSecurityTeam.et"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
ActionManager : "{795184CF9AD764DB}Configs/System/chimeraInputCommon.conf" {
|
||||||
|
Contexts {
|
||||||
|
ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" {
|
||||||
|
Actions {
|
||||||
|
Action ERR_ObserverPrevious {
|
||||||
|
InputSource InputSourceValue "{A7B19F416E541001}" {
|
||||||
|
Input "keyboard:KC_LEFT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action ERR_ObserverNext {
|
||||||
|
InputSource InputSourceValue "{A7B19F416E541002}" {
|
||||||
|
Input "keyboard:KC_RIGHT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action ERR_ObserverView {
|
||||||
|
InputSource InputSourceSum "{A7B19F416E541003}" {
|
||||||
|
Sources {
|
||||||
|
InputSourceValue "{A7B19F416E541004}" {
|
||||||
|
Input "keyboard:KC_RETURN"
|
||||||
|
}
|
||||||
|
InputSourceValue "{A7B19F416E541005}" {
|
||||||
|
Input "keyboard:KC_NUMPADENTER"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
MetaFileClass {
|
||||||
|
Name "{A7B19F416E541000}Configs/ERR/ObserverInput.conf"
|
||||||
|
Configurations {
|
||||||
|
CONFResourceClass PC { }
|
||||||
|
CONFResourceClass HEADLESS : PC { }
|
||||||
|
CONFResourceClass XBOX_ONE : PC { }
|
||||||
|
CONFResourceClass XBOX_SERIES : PC { }
|
||||||
|
CONFResourceClass PS4 : PC { }
|
||||||
|
CONFResourceClass PS5 : PC { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
GenericEntity : "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" {
|
||||||
|
ID "BF10090634B949D0"
|
||||||
|
components {
|
||||||
|
InventoryItemComponent "{5222EB4D0C73006B}" {
|
||||||
|
Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" {
|
||||||
|
ItemDisplayName SCR_InventoryUIInfo "{5222EB4D07D865FA}" {
|
||||||
|
Name "Epinephrine injector"
|
||||||
|
Description "Helps an unconscious casualty recover consciousness over time. Stop bleeding and replace lost blood separately. Cannot revive the dead."
|
||||||
|
m_aItemHints {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SCR_ConsumableItemComponent "{59E68C74C5CEE74F}" {
|
||||||
|
m_ConsumableEffect ERR_ConsumableEpinephrine "{59E68C74C1F3910E}" {
|
||||||
|
m_fItemRegenerationSpeed 5
|
||||||
|
m_fItemRegenerationDuration 30
|
||||||
|
m_aDamageEffectsToLoad {
|
||||||
|
ERR_EpinephrineEffect "{6122D60861B541E7}" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
MetaFileClass {
|
||||||
|
Name "{C68512DB5FC74698}Prefabs/ERR/Epinephrine.et"
|
||||||
|
Configurations {
|
||||||
|
EntityTemplateResourceClass PC {}
|
||||||
|
EntityTemplateResourceClass HEADLESS : PC {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
SCR_ManualCamera {
|
SCR_ManualCamera {
|
||||||
ID "634904BCEDA1A285"
|
ID "634904BCEDA1A285"
|
||||||
m_bInputEnabled 1
|
m_bInputEnabled 1
|
||||||
|
m_sActionContext "ERR_ObserverContext"
|
||||||
m_bStartAtEntity 1
|
m_bStartAtEntity 1
|
||||||
m_aComponents {
|
m_aComponents {
|
||||||
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class ERR_ContentOption
|
|||||||
string NativeFactionKey;
|
string NativeFactionKey;
|
||||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
||||||
ResourceName GuardGroup;
|
ResourceName GuardGroup;
|
||||||
|
[Attribute("", UIWidgets.ResourceNamePicker, "Optional HQ response soldier; use for factions with empty character catalogs", "et")]
|
||||||
|
ResourceName ResponseSoldier;
|
||||||
|
[Attribute("", UIWidgets.ResourceNamePicker, "Optional same-faction response medic with authored medical backpack", "et")]
|
||||||
|
ResourceName ResponseMedic;
|
||||||
[Attribute("", UIWidgets.ResourceNamePicker, "Optional faction MANPADS override; must spawn loaded and usable. Otherwise FIM-92 is used.", "et")]
|
[Attribute("", UIWidgets.ResourceNamePicker, "Optional faction MANPADS override; must spawn loaded and usable. Otherwise FIM-92 is used.", "et")]
|
||||||
ResourceName AntiAirLauncher;
|
ResourceName AntiAirLauncher;
|
||||||
[Attribute("", UIWidgets.EditBox, "Exact resource GUIDs for optional equipment owned by this pack")]
|
[Attribute("", UIWidgets.EditBox, "Exact resource GUIDs for optional equipment owned by this pack")]
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Native injection animation/consumption; only resilience is restored over time.
|
||||||
|
// This is not a blood replacement, bandage, full heal, or resurrection.
|
||||||
|
[BaseContainerProps()]
|
||||||
|
class ERR_ConsumableEpinephrine : SCR_ConsumableMorphine
|
||||||
|
{
|
||||||
|
override bool CanApplyEffect(notnull IEntity target, notnull IEntity user, out SCR_EConsumableFailReason failReason)
|
||||||
|
{
|
||||||
|
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(target.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
|
CharacterControllerComponent controller = CharacterControllerComponent.Cast(target.FindComponent(CharacterControllerComponent));
|
||||||
|
if (!damage || damage.GetState() == EDamageState.DESTROYED || !controller || !controller.IsUnconscious())
|
||||||
|
{ failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
|
||||||
|
array<ref SCR_PersistentDamageEffect> effects = damage.GetAllPersistentEffectsOfType(ERR_EpinephrineEffect);
|
||||||
|
if (!effects.IsEmpty()) { failReason = SCR_EConsumableFailReason.ALREADY_APPLIED; return false; }
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_EpinephrineEffect : SCR_DotDamageEffect
|
||||||
|
{
|
||||||
|
override bool HijackDamageEffect(SCR_ExtendedDamageManagerComponent dmgManager)
|
||||||
|
{
|
||||||
|
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
|
||||||
|
if (damage) SetAffectedHitZone(damage.GetResilienceHitZone());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
|
||||||
|
}
|
||||||
|
// The held injector uses the existing native injection interaction and animation.
|
||||||
|
// Only its displayed name changes; ordinary morphine keeps its native behavior.
|
||||||
|
modded class SCR_MorphineUserAction
|
||||||
|
{
|
||||||
|
protected bool m_ERRHoldingEpinephrine;
|
||||||
|
override bool CanBeShownScript(IEntity user)
|
||||||
|
{
|
||||||
|
m_ERRHoldingEpinephrine = false;
|
||||||
|
bool shown = super.CanBeShownScript(user);
|
||||||
|
ChimeraCharacter character = ChimeraCharacter.Cast(user);
|
||||||
|
if (shown && character)
|
||||||
|
{
|
||||||
|
SCR_ConsumableItemComponent item = GetConsumableComponent(character);
|
||||||
|
m_ERRHoldingEpinephrine = item && ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||||
|
}
|
||||||
|
return shown;
|
||||||
|
}
|
||||||
|
override bool GetActionNameScript(out string outName)
|
||||||
|
{
|
||||||
|
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
|
||||||
|
return super.GetActionNameScript(outName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -497,7 +497,7 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
if (!Active()) return;
|
if (!Active()) return;
|
||||||
if (m_Recovery.DefeatDue(now))
|
if (m_Recovery.DefeatDue(now))
|
||||||
{
|
{
|
||||||
m_EndReason = "All prisoners are unconscious and no treatment or recovery is progressing.";
|
m_EndReason = "Everybody died. All prisoners are unconscious and nobody can recover or receive help.";
|
||||||
ERR_HQ.Event("MISSION FAILED", m_EndReason);
|
ERR_HQ.Event("MISSION FAILED", m_EndReason);
|
||||||
Transition(ERR_Phase.DEFEAT);
|
Transition(ERR_Phase.DEFEAT);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -120,6 +120,24 @@ class ERR_GeneratedSites
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
protected bool SkipOptional(ERR_SiteDefinition site)
|
||||||
|
{
|
||||||
|
if (site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.DEPOT) return false;
|
||||||
|
int index = m_Definition.Sites.Find(site);
|
||||||
|
if (index < 0) return false;
|
||||||
|
// Remove before spawning/registration, so no empty map or intel target remains.
|
||||||
|
// Keep the object alive while validating or restoring its original position.
|
||||||
|
ref ERR_SiteDefinition retained = site;
|
||||||
|
m_Definition.Sites.Remove(index);
|
||||||
|
if (!m_Definition.Valid())
|
||||||
|
{
|
||||||
|
m_Definition.Sites.InsertAt(retained, index);
|
||||||
|
PrintFormat("Co10 Escape Reintroduced [site required]: %1 (%2) cannot be skipped: remaining scenario would lack required objectives or accessible radio intel", retained.Label, retained.Id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
PrintFormat("Co10 Escape Reintroduced [site skipped]: %1 (%2): no safe placement after %3 attempts and authored fallback rejected; omitted for this round; mission preparation continues", retained.Label, retained.Id, m_Attempts);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
ERR_PrepareResult Tick()
|
ERR_PrepareResult Tick()
|
||||||
{
|
{
|
||||||
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
|
||||||
@@ -145,8 +163,15 @@ class ERR_GeneratedSites
|
|||||||
}
|
}
|
||||||
else if (fallback)
|
else if (fallback)
|
||||||
{
|
{
|
||||||
m_Error = "No safe terrain or valid authored fallback for " + site.Id;
|
if (SkipOptional(site))
|
||||||
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
{
|
||||||
|
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_Error = "No safe terrain or valid authored fallback for required site " + site.Id;
|
||||||
|
m_Result = ERR_PrepareResult.FAILED; return m_Result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (System.GetTickCount() - tick >= 8) return ERR_PrepareResult.PENDING;
|
if (System.GetTickCount() - tick >= 8) return ERR_PrepareResult.PENDING;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ class ERR_JournalText
|
|||||||
if (kind == "MORTAR SITE STATUS") return "Mortar " + Field(message, "site") + ": operational: " + YesNo(Field(message, "operational")) + "; combat-capable crew: " + Field(message, "fighting") + "; HE shells: " + Field(message, "inventory HE") + "; HQ radio connected: " + YesNo(Field(message, "radio connected")) + ".";
|
if (kind == "MORTAR SITE STATUS") return "Mortar " + Field(message, "site") + ": operational: " + YesNo(Field(message, "operational")) + "; combat-capable crew: " + Field(message, "fighting") + "; HE shells: " + Field(message, "inventory HE") + "; HQ radio connected: " + YesNo(Field(message, "radio connected")) + ".";
|
||||||
if (kind == "MORTAR ORDER STATUS") return "Mortar " + Field(message, "site") + ": " + Field(message, "order shots") + " shots in this order; " + Field(message, "total shots") + " shots fired in total.";
|
if (kind == "MORTAR ORDER STATUS") return "Mortar " + Field(message, "site") + ": " + Field(message, "order shots") + " shots in this order; " + Field(message, "total shots") + " shots fired in total.";
|
||||||
if (kind == "BODY SEARCH COMPLETE") return "A recovery team finished searching a body. " + Short(message);
|
if (kind == "BODY SEARCH COMPLETE") return "A recovery team finished searching a body. " + Short(message);
|
||||||
|
if (kind == "CONTACT REPORT") return "Squad reporting a captured sighting to HQ: " + Short(message);
|
||||||
|
if (kind == "GUNFIRE REPORT") return "Squad reporting heard gunfire to HQ: " + Short(message);
|
||||||
|
if (kind == "INFANTRY COMBAT") return message;
|
||||||
if (kind == "INTEL COMPROMISE") return "HQ learned that intel is missing: " + Short(message);
|
if (kind == "INTEL COMPROMISE") return "HQ learned that intel is missing: " + Short(message);
|
||||||
if (kind == "OBJECTIVE DEFENCE") return "Objective security: " + Short(message);
|
if (kind == "OBJECTIVE DEFENCE") return "Objective security: " + Short(message);
|
||||||
if (kind == "HELICOPTER LAUNCH") return "Search helicopter launched: " + Short(message);
|
if (kind == "HELICOPTER LAUNCH") return "Search helicopter launched: " + Short(message);
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ modded class ERR_GameMode
|
|||||||
string enabled;
|
string enabled;
|
||||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||||
string nativeEndCase;
|
string nativeEndCase;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "unconscious-camera" || nativeEndCase == "town-garrisons" || nativeEndCase == "site-garrisons" || nativeEndCase == "radio-interaction" || nativeEndCase == "enemy-loadout" || nativeEndCase == "aggression" || nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing" || nativeEndCase == "generated-sites" || nativeEndCase == "mortar-station" || nativeEndCase == "opening-tools" || nativeEndCase == "airfield-markers" || nativeEndCase == "air-logistics" || nativeEndCase == "air-assault" || nativeEndCase == "air-recon")) return;
|
array<string> independentFixtures = {"optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "town-garrisons", "site-garrisons", "radio-interaction", "enemy-loadout", "aggression", "native-end-active", "native-end-preparing", "generated-sites", "mortar-station", "opening-tools", "airfield-markers", "air-logistics", "air-assault", "air-recon"};
|
||||||
|
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && independentFixtures.Contains(nativeEndCase)) return;
|
||||||
ERR_NativeObjectiveProbe.BeginRound();
|
ERR_NativeObjectiveProbe.BeginRound();
|
||||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||||
string outcome;
|
string outcome;
|
||||||
@@ -269,7 +270,7 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("All prisoners are unconscious"), "stalled all-unconscious team reaches automatic mission defeat")) return;
|
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("All prisoners are unconscious"), "stalled all-unconscious team reaches automatic mission defeat")) return;
|
||||||
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
|
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
|
||||||
Print("ERR SMOKE COMPLETE: player casualty protection, native healing and automatic all-unconscious defeat");
|
Print("ERR SMOKE COMPLETE: player unconsciousness, native healing and automatic all-unconscious defeat");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_ERRExtractionCase == "objective-ui")
|
if (m_ERRExtractionCase == "objective-ui")
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
// Isolate native waypoint lifecycle from the separate long-distance patrol test.
|
||||||
|
// Contacts are explicitly injected; no natural target acquisition claim.
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected ref ERR_NativePatrolHQProbe m_ERRInfantryCombatProbe;
|
||||||
|
protected float m_ERRInfantryCombatDeadline;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "infantry-combat") return;
|
||||||
|
m_ERRInfantryCombatDeadline = System.GetTickCount() + 180000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_InfantryCombatTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected void ERR_InfantryCombatTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRInfantryCombatDeadline)
|
||||||
|
{
|
||||||
|
Print("ERR SMOKE FAIL: infantry combat fixture timed out", LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_InfantryCombatTick); return;
|
||||||
|
}
|
||||||
|
if (Phase() != ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||||
|
if (!m_ERRInfantryCombatProbe)
|
||||||
|
{
|
||||||
|
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
|
m_ERRInfantryCombatProbe = new ERR_NativePatrolHQProbe(this);
|
||||||
|
}
|
||||||
|
int result = m_ERRInfantryCombatProbe.Tick(GetWorld().GetWorldTime());
|
||||||
|
if (result == 0) return;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_InfantryCombatTick);
|
||||||
|
m_ERRInfantryCombatProbe.Stop();
|
||||||
|
if (result < 0) return;
|
||||||
|
Transition(ERR_Phase.DEFEAT);
|
||||||
|
Print("ERR SMOKE COMPLETE: native infantry search pause/resume and preserved home queue; injected contact, not live shooting acceptance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
modded class ERR_FactionSelection
|
||||||
|
{
|
||||||
|
override bool Prepare(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
string outcome;
|
||||||
|
if (System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "hunter-faction" || outcome == "air-recon" || outcome == "air-assault"))
|
||||||
|
{
|
||||||
|
m_Saved.Set("escapists", "us"); m_Saved.Set("enemy", "rhs_ion"); m_Saved.Set("independent", "fia");
|
||||||
|
}
|
||||||
|
return super.Prepare(mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class ERR_HQ
|
||||||
|
{
|
||||||
|
void ERR_HunterFixtureSighting(IEntity target, SCR_AIGroup source)
|
||||||
|
{
|
||||||
|
ERR_HQReport report = new ERR_HQReport();
|
||||||
|
report.Target = target; report.Source = source; report.Position = target.GetOrigin();
|
||||||
|
report.ObservedAt = GetGame().GetWorld().GetWorldTime(); report.MessageId = Journal().Message();
|
||||||
|
Accept(report, report.ObservedAt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class ERR_HunterService
|
||||||
|
{
|
||||||
|
ERR_HunterVehicle ERR_MedicalFixtureHunter()
|
||||||
|
{
|
||||||
|
string outcome;
|
||||||
|
if (!System.GetCLIParam("errSmokeOutcome", outcome) || (outcome != "medical-combat" && outcome != "hunter-faction")) return null;
|
||||||
|
foreach (ERR_HunterVehicle hunter : m_Hunters) if (hunter.Registered && !hunter.Released) return hunter;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected bool m_ERRHunterFactionFixture;
|
||||||
|
protected vector m_ERRHunterFactionStart;
|
||||||
|
protected int m_ERRMedicalCombatStep;
|
||||||
|
protected float m_ERRMedicalCombatDeadline;
|
||||||
|
protected ref ERR_NativeMedicalProbe m_ERRMedicalCombatProbe;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || (outcome != "medical-combat" && outcome != "hunter-faction")) return;
|
||||||
|
m_ERRHunterFactionFixture = outcome == "hunter-faction";
|
||||||
|
m_ERRMedicalCombatDeadline = System.GetTickCount() + 300000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_MedicalCombatTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_MedicalCombatCheck(bool ok, string detail)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: medical-combat " + detail); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: medical-combat " + detail, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_MedicalCombatTick); return false;
|
||||||
|
}
|
||||||
|
protected ERR_UnlockPrisonAction ERR_MedicalCombatLock(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_MedicalCombatLock(child);
|
||||||
|
if (lock) return lock;
|
||||||
|
child = child.GetSibling();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
protected void ERR_MedicalCombatTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRMedicalCombatDeadline) { ERR_MedicalCombatCheck(false, "bounded native fixture deadline"); return; }
|
||||||
|
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||||
|
if (m_ERRMedicalCombatStep == 0)
|
||||||
|
{
|
||||||
|
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
|
ERR_UnlockPrisonAction opening;
|
||||||
|
foreach (IEntity tracked : m_OwnedEntities) { if (tracked) opening = ERR_MedicalCombatLock(tracked); if (opening) break; }
|
||||||
|
if (!ERR_MedicalCombatCheck(opening != null, "owned prison gate located")) return;
|
||||||
|
player.SetOrigin(opening.GetOwner().GetOrigin() + "1 0 1");
|
||||||
|
opening.PerformAction(opening.GetOwner(), player);
|
||||||
|
if (!ERR_MedicalCombatCheck(Phase() == ERR_Phase.RADIO_SEARCH, "actual prison unlock releases response teams")) return;
|
||||||
|
m_ERRMedicalCombatStep = 1;
|
||||||
|
}
|
||||||
|
if (m_ERRMedicalCombatStep == 1)
|
||||||
|
{
|
||||||
|
ERR_HQ hq = ERR_HQ.Cast(Command());
|
||||||
|
ERR_HunterVehicle hunter; if (hq) hunter = hq.Hunters().ERR_MedicalFixtureHunter();
|
||||||
|
if (!hunter) return;
|
||||||
|
IEntity medic;
|
||||||
|
int specialists;
|
||||||
|
foreach (IEntity actor : hunter.Members)
|
||||||
|
{
|
||||||
|
if (!actor || !actor.GetPrefabData()) continue;
|
||||||
|
string path = actor.GetPrefabData().GetPrefabName();
|
||||||
|
if (ERR_EnemyMedicalCatalog.Medic(path)) medic = actor;
|
||||||
|
if (ERR_EnemyMedicalCatalog.Specialist(path)) specialists++;
|
||||||
|
}
|
||||||
|
if (!medic || hunter.Members.Count() <= 2 || hunter.Members.Count() != hunter.Seats.Count() || (!m_ERRHunterFactionFixture && specialists != hunter.Members.Count()))
|
||||||
|
{ ERR_MedicalCombatCheck(false, "actual hunter UAZ has full special-forces crew and medic"); return; }
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(medic.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
array<IEntity> items = {}; inventory.GetItems(items);
|
||||||
|
int epi, morphine, saline, dressings; bool bag;
|
||||||
|
foreach (IEntity item : items)
|
||||||
|
{
|
||||||
|
if (!item || !item.GetPrefabData()) continue;
|
||||||
|
string prefab = item.GetPrefabData().GetPrefabName();
|
||||||
|
if (prefab == ERR_EnemyMedicalCatalog.EPI) epi++;
|
||||||
|
if (prefab == ERR_EnemyMedicalCatalog.MORPHINE) morphine++;
|
||||||
|
if (prefab.Contains("SalineBag")) saline++;
|
||||||
|
if (prefab.Contains("FieldDressing")) dressings++;
|
||||||
|
if (prefab.Contains("Backpack_Medical") || (m_ERRHunterFactionFixture && prefab.Contains("Backpack_"))) bag = true;
|
||||||
|
}
|
||||||
|
if (epi < 6 || morphine < 6) return;
|
||||||
|
if (!ERR_MedicalCombatCheck(bag && saline >= 4 && dressings >= 4, "medic carries native medical bag, at least four saline/bandages, six morphine and six epinephrine")) return;
|
||||||
|
if (m_ERRHunterFactionFixture)
|
||||||
|
{
|
||||||
|
if (!ERR_MedicalCombatCheck(hunter.Group.GetFaction() == EnemyFaction() && EnemyFaction().GetFactionKey() == "RHS_ION", "response crew belongs specifically to RHS ION")) return;
|
||||||
|
if (!ERR_MedicalCombatCheck(hq.Hunters().VehicleRadioCapability(hunter.Driver) == 2, "seated ION driver can use the physical vehicle manpack")) return;
|
||||||
|
m_ERRHunterFactionStart = hunter.Entity.GetOrigin();
|
||||||
|
hq.ERR_HunterFixtureSighting(player, hunter.Group);
|
||||||
|
m_ERRMedicalCombatStep = 3; return;
|
||||||
|
}
|
||||||
|
m_ERRMedicalCombatProbe = new ERR_NativeMedicalProbe(this); m_ERRMedicalCombatStep = 2;
|
||||||
|
}
|
||||||
|
if (m_ERRMedicalCombatStep == 3)
|
||||||
|
{
|
||||||
|
ERR_HQ command = ERR_HQ.Cast(Command());
|
||||||
|
ERR_HunterVehicle response = command.Hunters().ERR_MedicalFixtureHunter();
|
||||||
|
if (!response || !response.Waypoint || response.SourceMessage <= 0 || vector.DistanceXZ(m_ERRHunterFactionStart, response.Entity.GetOrigin()) < 30) return;
|
||||||
|
if (!ERR_MedicalCombatCheck(response.CrewValid(this), "mod-faction UAZ receives contact order by radio and drives over 30 metres")) return;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_MedicalCombatTick);
|
||||||
|
Transition(ERR_Phase.DEFEAT);
|
||||||
|
Print("ERR SMOKE COMPLETE: selected mod-faction hunter, stocked medic, contact downlink and native vehicle movement; sighting injected at delivered intake");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_ERRMedicalCombatStep == 2)
|
||||||
|
{
|
||||||
|
int result = m_ERRMedicalCombatProbe.Tick(GetWorld().GetWorldTime());
|
||||||
|
if (result < 0) { ERR_MedicalCombatCheck(false, "native medical treatment fixture"); return; }
|
||||||
|
if (result == 0) return;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_MedicalCombatTick);
|
||||||
|
Transition(ERR_Phase.DEFEAT);
|
||||||
|
Print("ERR SMOKE COMPLETE: native full hunter crew, stocked medic, bleeding, finite treatment, epinephrine and protected lethal blood loss; injected medical states, not manual/multiplayer acceptance");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -7,6 +7,8 @@ class ERR_NativeMedicalProbe
|
|||||||
protected ERR_GameMode m_Mode;
|
protected ERR_GameMode m_Mode;
|
||||||
protected IEntity m_Casualty;
|
protected IEntity m_Casualty;
|
||||||
protected IEntity m_Healer;
|
protected IEntity m_Healer;
|
||||||
|
protected IEntity m_Epinephrine;
|
||||||
|
protected float m_BeforeEpi;
|
||||||
protected IEntity m_Dressing;
|
protected IEntity m_Dressing;
|
||||||
protected IEntity m_Saline;
|
protected IEntity m_Saline;
|
||||||
protected IEntity m_SpareSaline;
|
protected IEntity m_SpareSaline;
|
||||||
@@ -110,12 +112,12 @@ class ERR_NativeMedicalProbe
|
|||||||
m_SpareSaline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "6 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(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;
|
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_Deadline = now + 240000;
|
||||||
m_Wait = now + 1000;
|
m_Wait = now + 1000;
|
||||||
m_Step = 1;
|
m_Step = 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (!ERR_Actors.Alive(m_Casualty) || !ERR_Actors.Alive(m_Healer))
|
if (m_Step < 9 && (!ERR_Actors.Alive(m_Casualty) || !ERR_Actors.Alive(m_Healer)))
|
||||||
{
|
{
|
||||||
Check(false, "owned casualty or healer died unexpectedly");
|
Check(false, "owned casualty or healer died unexpectedly");
|
||||||
return -1;
|
return -1;
|
||||||
@@ -127,6 +129,7 @@ class ERR_NativeMedicalProbe
|
|||||||
m_SalineStorage = SCR_SalineStorageComponent.Cast(m_Casualty.FindComponent(SCR_SalineStorageComponent));
|
m_SalineStorage = SCR_SalineStorageComponent.Cast(m_Casualty.FindComponent(SCR_SalineStorageComponent));
|
||||||
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Healer.FindComponent(SCR_InventoryStorageManagerComponent));
|
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;
|
if (!Check(m_Damage && m_Controller && m_SalineStorage && m_Inventory, "native health, controller and medical storage components available")) return -1;
|
||||||
|
m_Damage.ERR_EnableCasualtyProtection();
|
||||||
m_Blood = m_Damage.GetBloodHitZone();
|
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_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;
|
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;
|
||||||
@@ -177,6 +180,18 @@ class ERR_NativeMedicalProbe
|
|||||||
{
|
{
|
||||||
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_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;
|
if (!Check(m_Controller.IsUnconscious(), "bandage alone has not fabricated blood recovery")) return -1;
|
||||||
|
m_SeedBlood = m_Blood.GetHealth();
|
||||||
|
m_Wait = now + 35000; m_Step = 30; return 0;
|
||||||
|
}
|
||||||
|
if (m_Step == 30)
|
||||||
|
{
|
||||||
|
array<ref SCR_PersistentDamageEffect> diagnostics = m_Damage.GetAllPersistentEffectsOfType(SCR_DotDamageEffect, true);
|
||||||
|
foreach (SCR_PersistentDamageEffect diagnostic : diagnostics)
|
||||||
|
{
|
||||||
|
DotDamageEffect dot = DotDamageEffect.Cast(diagnostic);
|
||||||
|
PrintFormat("ERR MEDICAL EFFECT: type=%1 damageType=%2 dps=%3 zone=%4 bloodZone=%5 resilienceZone=%6", diagnostic.Type(), dot.GetDamageType(), dot.GetDPS(), dot.GetAffectedHitZone(), m_Blood, m_Damage.GetResilienceHitZone());
|
||||||
|
}
|
||||||
|
if (!Check(m_Blood.GetHealth() > m_SeedBlood && m_Controller.IsUnconscious() && m_Damage.ERR_CanRecoverNaturally(), "bandaged casualty regenerates blood slowly without saline and can still recover")) return -1;
|
||||||
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
|
SCR_ConsumableItemComponent saline = SCR_ConsumableItemComponent.Cast(m_Saline.FindComponent(SCR_ConsumableItemComponent));
|
||||||
SCR_ConsumableSalineBag effect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
|
SCR_ConsumableSalineBag effect = SCR_ConsumableSalineBag.Cast(saline.GetConsumableEffect());
|
||||||
SCR_EConsumableFailReason reason;
|
SCR_EConsumableFailReason reason;
|
||||||
@@ -209,11 +224,39 @@ class ERR_NativeMedicalProbe
|
|||||||
if (m_Saline || AttachedBags() != 0 || SalineEffects() != 0 || m_Damage.GetGroupSalineBagged(ECharacterHitZoneGroup.RIGHTARM)) return 0;
|
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);
|
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_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 (!ERR_Actors.CanFight(m_Casualty)) return 0;
|
||||||
|
if (!Check(m_Recovered && !m_Controller.IsUnconscious(), "bandaged casualty eventually wakes through slow native recovery without epinephrine")) 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(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;
|
if (!Check(m_SpareSaline != null, "unused spare survives the completed infusion")) return -1;
|
||||||
InventoryItemComponent spareItem = InventoryItemComponent.Cast(m_SpareSaline.FindComponent(InventoryItemComponent));
|
InventoryItemComponent spareItem = InventoryItemComponent.Cast(m_SpareSaline.FindComponent(InventoryItemComponent));
|
||||||
if (!Check(spareItem && spareItem.GetParentSlot(), "rejected spare bag remains a physical inventory item")) return -1;
|
if (!Check(spareItem && spareItem.GetParentSlot(), "rejected spare bag remains a physical inventory item")) return -1;
|
||||||
|
m_Damage.ForceUnconsciousness();
|
||||||
|
m_Epinephrine = Spawn(ERR_EnemyMedicalCatalog.EPI, m_Healer.GetOrigin());
|
||||||
|
if (!Check(m_Epinephrine && m_Inventory.TryInsertItem(m_Epinephrine), "finite epinephrine enters healer inventory")) return -1;
|
||||||
|
m_Deadline = now + 45000; m_Wait = now + 1000; m_Step = 6; return 0;
|
||||||
|
}
|
||||||
|
if (m_Step == 6)
|
||||||
|
{
|
||||||
|
SCR_ConsumableItemComponent item = SCR_ConsumableItemComponent.Cast(m_Epinephrine.FindComponent(SCR_ConsumableItemComponent));
|
||||||
|
ERR_ConsumableEpinephrine effect = ERR_ConsumableEpinephrine.Cast(item.GetConsumableEffect());
|
||||||
|
SCR_EConsumableFailReason reason;
|
||||||
|
if (!Check(effect && effect.CanApplyEffect(m_Casualty, m_Healer, reason), "unconscious casualty accepts actual epinephrine effect")) return -1;
|
||||||
|
m_BeforeEpi = m_Damage.GetResilienceHitZone().GetHealth();
|
||||||
|
item.ApplyItemEffect(m_Casualty, m_Healer, effect.GetAnimationParameters(m_Epinephrine, m_Casualty), m_Epinephrine, effect.GetDeleteOnUse());
|
||||||
|
if (!Check(!effect.CanApplyEffect(m_Casualty, m_Healer, reason), "duplicate injection refused while treatment active")) return -1;
|
||||||
|
m_Wait = now + 6000; m_Step = 7; return 0;
|
||||||
|
}
|
||||||
|
if (m_Step == 7)
|
||||||
|
{
|
||||||
|
if (!Check(!m_Epinephrine && m_Damage.GetResilienceHitZone().GetHealth() > m_BeforeEpi,
|
||||||
|
"epinephrine is consumed and resilience recovers over time")) return -1;
|
||||||
|
m_Damage.GetBloodHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||||
|
m_Wait = now + 1000; m_Step = 9; return 0;
|
||||||
|
}
|
||||||
|
if (m_Step == 9)
|
||||||
|
{
|
||||||
|
if (!Check(ERR_Actors.Alive(m_Casualty) && m_Controller.IsUnconscious() && m_Blood.GetDamageState() != EDamageState.DESTROYED && m_Blood.GetHealthScaled() < 0.1,
|
||||||
|
"otherwise fatal blood loss preserves the same unconscious treatable body without healing it")) return -1;
|
||||||
m_Result = 1;
|
m_Result = 1;
|
||||||
Stop();
|
Stop();
|
||||||
Print("ERR MEDICAL RESULT: native blood loss, finite treatment and recovery; not manual action or multiplayer acceptance");
|
Print("ERR MEDICAL RESULT: native blood loss, finite treatment and recovery; not manual action or multiplayer acceptance");
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
// Force the reported camp-5 terrain refusal only in this opt-in native fixture.
|
||||||
|
modded class ERR_CampTerrain
|
||||||
|
{
|
||||||
|
static bool s_ERROptionalSiteRefused;
|
||||||
|
override bool Site(ERR_SiteDefinition site, vector position, float heading, out float baseHeight)
|
||||||
|
{
|
||||||
|
string outcome;
|
||||||
|
if (site.Id == "everon-camp-5" && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "optional-site-skip")
|
||||||
|
{
|
||||||
|
s_ERROptionalSiteRefused = true; return false;
|
||||||
|
}
|
||||||
|
return super.Site(site, position, heading, baseHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected float m_ERROptionalSiteDeadline;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "optional-site-skip") return;
|
||||||
|
ERR_CampTerrain.s_ERROptionalSiteRefused = false;
|
||||||
|
m_ERROptionalSiteDeadline = System.GetTickCount() + 240000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_OptionalSiteTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected void ERR_OptionalSiteTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERROptionalSiteDeadline)
|
||||||
|
{
|
||||||
|
Print("ERR SMOKE FAIL: optional site skip startup timed out", LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_OptionalSiteTick); return;
|
||||||
|
}
|
||||||
|
if (Phase() != ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_OptionalSiteTick);
|
||||||
|
if (!ERR_CampTerrain.s_ERROptionalSiteRefused || Sites().Find("everon-camp-5"))
|
||||||
|
{ Print("ERR SMOKE FAIL: forced camp refusal must leave no spawned camp runtime", LogLevel.ERROR); return; }
|
||||||
|
if (!Sites().Find("everon-prison-1") || !Sites().Find("everon-radio-1") || !Sites().Find("everon-extraction-1"))
|
||||||
|
{ Print("ERR SMOKE FAIL: required mission sites missing after optional skip", LogLevel.ERROR); return; }
|
||||||
|
for (int index = 1; index <= 4; index++)
|
||||||
|
if (!Sites().Find("everon-mortar-" + index.ToString()))
|
||||||
|
{ Print("ERR SMOKE FAIL: required mortar missing after optional skip", LogLevel.ERROR); return; }
|
||||||
|
Print("ERR SMOKE PASS: forced camp-5 terrain/fallback failure omitted the camp before spawning");
|
||||||
|
Print("ERR SMOKE PASS: required prison, radio, extraction and all four mortars retained");
|
||||||
|
Print("ERR SMOKE PASS: mission preparation completed and a real player was admitted despite the skipped camp");
|
||||||
|
Transition(ERR_Phase.DEFEAT);
|
||||||
|
Print("ERR SMOKE COMPLETE: optional site refusal does not block actual mission preparation or player admission");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -40,17 +40,20 @@ class ERR_NativeRecoveryProbe
|
|||||||
if (now < m_Wait) return 0;
|
if (now < m_Wait) return 0;
|
||||||
if (m_Step == 0)
|
if (m_Step == 0)
|
||||||
{
|
{
|
||||||
if (!Check(m_Damage && m_Controller && m_Damage.ERR_IsRevivablePlayer(), "real admitted prisoner has player-only casualty protection")) return -1;
|
if (!Check(m_Damage && m_Controller && m_Damage.ERR_IsRevivablePlayer(), "real admitted prisoner has scoped medical policy")) return -1;
|
||||||
m_Damage.EnableDamageHandling(true);
|
m_Damage.EnableDamageHandling(true);
|
||||||
// Each exceeds full native health, including blood loss and a head injury.
|
m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||||
m_Damage.GetDefaultHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
|
||||||
m_Damage.GetBloodHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
|
||||||
m_Damage.GetHeadHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
|
||||||
m_Wait = now + 1000; m_Step = 1; return 0;
|
m_Wait = now + 1000; m_Step = 1; return 0;
|
||||||
}
|
}
|
||||||
if (m_Step == 1)
|
if (m_Step == 1)
|
||||||
{
|
{
|
||||||
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Damage.GetState() != EDamageState.DESTROYED && m_Damage.GetBloodHitZone().GetDamageState() != EDamageState.DESTROYED, "otherwise lethal health, head and blood damage leave the same living unconscious body")) return -1;
|
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Damage.GetState() != EDamageState.DESTROYED && m_Damage.GetBloodHitZone().GetDamageState() != EDamageState.DESTROYED, "survivable knockout leaves the same living unconscious body")) return -1;
|
||||||
|
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness();
|
||||||
|
m_Wait = now + 35000; m_Step = 10; return 0;
|
||||||
|
}
|
||||||
|
if (m_Step == 10)
|
||||||
|
{
|
||||||
|
if (!Check(m_Mode.Active() && (ERR_Actors.CanFight(m_Player) || m_Damage.ERR_CanRecoverNaturally()), "viable natural recovery keeps the all-unconscious round alive beyond the failure grace")) return -1;
|
||||||
// Reset fixture injuries, then exercise treatment of an isolated knockout.
|
// Reset fixture injuries, then exercise treatment of an isolated knockout.
|
||||||
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||||
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ class ERR_ObserverRules
|
|||||||
if (right && !previousRight) return 1;
|
if (right && !previousRight) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
static bool Toggle(bool enabled, bool pressed, bool previous)
|
||||||
|
{
|
||||||
|
return enabled && pressed && !previous;
|
||||||
|
}
|
||||||
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
|
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
|
||||||
{
|
{
|
||||||
return active && currentBody && alive && !conscious;
|
return active && currentBody && alive && !conscious;
|
||||||
@@ -48,7 +52,7 @@ class ERR_ObserverGuidance
|
|||||||
}
|
}
|
||||||
if (!survivorAvailable)
|
if (!survivorAvailable)
|
||||||
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
||||||
return message + " Press Left / Right arrow to change teammate (or /esc spectate next / /esc spectate previous). Use camera look controls to orbit and zoom controls to change distance.";
|
return message + " Press Left / Right arrow to change teammate (or /esc spectate next / /esc spectate previous). Press Enter to switch between third-person and player view. Use camera look controls to orbit and zoom controls to change distance in third-person.";
|
||||||
}
|
}
|
||||||
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
||||||
{
|
{
|
||||||
@@ -65,18 +69,55 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
|||||||
protected float m_Yaw;
|
protected float m_Yaw;
|
||||||
protected float m_Pitch = 20;
|
protected float m_Pitch = 20;
|
||||||
protected float m_Distance = 5;
|
protected float m_Distance = 5;
|
||||||
protected bool m_LeftDown, m_RightDown;
|
protected bool m_LeftDown, m_RightDown, m_EnterDown;
|
||||||
|
protected bool m_PlayerView;
|
||||||
|
protected bool m_InputContextActive;
|
||||||
|
bool InputContextActive() { return m_InputContextActive; }
|
||||||
|
bool IsPlayerView() { return m_PlayerView; }
|
||||||
|
// Process one edge per press; menus still consume the state so holding a key
|
||||||
|
// while closing a menu cannot unexpectedly change the followed teammate.
|
||||||
|
void Input(bool enabled, bool left, bool right, bool enter)
|
||||||
|
{
|
||||||
|
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
|
int cycle = ERR_ObserverRules.Cycle(enabled, left, right, m_LeftDown, m_RightDown);
|
||||||
|
if (controller && cycle != 0) controller.ERR_CycleObserver(cycle);
|
||||||
|
if (ERR_ObserverRules.Toggle(enabled, enter, m_EnterDown)) m_PlayerView = !m_PlayerView;
|
||||||
|
m_LeftDown = left; m_RightDown = right; m_EnterDown = enter;
|
||||||
|
}
|
||||||
|
static bool PlayerView(IEntity target, out vector transform[4])
|
||||||
|
{
|
||||||
|
ChimeraCharacter character = ChimeraCharacter.Cast(target);
|
||||||
|
if (!character || !character.GetAnimation()) return false;
|
||||||
|
int head = character.GetAnimation().GetBoneIndex("Head");
|
||||||
|
if (head < 0) return false;
|
||||||
|
vector bone[4], world[4];
|
||||||
|
if (!character.GetAnimation().GetBoneMatrix(head, bone)) return false;
|
||||||
|
character.GetWorldTransform(world);
|
||||||
|
vector position = bone[3].Multiply4(world);
|
||||||
|
vector direction = world[2];
|
||||||
|
CharacterControllerComponent controls = character.GetCharacterController();
|
||||||
|
if (controls && controls.GetHeadAimingComponent()) direction = controls.GetHeadAimingComponent().GetAimingDirectionWorld();
|
||||||
|
if (direction.LengthSq() < 0.01) direction = world[2];
|
||||||
|
direction.Normalize();
|
||||||
|
Math3D.DirectionAndUpMatrix(direction, "0 1 0", transform);
|
||||||
|
// Just ahead of the face, following posture and vehicle seating. This reads
|
||||||
|
// replicated pose; it does not possess the teammate or change their camera.
|
||||||
|
transform[3] = position + direction * 0.15;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
override bool EOnCameraInit() { return true; }
|
override bool EOnCameraInit() { return true; }
|
||||||
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
||||||
{
|
{
|
||||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
bool left = (Debug.KeyState(KeyCode.KC_LEFT) & 32768) != 0;
|
m_InputContextActive = GetInputManager().IsContextActive("ERR_ObserverContext");
|
||||||
bool right = (Debug.KeyState(KeyCode.KC_RIGHT) & 32768) != 0;
|
Input(param.isManualInputEnabled, GetInputManager().GetActionValue("ERR_ObserverPrevious") > 0.5, GetInputManager().GetActionValue("ERR_ObserverNext") > 0.5, GetInputManager().GetActionValue("ERR_ObserverView") > 0.5);
|
||||||
int cycle = ERR_ObserverRules.Cycle(param.isManualInputEnabled, left, right, m_LeftDown, m_RightDown);
|
|
||||||
m_LeftDown = left; m_RightDown = right;
|
|
||||||
if (cycle != 0) controller.ERR_CycleObserver(cycle);
|
|
||||||
IEntity target = controller.ERR_ObservedEntity();
|
IEntity target = controller.ERR_ObservedEntity();
|
||||||
|
if (m_PlayerView && target && PlayerView(target, param.transform))
|
||||||
|
{
|
||||||
|
param.isDirty = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||||
vector position = aim + "0 4 -6";
|
vector position = aim + "0 4 -6";
|
||||||
IEntity exclude = target;
|
IEntity exclude = target;
|
||||||
@@ -147,6 +188,8 @@ modded class SCR_PlayerController
|
|||||||
protected ERR_ObserverReason m_ERRShownObserverReason;
|
protected ERR_ObserverReason m_ERRShownObserverReason;
|
||||||
protected bool m_ERRShownObserverAvailable;
|
protected bool m_ERRShownObserverAvailable;
|
||||||
protected float m_ERRLastCycle = -1000;
|
protected float m_ERRLastCycle = -1000;
|
||||||
|
protected IEntity m_ERRRecoveryBody;
|
||||||
|
protected float m_ERRRecoveryDeadline = -1;
|
||||||
|
|
||||||
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
ERR_ObserverReason ERR_ObserverState() { return m_ERRObserverReason; }
|
||||||
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
vector ERR_ObserverAnchor() { return m_ERRObserverAnchor; }
|
||||||
@@ -266,6 +309,7 @@ modded class SCR_PlayerController
|
|||||||
protected void ERR_UpdateObserverClient()
|
protected void ERR_UpdateObserverClient()
|
||||||
{
|
{
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
ERR_UpdateRecoveryCamera(mode);
|
||||||
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
|
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
|
||||||
if (!observe)
|
if (!observe)
|
||||||
{
|
{
|
||||||
@@ -299,6 +343,31 @@ modded class SCR_PlayerController
|
|||||||
m_ERRShownObserverAvailable = survivorAvailable;
|
m_ERRShownObserverAvailable = survivorAvailable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Track the actual local body even when an editor/other camera owns the view.
|
||||||
|
// Run once per recovery, with retries for asynchronous editor close/camera setup.
|
||||||
|
protected void ERR_UpdateRecoveryCamera(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
IEntity body = GetControlledEntity();
|
||||||
|
if (!mode || !mode.Active() || !ERR_Actors.Alive(body))
|
||||||
|
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
|
||||||
|
if (!ERR_Actors.CanFight(body))
|
||||||
|
{ m_ERRRecoveryBody = body; m_ERRRecoveryDeadline = -1; return; }
|
||||||
|
if (!m_ERRRecoveryBody) return;
|
||||||
|
if (body != m_ERRRecoveryBody)
|
||||||
|
{ m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1; return; }
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
if (m_ERRRecoveryDeadline < 0) m_ERRRecoveryDeadline = now + 10000;
|
||||||
|
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
|
||||||
|
if (editor && editor.IsOpened()) editor.Close(false);
|
||||||
|
CameraManager manager = GetGame().GetCameraManager();
|
||||||
|
CameraBase playerCamera = GetPlayerCamera();
|
||||||
|
if (manager && playerCamera) manager.SetCamera(playerCamera);
|
||||||
|
if ((manager && playerCamera && manager.CurrentCamera() == playerCamera && (!editor || !editor.IsOpened())) || now >= m_ERRRecoveryDeadline)
|
||||||
|
{
|
||||||
|
if (now >= m_ERRRecoveryDeadline) Print("Reintroduced [spectator]: recovery camera return timed out waiting for editor/camera; player body remains controlled", LogLevel.WARNING);
|
||||||
|
m_ERRRecoveryBody = null; m_ERRRecoveryDeadline = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
protected void ERR_CloseObserverCamera()
|
protected void ERR_CloseObserverCamera()
|
||||||
{
|
{
|
||||||
m_ERRObserverReady = false;
|
m_ERRObserverReady = false;
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ modded class SCR_CharacterDamageManagerComponent
|
|||||||
if (!Replication.IsServer()) return;
|
if (!Replication.IsServer()) return;
|
||||||
m_ERRRevivablePlayer = true;
|
m_ERRRevivablePlayer = true;
|
||||||
SetPermitUnconsciousness(true, true);
|
SetPermitUnconsciousness(true, true);
|
||||||
|
SetRegenScale(0.25, true);
|
||||||
}
|
}
|
||||||
bool ERR_IsRevivablePlayer() { return m_ERRRevivablePlayer; }
|
bool ERR_IsRevivablePlayer() { return m_ERRRevivablePlayer; }
|
||||||
|
|
||||||
@@ -44,11 +45,34 @@ modded class SCR_CharacterDamageManagerComponent
|
|||||||
if (!m_ERRRevivablePlayer) { super.Kill(instigator); return; }
|
if (!m_ERRRevivablePlayer) { super.Kill(instigator); return; }
|
||||||
ERR_RequestKnockout();
|
ERR_RequestKnockout();
|
||||||
}
|
}
|
||||||
|
// Include delayed but viable native regeneration when deciding team defeat.
|
||||||
|
// A stopped/zero-rate effect or an actively bleeding casualty cannot self-rescue.
|
||||||
|
bool ERR_CanRecoverNaturally()
|
||||||
|
{
|
||||||
|
if (!m_ERRRevivablePlayer || IsBleeding()) return false;
|
||||||
|
array<ref SCR_PersistentDamageEffect> effects = GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true);
|
||||||
|
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||||
|
{
|
||||||
|
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
||||||
|
if (!dot || dot.GetDamageType() != EDamageType.REGENERATION || dot.GetDPS() >= 0) continue;
|
||||||
|
HitZone zone = dot.GetAffectedHitZone();
|
||||||
|
if (!zone || zone.GetHealthScaled() >= 1) continue;
|
||||||
|
if (zone == GetBloodHitZone()) return true;
|
||||||
|
if (zone == GetResilienceHitZone() && GetResilienceRegenScale() > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// A player always retains a living, treatable body.
|
||||||
override protected bool IsIndefinitelyUnconscious(bool onlyPlayers = true)
|
override protected bool IsIndefinitelyUnconscious(bool onlyPlayers = true)
|
||||||
{
|
{
|
||||||
if (m_ERRRevivablePlayer) return false;
|
if (m_ERRRevivablePlayer) return false;
|
||||||
return super.IsIndefinitelyUnconscious(onlyPlayers);
|
return super.IsIndefinitelyUnconscious(onlyPlayers);
|
||||||
}
|
}
|
||||||
|
override float GetResilienceRegenScale()
|
||||||
|
{
|
||||||
|
if (m_ERRRevivablePlayer && IsBleeding()) return 0;
|
||||||
|
return super.GetResilienceRegenScale();
|
||||||
|
}
|
||||||
bool ERR_TreatmentInProgress()
|
bool ERR_TreatmentInProgress()
|
||||||
{
|
{
|
||||||
if (m_aBeingHealedGroup && !m_aBeingHealedGroup.IsEmpty()) return true;
|
if (m_aBeingHealedGroup && !m_aBeingHealedGroup.IsEmpty()) return true;
|
||||||
@@ -56,8 +80,7 @@ modded class SCR_CharacterDamageManagerComponent
|
|||||||
foreach (SCR_PersistentDamageEffect effect : effects)
|
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||||
{
|
{
|
||||||
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
||||||
// Includes saline and ACE epinephrine. Passive regeneration is checked by
|
// Includes saline and epinephrine; passive regeneration is evaluated separately.
|
||||||
// observed progress, so a stalled regeneration effect cannot block defeat.
|
|
||||||
if (dot && dot.GetDamageType() == EDamageType.HEALING && dot.GetDPS() < 0) return true;
|
if (dot && dot.GetDamageType() == EDamageType.HEALING && dot.GetDPS() < 0) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -77,3 +100,16 @@ modded class SCR_HitZone
|
|||||||
return allowed;
|
return allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Native resilience ignores the per-character regeneration setting. Apply the
|
||||||
|
// same slow rate here, only to passive recovery; injected medicine keeps its DPS.
|
||||||
|
modded class SCR_CharacterResilienceHitZone
|
||||||
|
{
|
||||||
|
override float CalculatePassiveRegenDPS(bool considerRegenScale = true)
|
||||||
|
{
|
||||||
|
float rate = super.CalculatePassiveRegenDPS(considerRegenScale);
|
||||||
|
SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());
|
||||||
|
if (considerRegenScale && manager && manager.ERR_IsRevivablePlayer()) return rate * 0.25;
|
||||||
|
return rate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ class ERR_RecoveryService
|
|||||||
protected float m_StalledSince = -1;
|
protected float m_StalledSince = -1;
|
||||||
protected ref map<IEntity, ref ERR_RecoverySample> m_Samples = new map<IEntity, ref ERR_RecoverySample>();
|
protected ref map<IEntity, ref ERR_RecoverySample> m_Samples = new map<IEntity, ref ERR_RecoverySample>();
|
||||||
protected string m_Message;
|
protected string m_Message;
|
||||||
|
protected float m_NextMedicalLog;
|
||||||
|
protected string m_MedicalDetails;
|
||||||
|
protected bool m_DefeatLogged;
|
||||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||||
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
ScriptInvoker OnChanged() { return m_OnChanged; }
|
ScriptInvoker OnChanged() { return m_OnChanged; }
|
||||||
@@ -46,11 +49,29 @@ class ERR_RecoveryService
|
|||||||
int Incapacitated() { return m_Incapacitated; }
|
int Incapacitated() { return m_Incapacitated; }
|
||||||
int Reserved() { return m_Reserved; }
|
int Reserved() { return m_Reserved; }
|
||||||
string Message() { return m_Message; }
|
string Message() { return m_Message; }
|
||||||
bool DefeatDue(float now) { return ERR_RecoveryRules.DefeatDue(m_StalledSince, now); }
|
bool DefeatDue(float now)
|
||||||
|
{
|
||||||
|
bool due = ERR_RecoveryRules.DefeatDue(m_StalledSince, now);
|
||||||
|
if (due && !m_DefeatLogged)
|
||||||
|
{
|
||||||
|
m_DefeatLogged = true;
|
||||||
|
ERR_HQ.Event("MEDICAL DEFEAT", "30 seconds without a viable rescuer or recovery; " + m_MedicalDetails);
|
||||||
|
}
|
||||||
|
return due;
|
||||||
|
}
|
||||||
|
|
||||||
|
float StalledSeconds(float now)
|
||||||
|
{
|
||||||
|
if (m_StalledSince < 0) return 0;
|
||||||
|
return Math.Max(0, now - m_StalledSince) / 1000;
|
||||||
|
}
|
||||||
|
|
||||||
void Tick()
|
void Tick()
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
bool logMedical = now >= m_NextMedicalLog;
|
||||||
|
string medicalDetails;
|
||||||
int conscious, incapacitated, reserved, reservedRescuers, recovering;
|
int conscious, incapacitated, reserved, reservedRescuers, recovering;
|
||||||
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
||||||
array<ref ERR_PlayerLife> lives = {};
|
array<ref ERR_PlayerLife> lives = {};
|
||||||
@@ -76,22 +97,30 @@ class ERR_RecoveryService
|
|||||||
if (medical.GetResilienceHitZone()) sample.Resilience = medical.GetResilienceHitZone().GetHealth();
|
if (medical.GetResilienceHitZone()) sample.Resilience = medical.GetResilienceHitZone().GetHealth();
|
||||||
ERR_RecoverySample previous = m_Samples.Get(life.Actor);
|
ERR_RecoverySample previous = m_Samples.Get(life.Actor);
|
||||||
bool improving = previous && (sample.Blood > previous.Blood + 0.001 || sample.Resilience > previous.Resilience + 0.001);
|
bool improving = previous && (sample.Blood > previous.Blood + 0.001 || sample.Resilience > previous.Resilience + 0.001);
|
||||||
if (improving || medical.ERR_TreatmentInProgress()) recovering++;
|
bool treatment = medical.ERR_TreatmentInProgress();
|
||||||
|
bool natural = medical.ERR_CanRecoverNaturally();
|
||||||
|
if (improving || treatment || natural) recovering++;
|
||||||
|
medicalDetails += string.Format("player=%1 connected=%2 blood=%3 resilience=%4 bleeding=%5 improving=%6 treatment=%7 naturalRecovery=%8; ", life.ConnectionId, life.Connected, sample.Blood, sample.Resilience, medical.IsBleeding(), improving, treatment, natural);
|
||||||
nextSamples.Insert(life.Actor, sample);
|
nextSamples.Insert(life.Actor, sample);
|
||||||
}
|
}
|
||||||
|
m_MedicalDetails = medicalDetails;
|
||||||
m_Samples = nextSamples;
|
m_Samples = nextSamples;
|
||||||
m_Conscious = conscious;
|
m_Conscious = conscious;
|
||||||
m_Incapacitated = incapacitated;
|
m_Incapacitated = incapacitated;
|
||||||
m_Reserved = reserved;
|
m_Reserved = reserved;
|
||||||
float now = GetGame().GetWorld().GetWorldTime();
|
|
||||||
bool stalled = ERR_RecoveryRules.Stalled(conscious, incapacitated, reservedRescuers, recovering, GetGame().GetPlayerManager().GetPlayerCount() > 0);
|
bool stalled = ERR_RecoveryRules.Stalled(conscious, incapacitated, reservedRescuers, recovering, GetGame().GetPlayerManager().GetPlayerCount() > 0);
|
||||||
if (!stalled) m_StalledSince = -1;
|
if (!stalled) m_StalledSince = -1;
|
||||||
else if (m_StalledSince < 0 || now < m_StalledSince) m_StalledSince = now;
|
else if (m_StalledSince < 0 || now < m_StalledSince) m_StalledSince = now;
|
||||||
|
if (logMedical && incapacitated > 0)
|
||||||
|
{
|
||||||
|
m_NextMedicalLog = now + 10000;
|
||||||
|
ERR_HQ.Event("MEDICAL STATUS", string.Format("conscious=%1 unconscious=%2 reservedRescuers=%3 recovering=%4 defeatTimer=%5/30s; %6", conscious, incapacitated, reservedRescuers, recovering, StalledSeconds(now), medicalDetails));
|
||||||
|
}
|
||||||
string message;
|
string message;
|
||||||
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
|
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
|
||||||
{
|
{
|
||||||
message = "No conscious prisoner is currently available to help.";
|
message = "No conscious prisoner is currently available to help.";
|
||||||
if (recovering > 0) message += " Treatment or recovery is progressing; the round continues.";
|
if (recovering > 0) message += " Treatment or natural recovery is possible; the round continues.";
|
||||||
else if (reservedRescuers > 0) message += " A conscious prisoner's reconnection may provide help.";
|
else if (reservedRescuers > 0) message += " A conscious prisoner's reconnection may provide help.";
|
||||||
else message += " Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
else message += " Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
m_Started = true;
|
m_Started = true;
|
||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
string historicalFixture;
|
string historicalFixture;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "optional-site-skip" && historicalFixture != "infantry-combat" && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||||
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||||
#endif
|
#endif
|
||||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||||
@@ -38,7 +38,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
// Existing deterministic probes intentionally use authored coordinates.
|
// Existing deterministic probes intentionally use authored coordinates.
|
||||||
string fixture;
|
string fixture;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "optional-site-skip" && fixture != "infantry-combat" && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||||
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||||
#endif
|
#endif
|
||||||
m_Generated = new ERR_GeneratedSites();
|
m_Generated = new ERR_GeneratedSites();
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ modded class SCR_PlayerController
|
|||||||
if (m_ERRCameraProbeTarget && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "unconscious-camera") return m_ERRCameraProbeTarget;
|
if (m_ERRCameraProbeTarget && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "unconscious-camera") return m_ERRCameraProbeTarget;
|
||||||
return super.ERR_ObservedEntity();
|
return super.ERR_ObservedEntity();
|
||||||
}
|
}
|
||||||
|
ERR_FollowSurvivorCameraComponent ERR_ProbeFollowComponent()
|
||||||
|
{
|
||||||
|
if (!m_ERRObserverCamera) return null;
|
||||||
|
return ERR_FollowSurvivorCameraComponent.Cast(m_ERRObserverCamera.FindCameraComponent(ERR_FollowSurvivorCameraComponent));
|
||||||
|
}
|
||||||
bool ERR_ProbeCameraActive()
|
bool ERR_ProbeCameraActive()
|
||||||
{
|
{
|
||||||
return m_ERRObserverCamera && GetGame().GetCameraManager().CurrentCamera() == m_ERRObserverCamera;
|
return m_ERRObserverCamera && GetGame().GetCameraManager().CurrentCamera() == m_ERRObserverCamera;
|
||||||
@@ -24,6 +29,7 @@ modded class ERR_GameMode
|
|||||||
protected float m_ERRCameraWait;
|
protected float m_ERRCameraWait;
|
||||||
protected IEntity m_ERRCameraBody, m_ERRCameraTarget;
|
protected IEntity m_ERRCameraBody, m_ERRCameraTarget;
|
||||||
protected SCR_CharacterDamageManagerComponent m_ERRCameraDamage;
|
protected SCR_CharacterDamageManagerComponent m_ERRCameraDamage;
|
||||||
|
protected SCR_ManualCamera m_ERRCameraOther;
|
||||||
protected override void EOnInit(IEntity owner)
|
protected override void EOnInit(IEntity owner)
|
||||||
{
|
{
|
||||||
super.EOnInit(owner);
|
super.EOnInit(owner);
|
||||||
@@ -93,6 +99,7 @@ modded class ERR_GameMode
|
|||||||
if (m_ERRCameraStage == 2)
|
if (m_ERRCameraStage == 2)
|
||||||
{
|
{
|
||||||
vector camera = GetGame().GetCameraManager().CurrentCamera().GetOrigin();
|
vector camera = GetGame().GetCameraManager().CurrentCamera().GetOrigin();
|
||||||
|
PrintFormat("ERR CAMERA TRACK: camera=%1 target=%2 body=%3 active=%4 conscious=%5 reason=%6", camera, m_ERRCameraTarget.GetOrigin(), m_ERRCameraBody.GetOrigin(), pc.ERR_ProbeCameraActive(), ERR_Actors.CanFight(m_ERRCameraBody), pc.ERR_ObserverState());
|
||||||
if (!ERR_CameraCheck(vector.Distance(camera, m_ERRCameraTarget.GetOrigin()) < 13 && vector.Distance(camera, m_ERRCameraBody.GetOrigin()) > 15, "camera follows target in 3D without moving the casualty")) return;
|
if (!ERR_CameraCheck(vector.Distance(camera, m_ERRCameraTarget.GetOrigin()) < 13 && vector.Distance(camera, m_ERRCameraBody.GetOrigin()) > 15, "camera follows target in 3D without moving the casualty")) return;
|
||||||
m_ERRCameraTarget.SetOrigin(ERR_Actors.Ground(m_ERRCameraTarget.GetOrigin() + "15 0 0"));
|
m_ERRCameraTarget.SetOrigin(ERR_Actors.Ground(m_ERRCameraTarget.GetOrigin() + "15 0 0"));
|
||||||
m_ERRCameraStage = 3; m_ERRCameraWait = now + 1500; return;
|
m_ERRCameraStage = 3; m_ERRCameraWait = now + 1500; return;
|
||||||
@@ -100,6 +107,39 @@ modded class ERR_GameMode
|
|||||||
if (m_ERRCameraStage == 3)
|
if (m_ERRCameraStage == 3)
|
||||||
{
|
{
|
||||||
if (!ERR_CameraCheck(vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) < 13, "camera tracks a moving target")) return;
|
if (!ERR_CameraCheck(vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) < 13, "camera tracks a moving target")) return;
|
||||||
|
InputManager input = GetGame().GetInputManager();
|
||||||
|
ERR_FollowSurvivorCameraComponent follow = pc.ERR_ProbeFollowComponent();
|
||||||
|
if (!ERR_CameraCheck(follow != null, "follow component exists")) return;
|
||||||
|
ResourceName configuredInput;
|
||||||
|
GameProject.GetModuleConfig("InputManagerSettings").Get("Default", configuredInput);
|
||||||
|
PrintFormat("ERR CAMERA INPUT: default=%1 context=%2 previous=%3 next=%4 view=%5 orbit=%6", configuredInput, input.IsContextActive("ERR_ObserverContext"), input.IsActionActive("ERR_ObserverPrevious"), input.IsActionActive("ERR_ObserverNext"), input.IsActionActive("ERR_ObserverView"), input.IsActionActive("ManualCameraRotateYaw"));
|
||||||
|
for (int inputIndex = 0; inputIndex < input.GetActionCount(); inputIndex++)
|
||||||
|
{
|
||||||
|
string inputName = input.GetActionName(inputIndex);
|
||||||
|
if (inputName.Contains("Observer") || inputName == "ManualCameraRotateYaw") Print("ERR CAMERA INPUT REGISTERED: " + inputName);
|
||||||
|
}
|
||||||
|
if (!ERR_CameraCheck(follow.InputContextActive() && input.IsActionActive("ERR_ObserverPrevious") && input.IsActionActive("ERR_ObserverNext") && input.IsActionActive("ERR_ObserverView") && input.IsActionActive("ManualCameraRotateYaw"), "dedicated arrow/Enter actions and inherited orbit inputs loaded and active")) return;
|
||||||
|
follow.Input(true, false, false, true);
|
||||||
|
if (!ERR_CameraCheck(follow.IsPlayerView(), "Enter handler selects player view")) return;
|
||||||
|
follow.Input(true, false, false, true);
|
||||||
|
if (!ERR_CameraCheck(follow.IsPlayerView(), "held Enter does not toggle again")) return;
|
||||||
|
follow.Input(true, false, false, false);
|
||||||
|
m_ERRCameraStage = 30; m_ERRCameraWait = now + 1000; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 30)
|
||||||
|
{
|
||||||
|
vector expected[4];
|
||||||
|
if (!ERR_CameraCheck(ERR_FollowSurvivorCameraComponent.PlayerView(m_ERRCameraTarget, expected) && vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), expected[3]) < 0.1, "native camera follows teammate head in player view")) return;
|
||||||
|
pc.ERR_ProbeFollowComponent().Input(true, false, false, true);
|
||||||
|
pc.ERR_ProbeFollowComponent().Input(true, false, false, false);
|
||||||
|
m_ERRCameraStage = 31; m_ERRCameraWait = now + 1000; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 31)
|
||||||
|
{
|
||||||
|
if (!ERR_CameraCheck(!pc.ERR_ProbeFollowComponent().IsPlayerView() && vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) > 2, "second Enter restores orbit view")) return;
|
||||||
|
Resource otherResource = Resource.Load("{EB1210AAE1BB8F54}Prefabs/ERR/SpectatorCamera.et");
|
||||||
|
m_ERRCameraOther = SCR_ManualCamera.Cast(GetGame().SpawnEntityPrefab(otherResource, GetWorld()));
|
||||||
|
if (!ERR_CameraCheck(m_ERRCameraOther && GetGame().GetCameraManager().CurrentCamera() == m_ERRCameraOther && !pc.ERR_ProbeCameraActive(), "separate manual camera takes over before recovery")) return;
|
||||||
pc.ERR_ProbeCameraTarget(null); ERR_Actors.DeleteSpawned(m_ERRCameraTarget); m_ERRCameraTarget = null;
|
pc.ERR_ProbeCameraTarget(null); ERR_Actors.DeleteSpawned(m_ERRCameraTarget); m_ERRCameraTarget = null;
|
||||||
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||||
m_ERRCameraDamage.FullHeal(); m_ERRCameraStage = 4; m_ERRCameraWait = now + 4000; return;
|
m_ERRCameraDamage.FullHeal(); m_ERRCameraStage = 4; m_ERRCameraWait = now + 4000; return;
|
||||||
@@ -108,6 +148,7 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (!ERR_CameraCheck(ERR_Actors.CanFight(m_ERRCameraBody) && pc.GetControlledEntity() == m_ERRCameraBody && pc.ERR_ObserverState() == ERR_ObserverReason.NONE && !pc.ERR_HasObserverCamera(), "native healing closes spectator and retains the original conscious player")) return;
|
if (!ERR_CameraCheck(ERR_Actors.CanFight(m_ERRCameraBody) && pc.GetControlledEntity() == m_ERRCameraBody && pc.ERR_ObserverState() == ERR_ObserverReason.NONE && !pc.ERR_HasObserverCamera(), "native healing closes spectator and retains the original conscious player")) return;
|
||||||
if (!ERR_CameraCheck(GetGame().GetCameraManager().CurrentCamera() == pc.GetPlayerCamera(), "normal player camera restored")) return;
|
if (!ERR_CameraCheck(GetGame().GetCameraManager().CurrentCamera() == pc.GetPlayerCamera(), "normal player camera restored")) return;
|
||||||
|
if (m_ERRCameraOther) { m_ERRCameraOther.Terminate(); delete m_ERRCameraOther; m_ERRCameraOther = null; }
|
||||||
if (!ERR_CameraCheck(character.GetWeaponManagerComponent().GetCurrentWeapon() && character.GetWeaponManagerComponent().GetCurrentWeapon().GetOwner() == m_ERRCameraWeapon, "revival equips the same rifle without ground pickup")) return;
|
if (!ERR_CameraCheck(character.GetWeaponManagerComponent().GetCurrentWeapon() && character.GetWeaponManagerComponent().GetCurrentWeapon().GetOwner() == m_ERRCameraWeapon, "revival equips the same rifle without ground pickup")) return;
|
||||||
if (!ERR_CameraCheck(Settings().Set("keepWeaponOnUnconscious", 0), "retention disabled through persistent setting")) return;
|
if (!ERR_CameraCheck(Settings().Set("keepWeaponOnUnconscious", 0), "retention disabled through persistent setting")) return;
|
||||||
ERR_CameraKnockout(); m_ERRCameraStage = 5; m_ERRCameraWait = now + 3000; return;
|
ERR_CameraKnockout(); m_ERRCameraStage = 5; m_ERRCameraWait = now + 3000; return;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class ERR_AirSupportPolicy
|
|||||||
float ConsideringSince = -1;
|
float ConsideringSince = -1;
|
||||||
float NextDrop;
|
float NextDrop;
|
||||||
string Reason = "waiting for delivered intelligence";
|
string Reason = "waiting for delivered intelligence";
|
||||||
static string Refusal(ERR_AirSupportSituation s)
|
static string Refusal(ERR_AirSupportSituation s, bool checkAirspace = true)
|
||||||
{
|
{
|
||||||
if (!s || !s.AircraftReady) return "no ready helicopter passenger load";
|
if (!s || !s.AircraftReady) return "no ready helicopter passenger load";
|
||||||
if (!ERR_HQMemoryRules.Finite(s.Age) || !ERR_HQMemoryRules.Finite(s.Confidence) || !ERR_HQMemoryRules.Finite(s.Radius) || !ERR_HQMemoryRules.Finite(s.Speed)) return "invalid memory sample";
|
if (!ERR_HQMemoryRules.Finite(s.Age) || !ERR_HQMemoryRules.Finite(s.Confidence) || !ERR_HQMemoryRules.Finite(s.Radius) || !ERR_HQMemoryRules.Finite(s.Speed)) return "invalid memory sample";
|
||||||
@@ -31,13 +31,16 @@ class ERR_AirSupportPolicy
|
|||||||
int needed = Math.Clamp(s.Spotted * 2, 4, 12);
|
int needed = Math.Clamp(s.Spotted * 2, 4, 12);
|
||||||
if (s.CommittedFit >= needed) return "ground forces already committed in sufficient strength";
|
if (s.CommittedFit >= needed) return "ground forces already committed in sufficient strength";
|
||||||
if (s.GroundFit + s.CommittedFit >= needed && s.ConfirmedLosses < 2) return "nearby ground forces can handle this contact";
|
if (s.GroundFit + s.CommittedFit >= needed && s.ConfirmedLosses < 2) return "nearby ground forces can handle this contact";
|
||||||
if (s.ArtilleryBusy) return "mortar order or shells occupy the approach area";
|
if (checkAirspace && s.ArtilleryBusy) return "mortar order or shells occupy the approach area";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
bool Consider(ERR_AirSupportSituation s, float now)
|
bool Consider(ERR_AirSupportSituation s, float now)
|
||||||
{
|
{
|
||||||
if (!ERR_HQMemoryRules.Finite(now) || now < 0) return false;
|
if (!ERR_HQMemoryRules.Finite(now) || now < 0) return false;
|
||||||
string refusal = Refusal(s);
|
// Artillery temporarily closes the approach; it does not invalidate fresh
|
||||||
|
// contact history or the confirmed lack of ground support. Restarting the
|
||||||
|
// timer on every salvo could prevent a drop through every clear interval.
|
||||||
|
string refusal = Refusal(s, false);
|
||||||
if (!refusal.IsEmpty()) { Candidate = 0;
|
if (!refusal.IsEmpty()) { Candidate = 0;
|
||||||
ConsideringSince = -1;
|
ConsideringSince = -1;
|
||||||
Reason = refusal;
|
Reason = refusal;
|
||||||
@@ -49,6 +52,9 @@ class ERR_AirSupportPolicy
|
|||||||
if (Candidate != s.BeliefId || ConsideringSince < 0) { Candidate = s.BeliefId;
|
if (Candidate != s.BeliefId || ConsideringSince < 0) { Candidate = s.BeliefId;
|
||||||
ConsideringSince = now;
|
ConsideringSince = now;
|
||||||
}
|
}
|
||||||
|
if (s.ArtilleryBusy) { Reason = Refusal(s);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
float delay = 45000;
|
float delay = 45000;
|
||||||
if (s.ConfirmedLosses >= 2) delay = 20000;
|
if (s.ConfirmedLosses >= 2) delay = 20000;
|
||||||
if (now - ConsideringSince < delay) { Reason = "confirming persistent contact and ground-force shortfall";
|
if (now - ConsideringSince < delay) { Reason = "confirming persistent contact and ground-force shortfall";
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Bounded retries only after boarding/approach progress stops. Reissuing a
|
||||||
|
// waypoint while soldiers are still closing on the truck interrupts native AI.
|
||||||
|
class ERR_ConvoyBoardingProgress
|
||||||
|
{
|
||||||
|
int Seated = -1;
|
||||||
|
float Distance = -1;
|
||||||
|
float LastProgress;
|
||||||
|
float NextRetry;
|
||||||
|
int Retries;
|
||||||
|
bool Retry(int seated, float distance, float now)
|
||||||
|
{
|
||||||
|
if (Seated != seated || Distance < 0 || distance <= Distance - 5)
|
||||||
|
{
|
||||||
|
Seated = seated; Distance = distance; LastProgress = now;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (Retries >= 3 || now - LastProgress < 30000 || now < NextRetry) return false;
|
||||||
|
Retries++; NextRetry = now + 30000;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ class ERR_ConvoyRoadPlan
|
|||||||
}
|
}
|
||||||
return route.Count() < bestRoute.Count();
|
return route.Count() < bestRoute.Count();
|
||||||
}
|
}
|
||||||
static bool Build(vector seed, out array<vector> positions, out array<vector> angles, out ERR_ForceRoute route)
|
static bool Build(vector seed, out array<vector> positions, out array<vector> angles, out ERR_ForceRoute route, array<vector> excludedStarts = null, float separation = 0)
|
||||||
{
|
{
|
||||||
positions = {}; angles = {}; route = null;
|
positions = {}; angles = {}; route = null;
|
||||||
if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(seed)) return false;
|
if (!Replication.IsServer() || !ERR_ForceRules.FiniteVector(seed)) return false;
|
||||||
@@ -144,6 +144,10 @@ class ERR_ConvoyRoadPlan
|
|||||||
ERR_ForceRoute polyline;
|
ERR_ForceRoute polyline;
|
||||||
float distance;
|
float distance;
|
||||||
if (!FromPolyline(points, seed, candidatePositions, candidateAngles, polyline, distance, candidateIndex)) continue;
|
if (!FromPolyline(points, seed, candidatePositions, candidateAngles, polyline, distance, candidateIndex)) continue;
|
||||||
|
// Exclude occupied deployment areas before selecting the best formation.
|
||||||
|
// Returning the nearest clear-but-too-close road used to hide valid
|
||||||
|
// alternatives from startup's outer separation check.
|
||||||
|
if (excludedStarts && !ERR_SupplyConvoyRules.Separated(candidatePositions[0], excludedStarts, separation)) continue;
|
||||||
for (int i = 0; i < candidatePositions.Count(); i++) candidatePositions[i] = ERR_Actors.Ground(candidatePositions[i]);
|
for (int i = 0; i < candidatePositions.Count(); i++) candidatePositions[i] = ERR_Actors.Ground(candidatePositions[i]);
|
||||||
ERR_ForceRoute grounded = new ERR_ForceRoute();
|
ERR_ForceRoute grounded = new ERR_ForceRoute();
|
||||||
bool valid = true;
|
bool valid = true;
|
||||||
|
|||||||
@@ -80,17 +80,21 @@ class ERR_EnemyEquipmentEntry
|
|||||||
ref ERR_EnemyOptics Optics;
|
ref ERR_EnemyOptics Optics;
|
||||||
ref ERR_EnemyLauncherGrant Launcher = new ERR_EnemyLauncherGrant();
|
ref ERR_EnemyLauncherGrant Launcher = new ERR_EnemyLauncherGrant();
|
||||||
ref ERR_EnemyMineGrant Mines = new ERR_EnemyMineGrant();
|
ref ERR_EnemyMineGrant Mines = new ERR_EnemyMineGrant();
|
||||||
|
ref ERR_EnemyMedicine Medicine = new ERR_EnemyMedicine();
|
||||||
int SkillChecks;
|
int SkillChecks;
|
||||||
bool Released;
|
bool Released;
|
||||||
void Check(ERR_GameMode mode)
|
void Check(ERR_GameMode mode)
|
||||||
{
|
{
|
||||||
|
Medicine.Check(mode, Group, Actor);
|
||||||
|
// Medic storage is for treatment; do not consume it with optional mine/AA grants.
|
||||||
|
if (Actor && Actor.GetPrefabData() && ERR_EnemyMedicalCatalog.Medic(Actor.GetPrefabData().GetPrefabName())) return;
|
||||||
Launcher.Check(mode, Group, Actor);
|
Launcher.Check(mode, Group, Actor);
|
||||||
Mines.Check(mode, Group, Actor);
|
Mines.Check(mode, Group, Actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Stop(ERR_GameMode mode)
|
void Stop(ERR_GameMode mode)
|
||||||
{
|
{
|
||||||
Launcher.Stop(); Mines.Stop();
|
Launcher.Stop(); Mines.Stop(); Medicine.Stop();
|
||||||
Released = true;
|
Released = true;
|
||||||
if (Optics) Optics.Stop();
|
if (Optics) Optics.Stop();
|
||||||
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
class ERR_EnemyMedicalCatalog
|
||||||
|
{
|
||||||
|
static const ResourceName EPI = "{C68512DB5FC74698}Prefabs/ERR/Epinephrine.et";
|
||||||
|
static const ResourceName MORPHINE = "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et";
|
||||||
|
static bool Specialist(string path)
|
||||||
|
{
|
||||||
|
path.ToLower();
|
||||||
|
return path.Contains("_sf") || path.Contains("spetsnaz") || path.Contains("greenberets") || path.Contains("special") || path.Contains("recon") || path.Contains("marsoc");
|
||||||
|
}
|
||||||
|
static bool Medic(string path) { path.ToLower(); return path.Contains("medic"); }
|
||||||
|
static ResourceName Crew(ERR_GameMode mode, bool medic)
|
||||||
|
{
|
||||||
|
string key = mode.EnemyFaction().GetFactionKey();
|
||||||
|
if (key == "USSR")
|
||||||
|
{
|
||||||
|
if (medic) return "{8CA70597606992EC}Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF_Medic.et";
|
||||||
|
return "{2DB452B3EC386B92}Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF.et";
|
||||||
|
}
|
||||||
|
if (key == "US")
|
||||||
|
{
|
||||||
|
if (medic) return "{9A234C5857D92187}Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF_Medic.et";
|
||||||
|
return "{36E28C628A2F83CB}Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF.et";
|
||||||
|
}
|
||||||
|
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||||
|
if (definition && definition.Options)
|
||||||
|
foreach (ERR_ContentOption option : definition.Options)
|
||||||
|
{
|
||||||
|
if (!option || option.NativeFactionKey != key) continue;
|
||||||
|
ResourceName configured = option.ResponseSoldier;
|
||||||
|
if (medic) configured = option.ResponseMedic;
|
||||||
|
if (ERR_EnemyLoadoutCatalog.Available(configured)) return configured;
|
||||||
|
}
|
||||||
|
SCR_Faction faction = SCR_Faction.Cast(mode.EnemyFaction());
|
||||||
|
SCR_EntityCatalog catalog;
|
||||||
|
if (faction) catalog = faction.GetFactionEntityCatalogOfType(EEntityCatalogType.CHARACTER, false);
|
||||||
|
array<SCR_EntityCatalogEntry> entries = {};
|
||||||
|
if (catalog) catalog.GetEntityList(entries);
|
||||||
|
ResourceName fallback;
|
||||||
|
for (int i = 0; i < Math.Min(entries.Count(), 4096); i++)
|
||||||
|
{
|
||||||
|
SCR_EntityCatalogEntry entry = entries[i];
|
||||||
|
if (!entry || !entry.IsEnabled()) continue;
|
||||||
|
ResourceName prefab = entry.GetPrefab();
|
||||||
|
if (Medic(prefab) != medic || !ERR_EnemyLoadoutCatalog.Available(prefab)) continue;
|
||||||
|
if (Specialist(prefab)) return prefab;
|
||||||
|
if (fallback.IsEmpty()) fallback = prefab;
|
||||||
|
}
|
||||||
|
if (!fallback.IsEmpty()) return fallback;
|
||||||
|
// Never spawn a medic belonging to the wrong faction.
|
||||||
|
if (medic) return "";
|
||||||
|
return mode.FactionSelection().Soldier();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// One finite startup allowance per physical actor, never a periodic refill.
|
||||||
|
class ERR_EnemyMedicine
|
||||||
|
{
|
||||||
|
protected bool m_Started;
|
||||||
|
protected bool m_Done;
|
||||||
|
protected int m_Epi;
|
||||||
|
protected int m_Morphine;
|
||||||
|
protected int m_Checks;
|
||||||
|
protected int m_Issued;
|
||||||
|
protected bool m_Medic;
|
||||||
|
protected ref ERR_EnemyEquipmentCallback m_Operation;
|
||||||
|
void Stop() { m_Done = true; }
|
||||||
|
protected void Finish(IEntity actor, string reason)
|
||||||
|
{
|
||||||
|
m_Done = true;
|
||||||
|
ERR_HQ.Event("MEDICAL EQUIPMENT", string.Format("soldier=%1 position=%2 medic=%3 added=%4; %5", actor, actor.GetOrigin(), m_Medic, m_Issued, reason));
|
||||||
|
}
|
||||||
|
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||||
|
{
|
||||||
|
if (m_Done || !ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) return;
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
if (++m_Checks > 30) { Finish(actor, "startup deadline; no replacement of pending/looted medicine"); return; }
|
||||||
|
if (!inventory) return;
|
||||||
|
array<IEntity> items = {}; inventory.GetItems(items);
|
||||||
|
if (items.Count() > 512) { Finish(actor, "inventory inspection limit"); return; }
|
||||||
|
if (!m_Started)
|
||||||
|
{
|
||||||
|
m_Started = true;
|
||||||
|
string prefab;
|
||||||
|
if (actor.GetPrefabData()) prefab = actor.GetPrefabData().GetPrefabName();
|
||||||
|
ERR_HQ hq = ERR_HQ.Cast(mode.Command());
|
||||||
|
bool special = ERR_EnemyMedicalCatalog.Specialist(prefab) || (hq && hq.Hunters().Contains(group));
|
||||||
|
m_Medic = ERR_EnemyMedicalCatalog.Medic(prefab);
|
||||||
|
if (!special && !m_Medic && Math.RandomInt(0, 100) >= 25) { m_Done = true; return; }
|
||||||
|
m_Epi = 1; m_Morphine = 1;
|
||||||
|
if (special) { m_Epi = 2; m_Morphine = 2; }
|
||||||
|
if (m_Medic) { m_Epi = 6; m_Morphine = 6; }
|
||||||
|
foreach (IEntity existing : items)
|
||||||
|
{
|
||||||
|
if (!existing || !existing.GetPrefabData()) continue;
|
||||||
|
ResourceName name = existing.GetPrefabData().GetPrefabName();
|
||||||
|
if (name == ERR_EnemyMedicalCatalog.EPI) m_Epi--;
|
||||||
|
if (name == ERR_EnemyMedicalCatalog.MORPHINE) m_Morphine--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (m_Operation)
|
||||||
|
{
|
||||||
|
if (!m_Operation.Finished) return;
|
||||||
|
if (!m_Operation.Succeeded) { Finish(actor, "native insertion failed; no loose or duplicate medicine"); return; }
|
||||||
|
m_Issued++; m_Operation = null;
|
||||||
|
}
|
||||||
|
if (m_Epi <= 0 && m_Morphine <= 0) { Finish(actor, "finite medicine stocked; authored bandages, saline and medical bag retained"); return; }
|
||||||
|
ResourceName next = ERR_EnemyMedicalCatalog.MORPHINE;
|
||||||
|
if (m_Epi > 0) { next = ERR_EnemyMedicalCatalog.EPI; m_Epi--; }
|
||||||
|
else m_Morphine--;
|
||||||
|
BaseInventoryStorageComponent bag;
|
||||||
|
if (m_Medic)
|
||||||
|
foreach (IEntity item : items)
|
||||||
|
{
|
||||||
|
if (!item || !item.GetPrefabData()) continue;
|
||||||
|
string path = item.GetPrefabData().GetPrefabName(); path.ToLower();
|
||||||
|
if (!path.Contains("backpack")) continue;
|
||||||
|
bag = BaseInventoryStorageComponent.Cast(item.FindComponent(BaseInventoryStorageComponent));
|
||||||
|
if (bag) break;
|
||||||
|
}
|
||||||
|
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||||
|
if (inventory.TrySpawnPrefabToStorage(next, bag, -1, EStoragePurpose.PURPOSE_DEPOSIT, m_Operation)) return;
|
||||||
|
// The native medical bag may already be physically full. A refused operation
|
||||||
|
// has inserted nothing: put the same finite dose into a free personal pouch.
|
||||||
|
if (bag)
|
||||||
|
{
|
||||||
|
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||||
|
if (inventory.TrySpawnPrefabToStorage(next, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, m_Operation)) return;
|
||||||
|
}
|
||||||
|
Finish(actor, "medicine did not fit; existing loadout preserved");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,10 +44,12 @@ class ERR_EnemyRadio
|
|||||||
array<IEntity> items = {}; inventory.GetItems(items);
|
array<IEntity> items = {}; inventory.GetItems(items);
|
||||||
if (items.Count() > 256) return -1;
|
if (items.Count() > 256) return -1;
|
||||||
int capability;
|
int capability;
|
||||||
|
ERR_HQ hq = ERR_HQ.Get();
|
||||||
|
if (hq && hq.Hunters()) capability = hq.Hunters().VehicleRadioCapability(actor);
|
||||||
foreach (IEntity item : items)
|
foreach (IEntity item : items)
|
||||||
{
|
{
|
||||||
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||||
capability = 1;
|
capability = Math.Max(capability, 1);
|
||||||
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||||
if (radio && radio.ERR_IsHQManpack()) return 2;
|
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,10 @@ modded class SCR_AIGetAimErrorOffset
|
|||||||
{
|
{
|
||||||
override float GetRandomFactor(EAISkill skill, float mu)
|
override float GetRandomFactor(EAISkill skill, float mu)
|
||||||
{
|
{
|
||||||
// CYLON normally eliminates aim error. Keep the ordinary REGULAR distribution
|
// Maximum decision skill, with REGULAR aim dispersion tightened by 10%.
|
||||||
// for these soldiers while leaving skill-dependent decisions at maximum.
|
// Preserve the requested mean; only the random error around it is reduced.
|
||||||
if (m_CombatComponent && m_CombatComponent.ERR_EnhancedEnemy()) skill = EAISkill.REGULAR;
|
if (m_CombatComponent && m_CombatComponent.ERR_EnhancedEnemy())
|
||||||
|
return mu + (super.GetRandomFactor(EAISkill.REGULAR, mu) - mu) * 0.9;
|
||||||
return super.GetRandomFactor(skill, mu);
|
return super.GetRandomFactor(skill, mu);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ class ERR_HQUnit
|
|||||||
SCR_AIGroupUtilityComponent Utility;
|
SCR_AIGroupUtilityComponent Utility;
|
||||||
SCR_AIGroupPerception Detection;
|
SCR_AIGroupPerception Detection;
|
||||||
float NextDetectionLog;
|
float NextDetectionLog;
|
||||||
|
ref ERR_HQLocalAwareness LocalAwareness = new ERR_HQLocalAwareness();
|
||||||
|
float NextContact;
|
||||||
|
ref ERR_InfantryCombat InfantryCombat = new ERR_InfantryCombat();
|
||||||
IEntity Radio;
|
IEntity Radio;
|
||||||
float NextReport;
|
float NextReport;
|
||||||
ref ERR_HQRoute Uplink;
|
ref ERR_HQRoute Uplink;
|
||||||
@@ -458,7 +461,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
SCR_AIGroup group = unit.Group;
|
SCR_AIGroup group = unit.Group;
|
||||||
string factionKey;
|
string factionKey;
|
||||||
if (group.GetFaction()) factionKey = group.GetFaction().GetFactionKey();
|
if (group.GetFaction()) factionKey = group.GetFaction().GetFactionKey();
|
||||||
m_Journal.Write("REGISTER", string.Format("group=%1 force=%2 faction=%3 position=%4 members=%5 home=%6 dedicated=%7; policy=maximum skill/regular aim/alert recognition", unit.Id, unit.ForceSourceId, factionKey, group.GetOrigin(), m_Mode.ForceLiving(group), unit.HomeId, unit.ReservedForSupport));
|
m_Journal.Write("REGISTER", string.Format("group=%1 force=%2 faction=%3 position=%4 members=%5 home=%6 dedicated=%7; policy=maximum skill/regular aim with ten percent less dispersion/alert recognition", unit.Id, unit.ForceSourceId, factionKey, group.GetOrigin(), m_Mode.ForceLiving(group), unit.HomeId, unit.ReservedForSupport));
|
||||||
array<AIAgent> auditAgents = {}; group.GetAgents(auditAgents);
|
array<AIAgent> auditAgents = {}; group.GetAgents(auditAgents);
|
||||||
foreach (AIAgent auditAgent : auditAgents)
|
foreach (AIAgent auditAgent : auditAgents)
|
||||||
{
|
{
|
||||||
@@ -523,7 +526,8 @@ class ERR_HQ : ERR_CommandService
|
|||||||
foreach (ERR_HQUnit unit : m_Units)
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
{
|
{
|
||||||
if (unit.Group != group) continue;
|
if (unit.Group != group) continue;
|
||||||
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Remove(Detected); unit.Detection = null; }
|
unit.LocalAwareness.Clear();
|
||||||
|
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Remove(Detected); unit.Detection.ERR_BindAwareness(null); unit.Detection = null; }
|
||||||
ReleaseUnit(unit, GetGame().GetWorld().GetWorldTime());
|
ReleaseUnit(unit, GetGame().GetWorld().GetWorldTime());
|
||||||
unit.Group = null;
|
unit.Group = null;
|
||||||
unit.Utility = null;
|
unit.Utility = null;
|
||||||
@@ -663,12 +667,32 @@ class ERR_HQ : ERR_CommandService
|
|||||||
unit.AmmoReporting.Queued(now);
|
unit.AmmoReporting.Queued(now);
|
||||||
m_Journal.Write("STATUS REPORT", route.Payload, route.MessageId);
|
m_Journal.Write("STATUS REPORT", route.Payload, route.MessageId);
|
||||||
}
|
}
|
||||||
// Due routine status and requested check-ins get the next free uplink.
|
// One captured contact every five seconds at most. Existing transmissions
|
||||||
// Positive observations precede a negative search report.
|
// finish normally; overdue status gets a slot before more urgent contacts.
|
||||||
|
protected void ContactReport(ERR_HQUnit unit, float now)
|
||||||
|
{
|
||||||
|
if (unit.Uplink || now < unit.NextContact || !unit.Group) return;
|
||||||
|
ERR_HQReport report = unit.LocalAwareness.Peek(now, unit.SentObservations);
|
||||||
|
if (!report) return;
|
||||||
|
unit.NextContact = now + 5000;
|
||||||
|
ERR_HQRoute route = ObservationRoute(unit);
|
||||||
|
if (!route) return;
|
||||||
|
report.MessageId = m_Journal.Message();
|
||||||
|
route.MessageId = report.MessageId; route.Payload = report.Description(now);
|
||||||
|
unit.PendingReport = report; unit.Uplink = route;
|
||||||
|
unit.LocalAwareness.Queued(report, now);
|
||||||
|
if (report.Target) unit.SentObservations.Queued(report.Target, report.ObservedAt, now);
|
||||||
|
unit.NextReport = now + m_Mode.Settings().Runtime("reportIntervalSeconds") * 1000;
|
||||||
|
string kind = "CONTACT REPORT";
|
||||||
|
if (report.TravelMode == "heard gunfire") kind = "GUNFIRE REPORT";
|
||||||
|
m_Journal.Write(kind, route.Payload, report.MessageId);
|
||||||
|
}
|
||||||
protected void QueueReports(ERR_HQUnit unit, float now)
|
protected void QueueReports(ERR_HQUnit unit, float now)
|
||||||
{
|
{
|
||||||
if (OriginOccupied(unit.Radio, unit)) { if (unit.Task) unit.Task.ArrivedAt = -1; return; }
|
if (OriginOccupied(unit.Radio, unit)) { if (unit.Task) unit.Task.ArrivedAt = -1; return; }
|
||||||
|
if (ERR_HQLocalAwareness.Priority(now, unit.NextContact, unit.NextStatus)) ContactReport(unit, now);
|
||||||
StatusReport(unit, now);
|
StatusReport(unit, now);
|
||||||
|
ContactReport(unit, now);
|
||||||
Observe(unit, now);
|
Observe(unit, now);
|
||||||
ObserveBody(unit, now);
|
ObserveBody(unit, now);
|
||||||
Sweep(unit, now);
|
Sweep(unit, now);
|
||||||
@@ -783,6 +807,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
}
|
}
|
||||||
if (!m_Memory.Ingest(report, now)) return;
|
if (!m_Memory.Ingest(report, now)) return;
|
||||||
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.SIGHTING) m_Coverage.Forget(report.Position);
|
if (ERR_HQMemoryRules.Kind(report) == ERR_HQMemoryKind.SIGHTING) m_Coverage.Forget(report.Position);
|
||||||
|
m_NextAssignment = Math.Min(m_NextAssignment, now);
|
||||||
m_Journal.Write("BELIEF", string.Format("delivered report M%1 retained; tracks=%2", report.MessageId, m_Memory.Count()), report.MessageId);
|
m_Journal.Write("BELIEF", string.Format("delivered report M%1 retained; tracks=%2", report.MessageId, m_Memory.Count()), report.MessageId);
|
||||||
// A reported corpse or distress call can justify investigation, never fire.
|
// A reported corpse or distress call can justify investigation, never fire.
|
||||||
if (ERR_HQMemoryRules.Kind(report) != ERR_HQMemoryKind.SIGHTING) return;
|
if (ERR_HQMemoryRules.Kind(report) != ERR_HQMemoryKind.SIGHTING) return;
|
||||||
@@ -804,10 +829,30 @@ class ERR_HQ : ERR_CommandService
|
|||||||
m_Contacts.Insert(report.Snapshot());
|
m_Contacts.Insert(report.Snapshot());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current combat suspends a sweep without deleting its native queued orders.
|
// Recent native contacts and the quiet grace period block HQ retasking.
|
||||||
protected bool Fighting(ERR_HQUnit unit)
|
protected bool Fighting(ERR_HQUnit unit)
|
||||||
{
|
{
|
||||||
return unit.Utility && unit.Utility.m_Perception && !unit.Utility.m_Perception.m_aTargets.IsEmpty();
|
return unit.InfantryCombat.Held || ERR_HunterCombat.Active(unit.Group);
|
||||||
|
}
|
||||||
|
protected void UpdateInfantryCombat(ERR_HQUnit unit, float now)
|
||||||
|
{
|
||||||
|
bool eligible = unit.Eligible(m_Mode);
|
||||||
|
int changed = unit.InfantryCombat.Update(unit, eligible, eligible && ERR_HunterCombat.Active(unit.Group), now);
|
||||||
|
if (!changed) return;
|
||||||
|
string detail = "contact ended; no current mobile search to resume";
|
||||||
|
if (changed == 1)
|
||||||
|
{
|
||||||
|
unit.Group.ActivateAI();
|
||||||
|
array<AIAgent> reacting = {}; unit.Group.GetAgents(reacting);
|
||||||
|
foreach (AIAgent actor : reacting)
|
||||||
|
if (actor && ERR_Actors.CanFight(actor.GetControlledEntity())) actor.ActivateAI();
|
||||||
|
detail = "enemy contact; native combat takes priority; defensive positions retained";
|
||||||
|
if (unit.InfantryCombat.Paused()) detail = "enemy contact; HQ search paused; fire at will";
|
||||||
|
// Preserve status/convoy transports. Only superseded infantry task orders stop.
|
||||||
|
if (unit.PendingTask) LostOrder(unit, now);
|
||||||
|
}
|
||||||
|
else if (changed == 2) detail = "contact quiet for 15 seconds; existing HQ search resumed";
|
||||||
|
m_Journal.Write("INFANTRY COMBAT", string.Format("Squad %1 (%2): %3", unit.Id, unit.ForceSourceId, detail));
|
||||||
}
|
}
|
||||||
protected int HomeAvailable(ERR_HQUnit candidate) { return m_HomeAvailable.Get(candidate.HomeId); }
|
protected int HomeAvailable(ERR_HQUnit candidate) { return m_HomeAvailable.Get(candidate.HomeId); }
|
||||||
protected ERR_HQHuntWindow Hunt(int beliefId)
|
protected ERR_HQHuntWindow Hunt(int beliefId)
|
||||||
@@ -1160,6 +1205,8 @@ class ERR_HQ : ERR_CommandService
|
|||||||
if (!hunt || now < hunt.NextHunt) continue;
|
if (!hunt || now < hunt.NextHunt) continue;
|
||||||
for (int slot = 0; slot < limit && assigned < limit && Assigned() < globalLimit; slot++)
|
for (int slot = 0; slot < limit && assigned < limit && Assigned() < globalLimit; slot++)
|
||||||
{
|
{
|
||||||
|
// One mobile search squad for uncertain sound; never empty objectives for it.
|
||||||
|
if (belief.Kind == ERR_HQMemoryKind.GUNFIRE && slot > 0) break;
|
||||||
float age = (now - belief.ObservedAt) / 1000;
|
float age = (now - belief.ObservedAt) / 1000;
|
||||||
ERR_HQPursuitRole role = ERR_HQPursuitRules.Role(belief.Confidence, age, belief.Radius, belief.HasVelocity, slot);
|
ERR_HQPursuitRole role = ERR_HQPursuitRules.Role(belief.Confidence, age, belief.Radius, belief.HasVelocity, slot);
|
||||||
vector destination = ERR_HQPursuitRules.Objective(belief.PredictedPosition, belief.Velocity, belief.HasVelocity, belief.Confidence, age, belief.Radius, role, slot);
|
vector destination = ERR_HQPursuitRules.Objective(belief.PredictedPosition, belief.Velocity, belief.HasVelocity, belief.Confidence, age, belief.Radius, role, slot);
|
||||||
@@ -1200,6 +1247,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
best.PendingTask = task; best.PendingOrder = belief.Report.Snapshot(); best.Downlink = bestRoute;
|
best.PendingTask = task; best.PendingOrder = belief.Report.Snapshot(); best.Downlink = bestRoute;
|
||||||
bestRoute.MessageId = m_Journal.Message();
|
bestRoute.MessageId = m_Journal.Message();
|
||||||
bestRoute.Payload = string.Format("group %1 %2; belief=%3 from report M%4; destination=%5", best.Id, task.Name(), task.BeliefId, belief.MessageId, destination);
|
bestRoute.Payload = string.Format("group %1 %2; belief=%3 from report M%4; destination=%5", best.Id, task.Name(), task.BeliefId, belief.MessageId, destination);
|
||||||
|
if (belief.Kind == ERR_HQMemoryKind.GUNFIRE) bestRoute.Payload += "; investigate heard gunfire, enemy identity unknown";
|
||||||
m_Journal.Write("ORDER QUEUED", bestRoute.Payload, bestRoute.MessageId);
|
m_Journal.Write("ORDER QUEUED", bestRoute.Payload, bestRoute.MessageId);
|
||||||
reserved.Insert(destination);
|
reserved.Insert(destination);
|
||||||
if (first < 0) first = bestBearing;
|
if (first < 0) first = bestBearing;
|
||||||
@@ -1242,10 +1290,11 @@ class ERR_HQ : ERR_CommandService
|
|||||||
for (int i = m_Units.Count() - 1; i >= 0; i--)
|
for (int i = m_Units.Count() - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
ERR_HQUnit unit = m_Units[i];
|
ERR_HQUnit unit = m_Units[i];
|
||||||
|
UpdateInfantryCombat(unit, now);
|
||||||
if (!unit.Detection && unit.Utility && unit.Group)
|
if (!unit.Detection && unit.Utility && unit.Group)
|
||||||
{
|
{
|
||||||
unit.Detection = unit.Utility.m_Perception;
|
unit.Detection = unit.Utility.m_Perception;
|
||||||
if (unit.Detection) unit.Detection.GetOnEnemyDetected().Insert(Detected);
|
if (unit.Detection) { unit.Detection.GetOnEnemyDetected().Insert(Detected); unit.Detection.ERR_BindAwareness(unit.LocalAwareness); }
|
||||||
}
|
}
|
||||||
if (unit.Uplink)
|
if (unit.Uplink)
|
||||||
{
|
{
|
||||||
@@ -1289,7 +1338,11 @@ class ERR_HQ : ERR_CommandService
|
|||||||
m_Towns.Stop();
|
m_Towns.Stop();
|
||||||
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills) if (specialSkill) specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills) if (specialSkill) specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
||||||
m_SpecialSkills.Clear();
|
m_SpecialSkills.Clear();
|
||||||
foreach (ERR_HQUnit detector : m_Units) if (detector.Detection) detector.Detection.GetOnEnemyDetected().Remove(Detected);
|
foreach (ERR_HQUnit detector : m_Units)
|
||||||
|
{
|
||||||
|
detector.LocalAwareness.Clear();
|
||||||
|
if (detector.Detection) { detector.Detection.GetOnEnemyDetected().Remove(Detected); detector.Detection.ERR_BindAwareness(null); }
|
||||||
|
}
|
||||||
m_Airfield.Stop(); m_Recovery.Stop();
|
m_Airfield.Stop(); m_Recovery.Stop();
|
||||||
m_AirAssault.Stop();
|
m_AirAssault.Stop();
|
||||||
m_Recon.Stop();
|
m_Recon.Stop();
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
// Local evidence belongs to the observing squad. HQ sees it only after the
|
||||||
|
// existing physical radio route completes. No live target position is queried.
|
||||||
|
class ERR_HQLocalAwareness
|
||||||
|
{
|
||||||
|
protected ref array<ref ERR_HQReport> m_Visual = {};
|
||||||
|
protected ref ERR_HQReport m_Sound;
|
||||||
|
protected float m_NextSound;
|
||||||
|
|
||||||
|
static bool Fresh(float observedAt, float now)
|
||||||
|
{
|
||||||
|
return ERR_ForceRules.Finite(observedAt) && ERR_ForceRules.Finite(now)
|
||||||
|
&& observedAt >= 0 && now >= observedAt && now - observedAt <= 30000;
|
||||||
|
}
|
||||||
|
static bool Priority(float now, float nextContact, float nextStatus)
|
||||||
|
{
|
||||||
|
// Fresh contacts may precede routine status, but cannot starve overdue status.
|
||||||
|
return now >= nextContact && now < nextStatus + 15000;
|
||||||
|
}
|
||||||
|
static vector SoundArea(vector position)
|
||||||
|
{
|
||||||
|
// Do not distribute the engine's precise shot origin or the shooter's identity.
|
||||||
|
return Vector(Math.Floor(position[0] / 150) * 150 + 75, position[1], Math.Floor(position[2] / 150) * 150 + 75);
|
||||||
|
}
|
||||||
|
void Clear() { m_Visual.Clear(); m_Sound = null; m_NextSound = 0; }
|
||||||
|
void Prune(float now)
|
||||||
|
{
|
||||||
|
for (int i = m_Visual.Count() - 1; i >= 0; i--)
|
||||||
|
if (!m_Visual[i].Target || !Fresh(m_Visual[i].ObservedAt, now)) m_Visual.Remove(i);
|
||||||
|
if (m_Sound && !Fresh(m_Sound.ObservedAt, now)) m_Sound = null;
|
||||||
|
}
|
||||||
|
bool CaptureDue(IEntity target, float now)
|
||||||
|
{
|
||||||
|
foreach (ERR_HQReport known : m_Visual)
|
||||||
|
if (known.Target == target && now - known.ObservedAt < 1000) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void Remember(ERR_HQReport report, float now)
|
||||||
|
{
|
||||||
|
if (!report || !report.Target || !Fresh(report.ObservedAt, now)) return;
|
||||||
|
Prune(now);
|
||||||
|
for (int i = 0; i < m_Visual.Count(); i++)
|
||||||
|
{
|
||||||
|
if (m_Visual[i].Target != report.Target) continue;
|
||||||
|
if (report.ObservedAt > m_Visual[i].ObservedAt) m_Visual[i] = report.Snapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_Visual.Count() >= 16) m_Visual.Remove(0);
|
||||||
|
m_Visual.Insert(report.Snapshot());
|
||||||
|
}
|
||||||
|
void Hear(SCR_AIGroup source, vector position, float observedAt, float now)
|
||||||
|
{
|
||||||
|
if (!source || !Fresh(observedAt, now) || !ERR_HQMemoryRules.PositionValid(position)) return;
|
||||||
|
if (m_Sound && observedAt - m_Sound.ObservedAt < 1000) return;
|
||||||
|
m_Sound = new ERR_HQReport();
|
||||||
|
m_Sound.Source = source; m_Sound.Position = SoundArea(position);
|
||||||
|
m_Sound.ObservedAt = observedAt; m_Sound.TravelMode = "heard gunfire";
|
||||||
|
}
|
||||||
|
ERR_HQReport Peek(float now, ERR_HQObservationHistory history)
|
||||||
|
{
|
||||||
|
Prune(now);
|
||||||
|
foreach (ERR_HQReport report : m_Visual)
|
||||||
|
if (history.CanQueue(report.Target, report.ObservedAt, now)) return report.Snapshot();
|
||||||
|
if (m_Sound && now >= m_NextSound) return m_Sound.Snapshot();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
void Queued(ERR_HQReport report, float now)
|
||||||
|
{
|
||||||
|
if (report.TravelMode == "heard gunfire") { m_Sound = null; m_NextSound = now + 30000; return; }
|
||||||
|
for (int i = m_Visual.Count() - 1; i >= 0; i--)
|
||||||
|
if (m_Visual[i].Target == report.Target && m_Visual[i].ObservedAt <= report.ObservedAt) m_Visual.Remove(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_AIGroupPerception
|
||||||
|
{
|
||||||
|
protected ERR_HQLocalAwareness m_ERRAwareness;
|
||||||
|
void ERR_BindAwareness(ERR_HQLocalAwareness awareness) { m_ERRAwareness = awareness; }
|
||||||
|
protected bool ERR_AwarenessActive()
|
||||||
|
{
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
return m_ERRAwareness && Replication.IsServer() && mode && mode.Active() && mode.Phase() > ERR_Phase.PRISON
|
||||||
|
&& m_Group && !m_Group.IsSlave() && m_Group.GetPlayerCount(true) == 0 && m_Group.GetFaction() == mode.EnemyFaction();
|
||||||
|
}
|
||||||
|
override SCR_AITargetInfo AddOrUpdateTarget(notnull BaseTarget target, out bool outNewTarget)
|
||||||
|
{
|
||||||
|
SCR_AITargetInfo result = super.AddOrUpdateTarget(target, outNewTarget);
|
||||||
|
if (!result || !ERR_AwarenessActive() || target.IsDisarmed()) return result;
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
if (!m_ERRAwareness.CaptureDue(target.GetTargetEntity(), now)) return result;
|
||||||
|
ERR_HQVisualContact seen = ERR_HQObservation.Capture(target, now);
|
||||||
|
if (!seen) return result;
|
||||||
|
ERR_HQReport report = new ERR_HQReport();
|
||||||
|
report.Source = m_Group; report.Target = seen.Entity; report.Position = seen.Position; report.ObservedAt = seen.ObservedAt;
|
||||||
|
report.Engaged = result.m_bEndangering;
|
||||||
|
array<ref ERR_HQVisualContact> samples = {seen};
|
||||||
|
ERR_HQObservation.Describe(report, samples, now);
|
||||||
|
m_ERRAwareness.Remember(report, now);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
override void AddOrUpdateGunshot(notnull IEntity shooter, vector worldPos, Faction faction, float timestamp, bool endangering)
|
||||||
|
{
|
||||||
|
super.AddOrUpdateGunshot(shooter, worldPos, faction, timestamp, endangering);
|
||||||
|
if (!ERR_AwarenessActive() || !faction || !m_Group.GetFaction().IsFactionEnemy(faction)) return;
|
||||||
|
PerceptionManager manager = GetGame().GetPerceptionManager();
|
||||||
|
if (!manager) return;
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
float age = manager.GetTime() - timestamp;
|
||||||
|
if (age < 0 || age > 30) return;
|
||||||
|
m_ERRAwareness.Hear(m_Group, worldPos, now - age * 1000, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,14 +19,14 @@ class ERR_HQLocalHandoff
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only a genuinely newer original sighting may be attempted. Old watermarks
|
// Only a genuinely newer original sighting may be attempted. Old watermarks
|
||||||
// can expire because the observation capture itself admits at most 500ms age.
|
// outlive the 30-second local evidence cache. Visual capture still requires 500ms freshness.
|
||||||
class ERR_HQObservationHistory
|
class ERR_HQObservationHistory
|
||||||
{
|
{
|
||||||
protected ref array<IEntity> m_Targets = {};
|
protected ref array<IEntity> m_Targets = {};
|
||||||
protected ref array<float> m_Times = {};
|
protected ref array<float> m_Times = {};
|
||||||
bool CanQueue(IEntity target, float observedAt, float now)
|
bool CanQueue(IEntity target, float observedAt, float now)
|
||||||
{
|
{
|
||||||
if (!target || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observedAt) || observedAt < 0 || now < observedAt || now - observedAt > 500) return false;
|
if (!target || !ERR_ForceRules.Finite(now) || !ERR_ForceRules.Finite(observedAt) || observedAt < 0 || now < observedAt || now - observedAt > 30000) return false;
|
||||||
for (int i = m_Targets.Count() - 1; i >= 0; i--)
|
for (int i = m_Targets.Count() - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
if (!m_Targets[i] || now - m_Times[i] > 120000) { m_Targets.Remove(i); m_Times.Remove(i); }
|
if (!m_Targets[i] || now - m_Times[i] > 120000) { m_Targets.Remove(i); m_Times.Remove(i); }
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ enum ERR_HQMemoryKind
|
|||||||
{
|
{
|
||||||
SIGHTING,
|
SIGHTING,
|
||||||
DISTRESS,
|
DISTRESS,
|
||||||
BODY
|
BODY,
|
||||||
|
GUNFIRE
|
||||||
}
|
}
|
||||||
|
|
||||||
class ERR_HQMemoryRules
|
class ERR_HQMemoryRules
|
||||||
@@ -22,6 +23,7 @@ class ERR_HQMemoryRules
|
|||||||
}
|
}
|
||||||
static int Kind(ERR_HQReport report)
|
static int Kind(ERR_HQReport report)
|
||||||
{
|
{
|
||||||
|
if (report.TravelMode == "heard gunfire") return ERR_HQMemoryKind.GUNFIRE;
|
||||||
if (report.Distress) return ERR_HQMemoryKind.DISTRESS;
|
if (report.Distress) return ERR_HQMemoryKind.DISTRESS;
|
||||||
if (report.TravelMode == "body" || report.TravelMode == "dead body") return ERR_HQMemoryKind.BODY;
|
if (report.TravelMode == "body" || report.TravelMode == "dead body") return ERR_HQMemoryKind.BODY;
|
||||||
return ERR_HQMemoryKind.SIGHTING;
|
return ERR_HQMemoryKind.SIGHTING;
|
||||||
@@ -33,18 +35,21 @@ class ERR_HQMemoryRules
|
|||||||
}
|
}
|
||||||
static float BaseConfidence(int kind)
|
static float BaseConfidence(int kind)
|
||||||
{
|
{
|
||||||
|
if (kind == ERR_HQMemoryKind.GUNFIRE) return 0.4;
|
||||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 0.3;
|
if (kind == ERR_HQMemoryKind.DISTRESS) return 0.3;
|
||||||
if (kind == ERR_HQMemoryKind.BODY) return 0.2;
|
if (kind == ERR_HQMemoryKind.BODY) return 0.2;
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
static float HalfLife(int kind)
|
static float HalfLife(int kind)
|
||||||
{
|
{
|
||||||
|
if (kind == ERR_HQMemoryKind.GUNFIRE) return 60;
|
||||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 180;
|
if (kind == ERR_HQMemoryKind.DISTRESS) return 180;
|
||||||
if (kind == ERR_HQMemoryKind.BODY) return 300;
|
if (kind == ERR_HQMemoryKind.BODY) return 300;
|
||||||
return 600;
|
return 600;
|
||||||
}
|
}
|
||||||
static float RadiusFloor(int kind, string travel)
|
static float RadiusFloor(int kind, string travel)
|
||||||
{
|
{
|
||||||
|
if (kind == ERR_HQMemoryKind.GUNFIRE) return 150;
|
||||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 150;
|
if (kind == ERR_HQMemoryKind.DISTRESS) return 150;
|
||||||
if (kind == ERR_HQMemoryKind.BODY) return 75;
|
if (kind == ERR_HQMemoryKind.BODY) return 75;
|
||||||
if (travel == "aircraft") return 120;
|
if (travel == "aircraft") return 120;
|
||||||
@@ -53,6 +58,7 @@ class ERR_HQMemoryRules
|
|||||||
}
|
}
|
||||||
static float RadiusGrowth(int kind, string travel)
|
static float RadiusGrowth(int kind, string travel)
|
||||||
{
|
{
|
||||||
|
if (kind == ERR_HQMemoryKind.GUNFIRE) return 2;
|
||||||
if (kind == ERR_HQMemoryKind.DISTRESS) return 2;
|
if (kind == ERR_HQMemoryKind.DISTRESS) return 2;
|
||||||
if (kind == ERR_HQMemoryKind.BODY) return 1.2;
|
if (kind == ERR_HQMemoryKind.BODY) return 1.2;
|
||||||
if (travel == "aircraft") return 8;
|
if (travel == "aircraft") return 8;
|
||||||
@@ -179,6 +185,12 @@ class ERR_HQMemoryTrack
|
|||||||
belief.Confidence = ConfidenceAt(now);
|
belief.Confidence = ConfidenceAt(now);
|
||||||
belief.Radius = RadiusAt(now);
|
belief.Radius = RadiusAt(now);
|
||||||
belief.PredictionSource = "delivered sighting; motion unknown";
|
belief.PredictionSource = "delivered sighting; motion unknown";
|
||||||
|
if (Kind == ERR_HQMemoryKind.GUNFIRE)
|
||||||
|
{
|
||||||
|
belief.Target = null;
|
||||||
|
belief.PredictionSource = "heard gunfire; approximate search area, identity and motion unknown";
|
||||||
|
return belief;
|
||||||
|
}
|
||||||
if (Kind == ERR_HQMemoryKind.DISTRESS)
|
if (Kind == ERR_HQMemoryKind.DISTRESS)
|
||||||
{
|
{
|
||||||
belief.Target = null;
|
belief.Target = null;
|
||||||
@@ -191,7 +203,22 @@ class ERR_HQMemoryTrack
|
|||||||
return belief;
|
return belief;
|
||||||
}
|
}
|
||||||
if (Count() < 2) return belief;
|
if (Count() < 2) return belief;
|
||||||
ERR_HQReport previous = m_Samples[Count() - 2];
|
// Several observers can report almost simultaneously. Such a sample must
|
||||||
|
// not erase a useful motion baseline already present in bounded history.
|
||||||
|
// Stop at a mode change or the nearest sufficiently spaced observation;
|
||||||
|
// never search past an implausible pair for a more convenient estimate.
|
||||||
|
ERR_HQReport previous;
|
||||||
|
for (int i = Count() - 2; i >= 0; i--)
|
||||||
|
{
|
||||||
|
previous = m_Samples[i];
|
||||||
|
if (previous.TravelMode != last.TravelMode || last.ObservedAt - previous.ObservedAt >= 2000) break;
|
||||||
|
previous = null;
|
||||||
|
}
|
||||||
|
if (!previous)
|
||||||
|
{
|
||||||
|
belief.PredictionSource = "delivered sighting; motion samples too close together";
|
||||||
|
return belief;
|
||||||
|
}
|
||||||
float seconds = (last.ObservedAt - previous.ObservedAt) / 1000;
|
float seconds = (last.ObservedAt - previous.ObservedAt) / 1000;
|
||||||
float distance = vector.DistanceXZ(previous.Position, last.Position);
|
float distance = vector.DistanceXZ(previous.Position, last.Position);
|
||||||
if (!ERR_HQMemoryRules.MotionCredible(seconds, distance, previous.TravelMode, last.TravelMode))
|
if (!ERR_HQMemoryRules.MotionCredible(seconds, distance, previous.TravelMode, last.TravelMode))
|
||||||
@@ -265,7 +292,7 @@ class ERR_HQMemory
|
|||||||
if (m_Seen.Count() >= 4096) return Refuse("delivered-report replay guard full");
|
if (m_Seen.Count() >= 4096) return Refuse("delivered-report replay guard full");
|
||||||
int kind = ERR_HQMemoryRules.Kind(report);
|
int kind = ERR_HQMemoryRules.Kind(report);
|
||||||
IEntity key = report.Target;
|
IEntity key = report.Target;
|
||||||
if (kind == ERR_HQMemoryKind.DISTRESS) key = report.Source;
|
if (kind == ERR_HQMemoryKind.DISTRESS || kind == ERR_HQMemoryKind.GUNFIRE) key = report.Source;
|
||||||
if (!key) return Refuse("report has no observation identity");
|
if (!key) return Refuse("report has no observation identity");
|
||||||
int index = Find(key, kind);
|
int index = Find(key, kind);
|
||||||
if (index >= 0 && report.ObservedAt <= m_Tracks[index].Latest().ObservedAt)
|
if (index >= 0 && report.ObservedAt <= m_Tracks[index].Latest().ObservedAt)
|
||||||
@@ -324,7 +351,8 @@ class ERR_HQMemory
|
|||||||
for (int i = 0; i < m_Tracks.Count(); i++)
|
for (int i = 0; i < m_Tracks.Count(); i++)
|
||||||
{
|
{
|
||||||
ERR_HQMemoryTrack track = m_Tracks[i];
|
ERR_HQMemoryTrack track = m_Tracks[i];
|
||||||
if (track.BeliefId != beliefId || track.Kind != ERR_HQMemoryKind.SIGHTING) continue;
|
if (track.BeliefId != beliefId) continue;
|
||||||
|
if (track.Kind != ERR_HQMemoryKind.SIGHTING && track.Kind != ERR_HQMemoryKind.GUNFIRE) continue;
|
||||||
if (track.BeliefRevision != revision || track.NegativeRevision == revision) return false;
|
if (track.BeliefRevision != revision || track.NegativeRevision == revision) return false;
|
||||||
track.SearchConfidence = 0.65;
|
track.SearchConfidence = 0.65;
|
||||||
track.NegativeRevision = revision;
|
track.NegativeRevision = revision;
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ class ERR_HQRadio
|
|||||||
array<IEntity> items = {};
|
array<IEntity> items = {};
|
||||||
inventory.GetItems(items);
|
inventory.GetItems(items);
|
||||||
int result;
|
int result;
|
||||||
|
ERR_HQ hq = ERR_HQ.Get();
|
||||||
|
if (hq && hq.Hunters()) result = hq.Hunters().VehicleRadioCapability(character);
|
||||||
foreach (IEntity item : items)
|
foreach (IEntity item : items)
|
||||||
{
|
{
|
||||||
if (!item || !item.FindComponent(BaseRadioComponent))
|
if (!item || !item.FindComponent(BaseRadioComponent))
|
||||||
continue;
|
continue;
|
||||||
result = 1;
|
result = Math.Max(result, 1);
|
||||||
SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
SCR_RadioComponent gadget = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||||
if (gadget && gadget.ERR_IsHQManpack())
|
if (gadget && gadget.ERR_IsHQManpack())
|
||||||
return 2;
|
return 2;
|
||||||
@@ -105,6 +107,8 @@ class ERR_HQReport
|
|||||||
|
|
||||||
string Description(float now, bool brief = false)
|
string Description(float now, bool brief = false)
|
||||||
{
|
{
|
||||||
|
if (TravelMode == "heard gunfire")
|
||||||
|
return string.Format("Gunfire heard near X=%1 Z=%2; approximate area, enemy identity/count/equipment unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||||
if (Distress)
|
if (Distress)
|
||||||
return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
return string.Format("Distress at friendly X=%1 Z=%2; enemy details unknown (%3s ago)", Math.Round(Position[0]), Math.Round(Position[2]), Math.Round(Math.Max(0, now - ObservedAt) / 1000));
|
||||||
string count = "count unknown";
|
string count = "count unknown";
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Local combat owns reactions. HQ uses delivered knowledge for travel only.
|
||||||
|
class ERR_HunterCombat
|
||||||
|
{
|
||||||
|
static bool Recent(int category, float age)
|
||||||
|
{
|
||||||
|
return age >= 0 && age <= 15 && (category == EAITargetInfoCategory.IDENTIFIED || category == EAITargetInfoCategory.DETECTED);
|
||||||
|
}
|
||||||
|
static bool Active(SCR_AIGroup group)
|
||||||
|
{
|
||||||
|
if (!group) return false;
|
||||||
|
SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent();
|
||||||
|
PerceptionManager perception = GetGame().GetPerceptionManager();
|
||||||
|
if (!utility || !utility.m_Perception || !perception) return false;
|
||||||
|
float now = perception.GetTime();
|
||||||
|
foreach (SCR_AITargetInfo target : utility.m_Perception.m_aTargets)
|
||||||
|
if (target && Recent(target.m_eCategory, now - target.m_fTimestamp)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,17 @@
|
|||||||
|
// Complete only a task still owned by a live native utility. Dead actors and
|
||||||
|
// teardown can clear m_AIInfo before mission services release their references.
|
||||||
|
class ERR_VehicleBehaviorCleanup
|
||||||
|
{
|
||||||
|
static void Complete(SCR_AIVehicleBehavior behavior, IEntity actor)
|
||||||
|
{
|
||||||
|
if (!behavior || !actor) return;
|
||||||
|
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));
|
||||||
|
if (!agent || !agent.m_UtilityComponent || !agent.m_UtilityComponent.m_AIInfo) return;
|
||||||
|
array<ref AIActionBase> actions = {}; agent.m_UtilityComponent.GetActions(actions);
|
||||||
|
if (actions.Contains(behavior)) behavior.Complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||||
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
||||||
class ERR_HunterCrewRecovery
|
class ERR_HunterCrewRecovery
|
||||||
@@ -13,7 +27,7 @@ class ERR_HunterCrewRecovery
|
|||||||
void Stop()
|
void Stop()
|
||||||
{
|
{
|
||||||
m_Stopped = true;
|
m_Stopped = true;
|
||||||
if (m_Boarding) m_Boarding.Complete();
|
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||||
m_Boarding = null; m_BoardingActor = null;
|
m_Boarding = null; m_BoardingActor = null;
|
||||||
}
|
}
|
||||||
bool Lost(ERR_GameMode mode)
|
bool Lost(ERR_GameMode mode)
|
||||||
@@ -45,9 +59,15 @@ class ERR_HunterCrewRecovery
|
|||||||
SCR_AIGroupUtilityComponent groupUtility = m_Unit.Group.GetGroupUtilityComponent();
|
SCR_AIGroupUtilityComponent groupUtility = m_Unit.Group.GetGroupUtilityComponent();
|
||||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||||
if (groupUtility && groupUtility.m_VehicleMgr && usage) groupUtility.m_VehicleMgr.TryAddVehicle(usage);
|
if (groupUtility && groupUtility.m_VehicleMgr && usage) groupUtility.m_VehicleMgr.TryAddVehicle(usage);
|
||||||
|
if (m_Unit.CombatHeld || ERR_HunterCombat.Active(m_Unit.Group))
|
||||||
|
{
|
||||||
|
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||||
|
m_Boarding = null; m_BoardingActor = null;
|
||||||
|
Note("local combat active; driver reboarding yields to native combat"); return;
|
||||||
|
}
|
||||||
if (m_Unit.CrewValid(mode))
|
if (m_Unit.CrewValid(mode))
|
||||||
{
|
{
|
||||||
if (m_Boarding) m_Boarding.Complete();
|
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||||
m_Boarding = null; m_BoardingActor = null;
|
m_Boarding = null; m_BoardingActor = null;
|
||||||
Note("driver seated and conscious; patrol can continue"); return;
|
Note("driver seated and conscious; patrol can continue"); return;
|
||||||
}
|
}
|
||||||
@@ -68,7 +88,7 @@ class ERR_HunterCrewRecovery
|
|||||||
if (m_Boarding && m_BoardingActor == m_Unit.Driver && actions.Contains(m_Boarding)) return;
|
if (m_Boarding && m_BoardingActor == m_Unit.Driver && actions.Contains(m_Boarding)) return;
|
||||||
if (now < m_NextAttempt) return;
|
if (now < m_NextAttempt) return;
|
||||||
m_NextAttempt = now + 10000;
|
m_NextAttempt = now + 10000;
|
||||||
if (m_Boarding) m_Boarding.Complete();
|
ERR_VehicleBehaviorCleanup.Complete(m_Boarding, m_BoardingActor);
|
||||||
m_BoardingActor = m_Unit.Driver;
|
m_BoardingActor = m_Unit.Driver;
|
||||||
IEntity aboard = CompartmentAccessComponent.GetVehicleIn(m_Unit.Driver);
|
IEntity aboard = CompartmentAccessComponent.GetVehicleIn(m_Unit.Driver);
|
||||||
if (aboard)
|
if (aboard)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
class ERR_HunterVehicle : ERR_ConvoyVehicle
|
class ERR_HunterVehicle : ERR_ConvoyVehicle
|
||||||
{
|
{
|
||||||
|
bool RadioRequested;
|
||||||
|
ref ERR_EnemyEquipmentCallback RadioOperation;
|
||||||
string Source;
|
string Source;
|
||||||
float Deadline;
|
float Deadline;
|
||||||
float NextOrder;
|
float NextOrder;
|
||||||
@@ -8,7 +10,12 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
|||||||
int SourceMessage;
|
int SourceMessage;
|
||||||
float LastContact = -1;
|
float LastContact = -1;
|
||||||
float NextContactOrder;
|
float NextContactOrder;
|
||||||
|
vector ContactPosition;
|
||||||
|
vector DeliveredContactPosition;
|
||||||
|
bool DeliveredContact;
|
||||||
ref ERR_HunterCrewRecovery Recovery;
|
ref ERR_HunterCrewRecovery Recovery;
|
||||||
|
bool CombatHeld;
|
||||||
|
float CombatQuietUntil;
|
||||||
float NextMotionLog;
|
float NextMotionLog;
|
||||||
float ProgressAt;
|
float ProgressAt;
|
||||||
vector ProgressPosition;
|
vector ProgressPosition;
|
||||||
@@ -54,6 +61,43 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// A finite physical radio in the vehicle avoids replacing a medic's backpack
|
||||||
|
// or depending on a modded vest accepting a vanilla manpack. Only the original
|
||||||
|
// living crew currently inside their uncaptured vehicle can use this link.
|
||||||
|
int VehicleRadioCapability(IEntity actor)
|
||||||
|
{
|
||||||
|
if (!actor || !m_Mode || !m_Mode.Active() || m_Stopped) return 0;
|
||||||
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
|
{
|
||||||
|
if (unit.Released || !unit.Members.Contains(actor) || !unit.Entity || !ERR_Actors.Alive(unit.Entity)) continue;
|
||||||
|
if (CompartmentAccessComponent.GetVehicleIn(actor) != unit.Entity || !unit.CrewValid(m_Mode) || ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) continue;
|
||||||
|
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
if (!storage) continue;
|
||||||
|
array<IEntity> items = {}; storage.GetItems(items);
|
||||||
|
foreach (IEntity item : items)
|
||||||
|
{
|
||||||
|
if (!item || !item.FindComponent(BaseRadioComponent)) continue;
|
||||||
|
SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));
|
||||||
|
if (radio && radio.ERR_IsHQManpack()) return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
protected bool PrepareVehicleRadio(ERR_HunterVehicle unit)
|
||||||
|
{
|
||||||
|
if (!unit.RadioRequested)
|
||||||
|
{
|
||||||
|
unit.RadioRequested = true;
|
||||||
|
unit.RadioOperation = new ERR_EnemyEquipmentCallback();
|
||||||
|
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
if (!storage || !storage.TrySpawnPrefabToStorage(ERR_EnemyRadio.MANPACK, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, unit.RadioOperation))
|
||||||
|
{
|
||||||
|
ERR_HQ.Event("HUNTER RADIO FAILED", unit.Source + ": vehicle refused its one physical HQ radio; no crew backpack is removed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unit.RadioOperation && unit.RadioOperation.Finished && unit.RadioOperation.Succeeded && VehicleRadioCapability(unit.Driver) == 2;
|
||||||
|
}
|
||||||
int Deployed() { return m_Hunters.Count(); }
|
int Deployed() { return m_Hunters.Count(); }
|
||||||
int ActiveHunters()
|
int ActiveHunters()
|
||||||
{
|
{
|
||||||
@@ -93,8 +137,51 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Hunter UAZs carry a full response team, unlike the two-person convoy escort.
|
||||||
|
override protected bool FillCrew(ERR_ConvoyVehicle unit)
|
||||||
|
{
|
||||||
|
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||||
|
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||||
|
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||||
|
{ ERR_HQ.Event("HUNTER FAILED", "selected faction has no usable soldier/medic profile"); return false; }
|
||||||
|
array<BaseCompartmentSlot> all = {}, cargo = {}, turrets = {}, drivers = {};
|
||||||
|
if (!ERR_ConvoyRules.CollectSeats(unit.Entity, all)) return false;
|
||||||
|
foreach (BaseCompartmentSlot seat : all)
|
||||||
|
{
|
||||||
|
if (!seat.IsCompartmentAccessible() || seat.GetOccupant()) continue;
|
||||||
|
if (seat.GetType() == ECompartmentType.PILOT) drivers.Insert(seat);
|
||||||
|
else if (seat.GetType() == ECompartmentType.TURRET) turrets.Insert(seat);
|
||||||
|
else if (seat.GetType() == ECompartmentType.CARGO) cargo.Insert(seat);
|
||||||
|
}
|
||||||
|
if (drivers.Count() != 1 || turrets.IsEmpty() || cargo.IsEmpty() || all.Count() > 12) return false;
|
||||||
|
unit.Seats.Insert(drivers[0]);
|
||||||
|
foreach (BaseCompartmentSlot turret : turrets) unit.Seats.Insert(turret);
|
||||||
|
int medicIndex = unit.Seats.Count();
|
||||||
|
foreach (BaseCompartmentSlot passenger : cargo) unit.Seats.Insert(passenger);
|
||||||
|
AIGroup nativeGroup = unit.Group;
|
||||||
|
for (int i = 0; i < unit.Seats.Count(); i++)
|
||||||
|
{
|
||||||
|
ResourceName selected = soldier;
|
||||||
|
if (i == medicIndex) selected = medic;
|
||||||
|
IEntity actor = unit.Seats[i].SpawnCharacterInCompartment(selected, nativeGroup, ERR_ConvoyResources.GroupPrefab());
|
||||||
|
if (actor) { unit.Members.Insert(actor); m_Mode.Own(actor); }
|
||||||
|
if (!actor || nativeGroup != unit.Group) return false;
|
||||||
|
}
|
||||||
|
unit.Driver = unit.Members[0];
|
||||||
|
ERR_HQ.Event("HUNTER TEAM", string.Format("crew=%1 including one medic; soldier=%2 medic=%3", unit.Members.Count(), soldier, medic));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||||
{
|
{
|
||||||
|
// Missing faction data is a deferred deployment, not a lost physical team.
|
||||||
|
ResourceName soldier = ERR_EnemyMedicalCatalog.Crew(m_Mode, false);
|
||||||
|
ResourceName medic = ERR_EnemyMedicalCatalog.Crew(m_Mode, true);
|
||||||
|
if (!ERR_EnemyLoadoutCatalog.Available(soldier) || !ERR_EnemyLoadoutCatalog.Available(medic))
|
||||||
|
{
|
||||||
|
m_NextSpawn = now + 30000;
|
||||||
|
ERR_HQ.Event("HUNTER WAIT", string.Format("faction=%1 missing response crew; soldier=%2 medic=%3; no vehicle spawned or allowance consumed; configure ResponseSoldier/ResponseMedic", m_Mode.EnemyFaction().GetFactionKey(), soldier, medic));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||||
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||||
@@ -115,20 +202,22 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
}
|
}
|
||||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||||
{
|
{
|
||||||
if (!unit.CrewValid(m_Mode)) return false;
|
if (!unit.CrewValid(m_Mode) || !PrepareVehicleRadio(unit)) return false;
|
||||||
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
array<AIAgent> agents = {}; unit.Group.GetAgents(agents);
|
||||||
if (agents.Count() != unit.Members.Count()) return false;
|
if (agents.Count() != unit.Members.Count()) return false;
|
||||||
foreach (AIAgent agent : agents) if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
foreach (AIAgent agent : agents) if (!agent || !unit.Members.Contains(agent.GetControlledEntity()) || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||||
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||||
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
if (!utility || !utility.m_VehicleMgr || !usage || !utility.m_VehicleMgr.TryAddVehicle(usage)) return false;
|
||||||
|
for (int i = 0; i < unit.Seats.Count(); i++) if (unit.Seats[i].GetOccupant() != unit.Members[i]) return false;
|
||||||
|
utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||||
ERR_ForceRecord record = m_Mode.Forces().Register(unit.Source, ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
ERR_ForceRecord record = m_Mode.Forces().Register(unit.Source, ERR_ForceKind.CONVOY, unit.Entity.GetOrigin(), unit.Members.Count(), null, unit.Group, now);
|
||||||
if (!record) return false;
|
if (!record) return false;
|
||||||
record.Pinned = true; unit.Registered = true;
|
record.Pinned = true; unit.Registered = true;
|
||||||
m_Mode.RegisterGroup(unit.Group); hq.ReserveGroup(unit.Group);
|
m_Mode.RegisterGroup(unit.Group); hq.ReserveGroup(unit.Group);
|
||||||
utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
utility.AddUsableVehicle(usage); unit.Group.SetPermanentLOD(0); unit.Group.ActivateAI();
|
||||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||||
ERR_HQ.Event("HUNTER READY", unit.Source + " driver and gunner seated; HQ radio registration active");
|
ERR_HQ.Event("HUNTER READY", unit.Source + " full response crew and medic seated; physical vehicle HQ radio stocked; fire at will; radio registration active");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||||
@@ -148,14 +237,22 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
unit.NextOrder = now + 30000;
|
unit.NextOrder = now + 30000;
|
||||||
vector destination, angles;
|
vector destination, angles;
|
||||||
string reason = "road patrol between friendly objectives";
|
string reason = "road patrol between friendly objectives";
|
||||||
unit.SourceMessage = 0;
|
|
||||||
if (best)
|
if (best)
|
||||||
{
|
{
|
||||||
|
// Retain the driver's route for repeated reports from the same area.
|
||||||
|
// Re-projecting every sighting can alternate between nearby roads and
|
||||||
|
// repeatedly restart native navigation even for a stationary target.
|
||||||
|
if (unit.Waypoint && unit.DeliveredContact && vector.DistanceXZ(best.PredictedPosition, unit.DeliveredContactPosition) < 75)
|
||||||
|
{
|
||||||
|
unit.LastContact = best.ObservedAt; unit.NextContactOrder = now + 5000;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
||||||
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
unit.SourceMessage = 0;
|
||||||
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
|
if (unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group)) return;
|
||||||
bool found;
|
bool found;
|
||||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||||
@@ -171,9 +268,26 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
if (best)
|
if (best)
|
||||||
{
|
{
|
||||||
unit.NextContactOrder = now + 5000;
|
unit.NextContactOrder = now + 5000;
|
||||||
if (unit.Downlink) unit.LastContact = best.ObservedAt;
|
if (unit.Downlink) { unit.LastContact = best.ObservedAt; unit.ContactPosition = best.PredictedPosition; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
protected void UpdateCombat(ERR_HunterVehicle unit, float now)
|
||||||
|
{
|
||||||
|
if (unit.Registered && ERR_HunterCombat.Active(unit.Group))
|
||||||
|
{
|
||||||
|
unit.CombatQuietUntil = now + 15000;
|
||||||
|
if (!unit.CombatHeld)
|
||||||
|
{
|
||||||
|
unit.CombatHeld = true; unit.ClearOrder(); unit.Downlink = null;
|
||||||
|
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": local contact; travel and forced reboarding suspended, native combat takes priority");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (unit.CombatHeld && now >= unit.CombatQuietUntil)
|
||||||
|
{
|
||||||
|
unit.CombatHeld = false; unit.NextOrder = 0;
|
||||||
|
ERR_HQ.Event("HUNTER COMBAT", unit.Source + ": contact quiet for 15 seconds; HQ travel may resume");
|
||||||
|
}
|
||||||
|
}
|
||||||
override void Tick(float now)
|
override void Tick(float now)
|
||||||
{
|
{
|
||||||
if (m_Stopped || !Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode != ERR_GameMode.Get() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_NextHunter) return;
|
if (m_Stopped || !Replication.IsServer() || !m_Mode || !m_Mode.Active() || m_Mode != ERR_GameMode.Get() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_NextHunter) return;
|
||||||
@@ -208,7 +322,9 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
string loss = unit.Recovery.Reason;
|
string loss = unit.Recovery.Reason;
|
||||||
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||||
}
|
}
|
||||||
|
UpdateCombat(unit, now);
|
||||||
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||||
|
if (unit.CombatHeld) continue;
|
||||||
if (!unit.Registered)
|
if (!unit.Registered)
|
||||||
{
|
{
|
||||||
if (AdmitHunter(unit, hq, now)) continue;
|
if (AdmitHunter(unit, hq, now)) continue;
|
||||||
@@ -227,6 +343,13 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
{
|
{
|
||||||
if (delivered > 0)
|
if (delivered > 0)
|
||||||
{
|
{
|
||||||
|
unit.DeliveredContact = unit.SourceMessage > 0;
|
||||||
|
unit.DeliveredContactPosition = unit.ContactPosition;
|
||||||
|
if (unit.Waypoint && vector.DistanceXZ(unit.Waypoint.GetOrigin(), unit.Destination) < 50)
|
||||||
|
{
|
||||||
|
ERR_HQ.Event("HUNTER ORDER RETAINED", unit.Source + ": updated report confirms current destination; native route preserved", unit.SourceMessage);
|
||||||
|
unit.Downlink = null; continue;
|
||||||
|
}
|
||||||
unit.ClearOrder();
|
unit.ClearOrder();
|
||||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
||||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||||
@@ -243,8 +366,9 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
vector position = unit.Entity.GetOrigin();
|
vector position = unit.Entity.GetOrigin();
|
||||||
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
||||||
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
||||||
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
// Old entries remain in native perception after contact ends. They must
|
||||||
bool combat = utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty();
|
// not suppress stuck-route recovery indefinitely.
|
||||||
|
bool combat = ERR_HunterCombat.Active(unit.Group);
|
||||||
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||||
if (!travelling || combat) unit.ProgressAt = now;
|
if (!travelling || combat) unit.ProgressAt = now;
|
||||||
if (now >= unit.NextMotionLog)
|
if (now >= unit.NextMotionLog)
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Suspend only the HQ-owned mobile search. Defensive/home waypoints, perception
|
||||||
|
// and individual native combat actions remain owned by their existing systems.
|
||||||
|
class ERR_InfantryCombat
|
||||||
|
{
|
||||||
|
bool Held;
|
||||||
|
float QuietUntil;
|
||||||
|
protected AIWaypoint m_PausedWaypoint;
|
||||||
|
protected ref ERR_HQTask m_PausedTask;
|
||||||
|
|
||||||
|
static bool Mobile(ERR_HQTask task)
|
||||||
|
{
|
||||||
|
return task && !task.ReleaseOnly && !task.RoadIntercept && task.SecuritySite.IsEmpty();
|
||||||
|
}
|
||||||
|
// Contact is supplied from native group perception by HQ, never player positions.
|
||||||
|
// Return 1 on contact, 2 when search resumes, 3 when combat ends without resuming.
|
||||||
|
int Update(ERR_HQUnit unit, bool eligible, bool contact, float now)
|
||||||
|
{
|
||||||
|
if (!eligible)
|
||||||
|
{
|
||||||
|
// A logistics reservation, player takeover or vehicle entry must not cause
|
||||||
|
// us to insert a waypoint. Retain identity until normal HQ cleanup or return.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (contact)
|
||||||
|
{
|
||||||
|
QuietUntil = now + 15000;
|
||||||
|
if (Held) return 0;
|
||||||
|
Held = true;
|
||||||
|
if (unit.Assignment && unit.Waypoint && Mobile(unit.Task))
|
||||||
|
{
|
||||||
|
m_PausedWaypoint = unit.Waypoint; m_PausedTask = unit.Task;
|
||||||
|
unit.Group.RemoveWaypoint(m_PausedWaypoint);
|
||||||
|
unit.Utility.SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (!Held || now < QuietUntil) return 0;
|
||||||
|
Held = false;
|
||||||
|
int result = 3;
|
||||||
|
// An expired/released/replaced task cannot be resurrected after a fight.
|
||||||
|
if (m_PausedWaypoint && unit.Waypoint == m_PausedWaypoint && unit.Task == m_PausedTask
|
||||||
|
&& unit.Assignment && Mobile(unit.Task) && now < unit.Task.ExpiresAt)
|
||||||
|
{
|
||||||
|
unit.Group.AddWaypointAt(m_PausedWaypoint, 0);
|
||||||
|
result = 2;
|
||||||
|
}
|
||||||
|
m_PausedWaypoint = null; m_PausedTask = null;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
bool Paused() { return m_PausedWaypoint != null; }
|
||||||
|
}
|
||||||
@@ -259,6 +259,15 @@ modded class SCR_MortarMuzzleComponent
|
|||||||
// re-aim). Each salvo shell needs its own spread point and fresh authorization.
|
// re-aim). Each salvo shell needs its own spread point and fresh authorization.
|
||||||
modded class SCR_AIStaticArtilleryBehavior
|
modded class SCR_AIStaticArtilleryBehavior
|
||||||
{
|
{
|
||||||
|
void SCR_AIStaticArtilleryBehavior(SCR_AIUtilityComponent utility, SCR_AIActivityBase groupActivity, IEntity artilleryEntity, vector targetPos, SCR_EAIArtilleryAmmoType ammoType, float priorityLevel = PRIORITY_LEVEL_NORMAL)
|
||||||
|
{
|
||||||
|
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||||
|
// Only mission-owned AI batteries use the rapid loop. Its zero fixed
|
||||||
|
// delays still wait on our physical projectile and ammunition gates.
|
||||||
|
if (system && system.FindGun(artilleryEntity))
|
||||||
|
m_sBehaviorTree = "{D0BDF32990D2C687}AI/BehaviorTrees/ERR/MortarRapidFire.bt";
|
||||||
|
}
|
||||||
|
|
||||||
ERR_MortarBattery ERR_GetMortarBattery()
|
ERR_MortarBattery ERR_GetMortarBattery()
|
||||||
{
|
{
|
||||||
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
ERR_MortarSystem system = ERR_MortarSystem.Get();
|
||||||
@@ -301,6 +310,10 @@ modded class SCR_AIStaticArtilleryBehavior_ShootingLogic
|
|||||||
ENodeResult result = super.EOnTaskSimulate(owner, dt);
|
ENodeResult result = super.EOnTaskSimulate(owner, dt);
|
||||||
if (battery && result == ENodeResult.SUCCESS)
|
if (battery && result == ENodeResult.SUCCESS)
|
||||||
{
|
{
|
||||||
|
// Super counts the confirmed last shot and emits native completion.
|
||||||
|
// Hold here until HQ observes that event; a zero-delay repeater must
|
||||||
|
// not begin a ninth aim and turn successful completion into rejection.
|
||||||
|
if (battery.OrderShots >= ERR_MortarSalvo.ROUNDS) return ENodeResult.RUNNING;
|
||||||
SetVariableOut(PORT_AIMING_NEEDED, true);
|
SetVariableOut(PORT_AIMING_NEEDED, true);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class ERR_MortarStation
|
|||||||
{
|
{
|
||||||
if (m_Stopped) return;
|
if (m_Stopped) return;
|
||||||
m_Stopped = true;
|
m_Stopped = true;
|
||||||
if (m_Mount) m_Mount.Complete();
|
ERR_VehicleBehaviorCleanup.Complete(m_Mount, m_Gunner);
|
||||||
m_Mount = null;
|
m_Mount = null;
|
||||||
foreach (SCR_CharacterControllerComponent controller : m_Controllers)
|
foreach (SCR_CharacterControllerComponent controller : m_Controllers)
|
||||||
if (controller) controller.ERR_ClearMortarStation(this);
|
if (controller) controller.ERR_ClearMortarStation(this);
|
||||||
@@ -65,7 +65,7 @@ class ERR_MortarStation
|
|||||||
}
|
}
|
||||||
if (!Holds(m_Gunner) || !ERR_Actors.CanFight(m_Gunner))
|
if (!Holds(m_Gunner) || !ERR_Actors.CanFight(m_Gunner))
|
||||||
{
|
{
|
||||||
if (m_Mount) m_Mount.Complete();
|
ERR_VehicleBehaviorCleanup.Complete(m_Mount, m_Gunner);
|
||||||
m_Mount = null; m_Gunner = null;
|
m_Mount = null; m_Gunner = null;
|
||||||
foreach (AIAgent candidate : agents)
|
foreach (AIAgent candidate : agents)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -304,7 +304,9 @@ class ERR_MortarSystem
|
|||||||
if ((battery.Rejected || battery.RefreshRejected) && !battery.Released)
|
if ((battery.Rejected || battery.RefreshRejected) && !battery.Released)
|
||||||
{
|
{
|
||||||
battery.Cancel("native aim or firing authorization rejected");
|
battery.Cancel("native aim or firing authorization rejected");
|
||||||
battery.NextOrder = now + 10000;
|
// Do not churn another ballistic/crew task for the same unsuitable area
|
||||||
|
// every twenty seconds. All exact per-shot checks remain authoritative.
|
||||||
|
battery.NextOrder = now + 60000;
|
||||||
}
|
}
|
||||||
battery.Rejected = false;
|
battery.Rejected = false;
|
||||||
battery.Update(now);
|
battery.Update(now);
|
||||||
@@ -327,6 +329,13 @@ class ERR_MortarSystem
|
|||||||
if (!battery.CanRequest(report, now)) continue;
|
if (!battery.CanRequest(report, now)) continue;
|
||||||
float candidate = vector.DistanceXZ(battery.Position, report.Position);
|
float candidate = vector.DistanceXZ(battery.Position, report.Position);
|
||||||
if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue;
|
if (candidate < 350 || candidate > 4000 || candidate >= distance || !OutsideSettlements(report.Position)) continue;
|
||||||
|
vector predicted;
|
||||||
|
if (!ERR_MortarPrediction.Predict(report, now, 30, predicted)) continue;
|
||||||
|
if (!Safe(battery, predicted, 30))
|
||||||
|
{
|
||||||
|
battery.Trace("HQ HOLD", SafetyReason, "no firing order issued; reported impact=" + predicted.ToString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
selected = battery;
|
selected = battery;
|
||||||
target = report;
|
target = report;
|
||||||
distance = candidate;
|
distance = candidate;
|
||||||
|
|||||||
@@ -1,6 +1,41 @@
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
|
// Arrange a ground-response shortfall, without changing airborne policy,
|
||||||
|
// contacts, radio delivery, artillery safety or the drop order itself.
|
||||||
|
modded class ERR_HQ
|
||||||
|
{
|
||||||
|
bool ERR_AirFixtureSearchAssigned(SCR_AIGroup group, ERR_AirDropOrder order)
|
||||||
|
{
|
||||||
|
if (!group || !order) return false;
|
||||||
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
{
|
||||||
|
if (unit.Group != group) continue;
|
||||||
|
// Contact can remove the waypoint from the native queue while preserving
|
||||||
|
// this exact HQ assignment for resumption after combat.
|
||||||
|
return unit.Task && unit.Task.BeliefId == order.BeliefId && unit.Assignment && unit.Waypoint
|
||||||
|
&& vector.DistanceXZ(unit.Waypoint.GetOrigin(), order.Target) < 1;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
void ERR_AirFixtureReserveGround()
|
||||||
|
{
|
||||||
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
if (unit.Group && !unit.ReservedForSupport) ReserveGroup(unit.Group);
|
||||||
|
}
|
||||||
|
}
|
||||||
modded class ERR_AirAssaultLoad
|
modded class ERR_AirAssaultLoad
|
||||||
{
|
{
|
||||||
|
ERR_AirDropOrder ERR_TestOrder() { return m_Order; }
|
||||||
|
int ERR_TestSearchingTeams()
|
||||||
|
{
|
||||||
|
if (!m_Order) return 0;
|
||||||
|
int count;
|
||||||
|
foreach (ERR_AirAssaultTeam team : m_Teams)
|
||||||
|
{
|
||||||
|
if (!team.Released || !team.Group) continue;
|
||||||
|
if (ERR_HQ.Get().ERR_AirFixtureSearchAssigned(team.Group, m_Order)) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
int ERR_TestGrounded()
|
int ERR_TestGrounded()
|
||||||
{
|
{
|
||||||
int count;
|
int count;
|
||||||
@@ -24,7 +59,9 @@ modded class ERR_GameMode
|
|||||||
protected float m_ERRAirDeadline;
|
protected float m_ERRAirDeadline;
|
||||||
protected ERR_AirAssaultLoad m_ERRAirLoad;
|
protected ERR_AirAssaultLoad m_ERRAirLoad;
|
||||||
protected Vehicle m_ERRAirframe;
|
protected Vehicle m_ERRAirframe;
|
||||||
protected bool m_ERRAirStopped;
|
protected bool m_ERRAirStopped, m_ERRAirPlayerDamage;
|
||||||
|
protected IEntity m_ERRAirPlayer;
|
||||||
|
protected float m_ERRAirNextStatus;
|
||||||
protected override void EOnInit(IEntity owner)
|
protected override void EOnInit(IEntity owner)
|
||||||
{
|
{
|
||||||
super.EOnInit(owner);
|
super.EOnInit(owner);
|
||||||
@@ -36,6 +73,11 @@ modded class ERR_GameMode
|
|||||||
}
|
}
|
||||||
protected void ERR_AirTestStop() { m_ERRAirStopped = true;
|
protected void ERR_AirTestStop() { m_ERRAirStopped = true;
|
||||||
GetGame().GetCallqueue().Remove(ERR_AirTestTick);
|
GetGame().GetCallqueue().Remove(ERR_AirTestTick);
|
||||||
|
if (m_ERRAirPlayer)
|
||||||
|
{
|
||||||
|
DamageManagerComponent damage = DamageManagerComponent.Cast(m_ERRAirPlayer.FindComponent(DamageManagerComponent));
|
||||||
|
if (damage) damage.EnableDamageHandling(m_ERRAirPlayerDamage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
protected bool ERR_AirCheck(bool result, string detail)
|
protected bool ERR_AirCheck(bool result, string detail)
|
||||||
{
|
{
|
||||||
@@ -59,6 +101,9 @@ modded class ERR_GameMode
|
|||||||
ERR_Actors.Players(players, true);
|
ERR_Actors.Players(players, true);
|
||||||
if (players.IsEmpty()) return;
|
if (players.IsEmpty()) return;
|
||||||
if (!UnlockPrison(players[0])) return;
|
if (!UnlockPrison(players[0])) return;
|
||||||
|
m_ERRAirPlayer = players[0];
|
||||||
|
DamageManagerComponent playerDamage = DamageManagerComponent.Cast(m_ERRAirPlayer.FindComponent(DamageManagerComponent));
|
||||||
|
m_ERRAirPlayerDamage = playerDamage.IsDamageHandlingEnabled(); playerDamage.EnableDamageHandling(false);
|
||||||
if (hq.SupplyConvoys()) hq.SupplyConvoys().Stop();
|
if (hq.SupplyConvoys()) hq.SupplyConvoys().Stop();
|
||||||
hq.Recon().ERR_TestDeparture(now + 3000);
|
hq.Recon().ERR_TestDeparture(now + 3000);
|
||||||
m_ERRAirStage = 1;
|
m_ERRAirStage = 1;
|
||||||
@@ -74,20 +119,26 @@ modded class ERR_GameMode
|
|||||||
m_ERRAirLoad = flight.Assault();
|
m_ERRAirLoad = flight.Assault();
|
||||||
m_ERRAirframe = flight.Aircraft();
|
m_ERRAirframe = flight.Aircraft();
|
||||||
if (!ERR_AirCheck(m_ERRAirLoad && m_ERRAirLoad.Count() == 8 && m_ERRAirLoad.Aboard() == 8, "eight original passengers physically seated before takeoff")) return;
|
if (!ERR_AirCheck(m_ERRAirLoad && m_ERRAirLoad.Count() == 8 && m_ERRAirLoad.Aboard() == 8, "eight original passengers physically seated before takeoff")) return;
|
||||||
|
// Controlled visible runway encounter; no fabricated reports, policy
|
||||||
|
// overrides or direct Begin call. Native perception and radio feed HQ.
|
||||||
|
ChimeraCharacter encounter = ChimeraCharacter.Cast(m_ERRAirPlayer);
|
||||||
|
vector transform[4]; encounter.GetWorldTransform(transform);
|
||||||
|
transform[3] = ERR_Actors.Ground(flight.Home() + Vector(0, 0, 400)); encounter.Teleport(transform);
|
||||||
|
Print("ERR AIR FIXTURE: ground squads reserved for other duties; native sighting, radio and HQ airborne decision remain unchanged");
|
||||||
m_ERRAirStage = 2;
|
m_ERRAirStage = 2;
|
||||||
}
|
}
|
||||||
if (m_ERRAirStage == 2)
|
if (m_ERRAirStage == 2)
|
||||||
{
|
{
|
||||||
if (!m_ERRAirLoad.Ready()) return;
|
hq.ERR_AirFixtureReserveGround();
|
||||||
ERR_AirDropOrder order = new ERR_AirDropOrder();
|
if (now >= m_ERRAirNextStatus)
|
||||||
order.ExpiresAt = now + 180000;
|
{
|
||||||
order.Target = flight.Home();
|
m_ERRAirNextStatus = now + 15000;
|
||||||
order.Report = new ERR_HQReport();
|
Print("ERR AIR HQ WAIT: " + hq.AirAssault().Status());
|
||||||
order.Report.Position = order.Target;
|
}
|
||||||
order.Report.ObservedAt = now;
|
if (!m_ERRAirLoad.Used()) return;
|
||||||
// Controlled geometry/transfer fixture; policy and physical radio authorization
|
ERR_AirDropOrder order = m_ERRAirLoad.ERR_TestOrder();
|
||||||
// are verified independently. This does not fabricate a live sighting result.
|
if (!ERR_AirCheck(order && order.BeliefId > 0 && order.MessageId > 0 && order.Report && order.Report.Target == m_ERRAirPlayer,
|
||||||
if (!ERR_AirCheck(m_ERRAirLoad.Begin(order, now), "controlled order finds real clear landing corridor")) return;
|
"automatic HQ policy and physical radio downlink authorized drop from native player sightings")) return;
|
||||||
m_ERRAirStage = 3;
|
m_ERRAirStage = 3;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,8 +146,9 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (m_ERRAirLoad.ERR_TestGrounded() < 8 || m_ERRAirLoad.ERR_TestReleased() < 2) return;
|
if (m_ERRAirLoad.ERR_TestGrounded() < 8 || m_ERRAirLoad.ERR_TestReleased() < 2) return;
|
||||||
if (!ERR_AirCheck(flight.Aircraft() == m_ERRAirframe && flight.ERR_TestCrew() >= 2, "same eight actors landed and two original groups released; original aircrew retained")) return;
|
if (!ERR_AirCheck(flight.Aircraft() == m_ERRAirframe && flight.ERR_TestCrew() >= 2, "same eight actors landed and two original groups released; original aircrew retained")) return;
|
||||||
|
if (!ERR_AirCheck(m_ERRAirLoad.ERR_TestSearchingTeams() == 2, "both landed squads received native search waypoints for the carried HQ target")) return;
|
||||||
vector center;
|
vector center;
|
||||||
if (!ERR_AirCheck(m_ERRAirLoad.Cover(now, center) && vector.DistanceXZ(center, flight.Home()) < 1, "post-drop cover active")) return;
|
if (!ERR_AirCheck(m_ERRAirLoad.Cover(now, center) && vector.DistanceXZ(center, m_ERRAirLoad.ERR_TestOrder().Target) < 1, "post-drop cover active")) return;
|
||||||
m_ERRAirLoad.ERR_TestCoverEnd(now);
|
m_ERRAirLoad.ERR_TestCoverEnd(now);
|
||||||
m_ERRAirStage = 4;
|
m_ERRAirStage = 4;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,14 +12,35 @@ modded class ERR_AirReconSystem
|
|||||||
}
|
}
|
||||||
modded class ERR_ReconFlightComponent
|
modded class ERR_ReconFlightComponent
|
||||||
{
|
{
|
||||||
|
protected float m_ERRFlightTraceAt;
|
||||||
|
override void EOnSimulate(IEntity owner, float timeSlice)
|
||||||
|
{
|
||||||
|
string fixture;
|
||||||
|
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture == "air-recon" && m_Initialized && !m_Logistics)
|
||||||
|
{
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
Physics physics = owner.GetPhysics();
|
||||||
|
if (physics && now >= m_ERRFlightTraceAt)
|
||||||
|
{
|
||||||
|
m_ERRFlightTraceAt = now + 1000;
|
||||||
|
PrintFormat("ERR RECON MOTION: t=%1 phase=%2 position=%3 velocity=%4 angular=%5 angles=%6 pilot=%7 alive=%8", now, m_Phase, owner.GetOrigin(), physics.GetVelocity(), physics.GetAngularVelocity(), owner.GetYawPitchRoll(), LivingPilot(), ERR_Actors.Alive(owner));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.EOnSimulate(owner, timeSlice);
|
||||||
|
}
|
||||||
int ERR_TestCrew() { return m_OriginalCrew.Count(); }
|
int ERR_TestCrew() { return m_OriginalCrew.Count(); }
|
||||||
bool ERR_TestPilot() { return LivingPilot(); }
|
bool ERR_TestPilot() { return LivingPilot(); }
|
||||||
|
float ERR_TestCommandSpeed() { return m_Velocity.Length(); }
|
||||||
|
bool ERR_TestCloseContact(IEntity actor)
|
||||||
|
{
|
||||||
|
return m_Observation && m_Observation.Target == actor && vector.DistanceXZ(m_Aircraft.GetOrigin(), m_Observation.Position) < 180;
|
||||||
|
}
|
||||||
bool ERR_TestSeen(IEntity actor) { return m_Observation && m_Observation.Target == actor; }
|
bool ERR_TestSeen(IEntity actor) { return m_Observation && m_Observation.Target == actor; }
|
||||||
protected override void Search(float now)
|
protected override void Search(float now)
|
||||||
{
|
{
|
||||||
super.Search(now);
|
super.Search(now);
|
||||||
string fixture;
|
string fixture;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture == "air-recon")
|
if (System.GetCLIParam("errSmokeOutcome", fixture) && (fixture == "air-recon" || fixture == "air-assault"))
|
||||||
m_Center = m_Home + Vector(0, 0, 800); // Controlled runway-sector encounter, native perception unchanged.
|
m_Center = m_Home + Vector(0, 0, 800); // Controlled runway-sector encounter, native perception unchanged.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,7 +50,7 @@ modded class ERR_GameMode
|
|||||||
protected float m_ERRReconDeadline, m_ERRReconDue, m_ERRReconWait;
|
protected float m_ERRReconDeadline, m_ERRReconDue, m_ERRReconWait;
|
||||||
protected Vehicle m_ERRReconFirst;
|
protected Vehicle m_ERRReconFirst;
|
||||||
protected IEntity m_ERRReconPlayer;
|
protected IEntity m_ERRReconPlayer;
|
||||||
protected bool m_ERRReconDamage, m_ERRReconTakeoverOnly;
|
protected bool m_ERRReconDamage, m_ERRReconTakeoverOnly, m_ERRReconCloseSeen;
|
||||||
protected override void EOnInit(IEntity owner)
|
protected override void EOnInit(IEntity owner)
|
||||||
{
|
{
|
||||||
super.EOnInit(owner);
|
super.EOnInit(owner);
|
||||||
@@ -79,6 +100,7 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (now < m_ERRReconDue) { if (!ERR_ReconCheck(!flight && system.ERR_TestWaves() == 0, "no aircraft before scheduled deadline")) return; return; }
|
if (now < m_ERRReconDue) { if (!ERR_ReconCheck(!flight && system.ERR_TestWaves() == 0, "no aircraft before scheduled deadline")) return; return; }
|
||||||
if (!flight || flight.Phase() == ERR_ReconPhase.CREW) return;
|
if (!flight || flight.Phase() == ERR_ReconPhase.CREW) return;
|
||||||
|
if (flight.Lost() || flight.Phase() == ERR_ReconPhase.RELEASED) { ERR_ReconCheck(false, "crew preparation: " + flight.Status()); return; }
|
||||||
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.TAKEOFF && flight.ERR_TestCrew() >= 2 && flight.ERR_TestPilot(), "native original pilot/gunners seated before takeoff")) return;
|
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.TAKEOFF && flight.ERR_TestCrew() >= 2 && flight.ERR_TestPilot(), "native original pilot/gunners seated before takeoff")) return;
|
||||||
if (!ERR_ReconCheck(system.ERR_TestAirport(flight.Home()) && system.ERR_TestWaves() == 1 && system.ERR_TestDeadline() < 0, "one actual airfield departure disarms launch timer")) return;
|
if (!ERR_ReconCheck(system.ERR_TestAirport(flight.Home()) && system.ERR_TestWaves() == 1 && system.ERR_TestDeadline() < 0, "one actual airfield departure disarms launch timer")) return;
|
||||||
m_ERRReconFirst = flight.Aircraft();
|
m_ERRReconFirst = flight.Aircraft();
|
||||||
@@ -100,6 +122,29 @@ modded class ERR_GameMode
|
|||||||
if (!delivered) return;
|
if (!delivered) return;
|
||||||
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.SHADOW, "real native player sighting enters shadow orbit and reaches HQ through crew radio")) return;
|
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.SHADOW, "real native player sighting enters shadow orbit and reaches HQ through crew radio")) return;
|
||||||
if (!ERR_ReconCheck(ERR_Actors.Alive(flight.Aircraft()) && flight.ERR_TestPilot() && flight.Aircraft().GetOrigin()[1] - SCR_TerrainHelper.GetTerrainY(flight.Aircraft().GetOrigin()) > 80, "airport takeoff and more than 300 metres of terrain-cleared search flight")) return;
|
if (!ERR_ReconCheck(ERR_Actors.Alive(flight.Aircraft()) && flight.ERR_TestPilot() && flight.Aircraft().GetOrigin()[1] - SCR_TerrainHelper.GetTerrainY(flight.Aircraft().GetOrigin()) > 80, "airport takeoff and more than 300 metres of terrain-cleared search flight")) return;
|
||||||
|
// A new close sighting must not add an escape impulse every simulation
|
||||||
|
// frame. Move only the fixture player; perception/radio/aircraft stay native.
|
||||||
|
vector heading = flight.Aircraft().GetPhysics().GetVelocity(); heading[1] = 0; heading.Normalize();
|
||||||
|
ChimeraCharacter closeEncounter = ChimeraCharacter.Cast(m_ERRReconPlayer);
|
||||||
|
vector closeTransform[4]; closeEncounter.GetWorldTransform(closeTransform);
|
||||||
|
closeTransform[3] = ERR_Actors.Ground(flight.Aircraft().GetOrigin() + heading * 100);
|
||||||
|
closeEncounter.Teleport(closeTransform);
|
||||||
|
m_ERRReconWait = now + 60000; m_ERRReconStage = 11; return;
|
||||||
|
}
|
||||||
|
if (m_ERRReconStage == 11)
|
||||||
|
{
|
||||||
|
if (!flight || flight.Lost() || flight.Phase() == ERR_ReconPhase.RELEASED) { ERR_ReconCheck(false, "close-contact flight lost: " + system.Status()); return; }
|
||||||
|
float commanded = flight.ERR_TestCommandSpeed();
|
||||||
|
float measured = flight.Aircraft().GetPhysics().GetVelocity().Length();
|
||||||
|
if (commanded > 70 || measured > 80)
|
||||||
|
{ ERR_ReconCheck(false, string.Format("close-contact overspeed commanded=%1 m/s measured=%2 m/s", commanded, measured)); return; }
|
||||||
|
if (!m_ERRReconCloseSeen && flight.ERR_TestCloseContact(m_ERRReconPlayer))
|
||||||
|
{
|
||||||
|
m_ERRReconCloseSeen = true;
|
||||||
|
Print("ERR SMOKE PASS: air recon native close sighting exercised the 180-metre standoff controller");
|
||||||
|
}
|
||||||
|
if (now < m_ERRReconWait) return;
|
||||||
|
if (!ERR_ReconCheck(m_ERRReconCloseSeen && ERR_Actors.Alive(flight.Aircraft()) && flight.ERR_TestPilot(), "60 seconds of bounded close-contact flight with original living pilot")) return;
|
||||||
system.Return();
|
system.Return();
|
||||||
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.RETURNING, "production return-home request accepted")) return;
|
if (!ERR_ReconCheck(flight.Phase() == ERR_ReconPhase.RETURNING, "production return-home request accepted")) return;
|
||||||
m_ERRReconStage = 3; return;
|
m_ERRReconStage = 3; return;
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ class ERR_NativeMortarSalvoRules
|
|||||||
}
|
}
|
||||||
static bool QueueUpdate(bool accepted, bool pending, int shots, float observedAt, float orderedAt, float now)
|
static bool QueueUpdate(bool accepted, bool pending, int shots, float observedAt, float orderedAt, float now)
|
||||||
{
|
{
|
||||||
return accepted && !pending && shots < ERR_MortarSalvo.ROUNDS / 2 && now >= observedAt + 20000 && orderedAt >= 0 && now >= orderedAt && now - orderedAt < 90000;
|
// Keep observing through the sixth round's preparation. Stopping updates at
|
||||||
|
// half a salvo can expire the last report before the final native reload.
|
||||||
|
// Leave two rounds for the last real downlink to complete before closure.
|
||||||
|
return accepted && !pending && shots < ERR_MortarSalvo.ROUNDS - 2 && now >= observedAt + 20000 && orderedAt >= 0 && now >= orderedAt && now - orderedAt < 90000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,7 +437,10 @@ class ERR_NativeMortarSalvoProbe
|
|||||||
{
|
{
|
||||||
float observed;
|
float observed;
|
||||||
int messageId = hq.ERR_QueueMortarRefreshReport(m_Group, m_OriginalRTO, m_Target, m_Impact, observed);
|
int messageId = hq.ERR_QueueMortarRefreshReport(m_Group, m_OriginalRTO, m_Target, m_Impact, observed);
|
||||||
if (!Check(messageId > 0 && AddObservation(messageId, observed), "next distinct actual-clock synthetic observation queued only after previous acceptance")) return -1;
|
// Ordinary crew/status traffic can occupy the real uplink. Zero means
|
||||||
|
// no report was queued; wait within the existing deadline, without
|
||||||
|
// fabricating an observation or replacing the delivered report.
|
||||||
|
if (messageId > 0 && !Check(AddObservation(messageId, observed), "next distinct actual-clock synthetic observation queued only after previous acceptance")) return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_Battery.CommandOpen || m_Battery.Activity) return 0;
|
if (m_Battery.CommandOpen || m_Battery.Activity) return 0;
|
||||||
|
|||||||
@@ -40,12 +40,22 @@ modded class ERR_HQ
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
bool ERR_TestPatrolCombat(SCR_AIGroup group, AIWaypoint search, bool contact, float now, int expected)
|
||||||
|
{
|
||||||
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
{
|
||||||
|
if (unit.Group != group || unit.Waypoint != search) continue;
|
||||||
|
return unit.InfantryCombat.Update(unit, unit.Eligible(m_Mode), contact, now) == expected;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ERR_NativePatrolHQProbe
|
class ERR_NativePatrolHQProbe
|
||||||
{
|
{
|
||||||
protected ERR_GameMode m_Mode;
|
protected ERR_GameMode m_Mode;
|
||||||
protected ERR_SiteRuntime m_Site;
|
protected ERR_SiteRuntime m_Site;
|
||||||
|
protected SCR_AIGroup m_Group;
|
||||||
protected AIWaypoint m_Search;
|
protected AIWaypoint m_Search;
|
||||||
protected ref array<AIWaypoint> m_Baseline = {};
|
protected ref array<AIWaypoint> m_Baseline = {};
|
||||||
protected int m_Step;
|
protected int m_Step;
|
||||||
@@ -64,25 +74,35 @@ class ERR_NativePatrolHQProbe
|
|||||||
if (m_Step == 0)
|
if (m_Step == 0)
|
||||||
{
|
{
|
||||||
m_Site = m_Mode.Sites().Find("everon-outpost-1");
|
m_Site = m_Mode.Sites().Find("everon-outpost-1");
|
||||||
if (!Check(m_Site && m_Site.Group && m_Site.PatrolCycle, "existing outpost route available")) return -1;
|
if (!Check(m_Site && m_Site.Responders && m_Site.Responders.Group, "existing mobile outpost responders available")) return -1;
|
||||||
m_Site.Group.GetWaypoints(m_Baseline);
|
m_Group = m_Site.Responders.Group;
|
||||||
if (!Check(!m_Baseline.IsEmpty() && hq.ERR_TestPatrolSearch(m_Site.Group, m_Search), "real delivered-order method assigns existing squad")) return -1;
|
m_Group.GetWaypoints(m_Baseline);
|
||||||
|
if (!Check(!m_Baseline.IsEmpty() && hq.ERR_TestPatrolSearch(m_Group, m_Search), "real delivered-order method assigns existing squad")) return -1;
|
||||||
m_Step = 1; m_Wait = now + 750; return 0;
|
m_Step = 1; m_Wait = now + 750; return 0;
|
||||||
}
|
}
|
||||||
if (m_Step == 1)
|
if (m_Step == 1)
|
||||||
{
|
{
|
||||||
array<AIWaypoint> queue = {};
|
array<AIWaypoint> queue = {};
|
||||||
m_Site.Group.GetWaypoints(queue);
|
m_Group.GetWaypoints(queue);
|
||||||
if (!Check(m_Search && !queue.IsEmpty() && queue[0] == m_Search && m_Site.Group.GetCurrentWaypoint() == m_Search, "Search preempts native patrol")) return -1;
|
if (!Check(m_Search && !queue.IsEmpty() && queue[0] == m_Search && m_Group.GetCurrentWaypoint() == m_Search, "Search preempts native patrol")) return -1;
|
||||||
foreach (AIWaypoint waypoint : m_Baseline)
|
foreach (AIWaypoint waypoint : m_Baseline)
|
||||||
if (!Check(queue.Contains(waypoint), "preempted baseline route remains queued")) return -1;
|
if (!Check(queue.Contains(waypoint), "preempted baseline route remains queued")) return -1;
|
||||||
if (!Check(hq.ERR_TestPatrolRelease(m_Site.Group, m_Search), "real HQ release removes only its assignment")) return -1;
|
if (!Check(hq.ERR_TestPatrolCombat(m_Group, m_Search, true, now, 1), "injected local contact pauses actual HQ search")) return -1;
|
||||||
|
queue.Clear(); m_Group.GetWaypoints(queue);
|
||||||
|
if (!Check(!queue.Contains(m_Search), "combat removes HQ search from native queue without deleting it")) return -1;
|
||||||
|
foreach (AIWaypoint home : m_Baseline)
|
||||||
|
if (!Check(queue.Contains(home), "combat preserves baseline patrol ownership")) return -1;
|
||||||
|
if (!Check(hq.ERR_TestPatrolCombat(m_Group, m_Search, false, now + 14999, 0), "quiet grace period prevents immediate marching")) return -1;
|
||||||
|
if (!Check(hq.ERR_TestPatrolCombat(m_Group, m_Search, false, now + 15000, 2), "quiet period resumes the same unexpired assignment")) return -1;
|
||||||
|
queue.Clear(); m_Group.GetWaypoints(queue);
|
||||||
|
if (!Check(!queue.IsEmpty() && queue[0] == m_Search, "resumed search is queued exactly once at the front")) return -1;
|
||||||
|
if (!Check(hq.ERR_TestPatrolRelease(m_Group, m_Search), "real HQ release removes only its assignment")) return -1;
|
||||||
m_Step = 2; m_Wait = now + 750; return 0;
|
m_Step = 2; m_Wait = now + 750; return 0;
|
||||||
}
|
}
|
||||||
if (m_Step == 2)
|
if (m_Step == 2)
|
||||||
{
|
{
|
||||||
AIWaypoint current = m_Site.Group.GetCurrentWaypoint();
|
AIWaypoint current = m_Group.GetCurrentWaypoint();
|
||||||
if (!Check(current && (current == m_Site.PatrolCycle || m_Site.PatrolWaypoints.Contains(current)), "native patrol resumes after Search release")) return -1;
|
if (!Check(current && m_Baseline.Contains(current), "native home defence returns after Search release")) return -1;
|
||||||
m_Step = 3; return 1;
|
m_Step = 3; return 1;
|
||||||
}
|
}
|
||||||
return 1;
|
return 1;
|
||||||
@@ -90,7 +110,7 @@ class ERR_NativePatrolHQProbe
|
|||||||
void Stop()
|
void Stop()
|
||||||
{
|
{
|
||||||
ERR_HQ hq = ERR_HQ.Get();
|
ERR_HQ hq = ERR_HQ.Get();
|
||||||
if (hq && m_Site && m_Search) hq.ERR_TestPatrolRelease(m_Site.Group, m_Search);
|
if (hq && m_Site && m_Search) hq.ERR_TestPatrolRelease(m_Group, m_Search);
|
||||||
}
|
}
|
||||||
void ~ERR_NativePatrolHQProbe() { Stop(); }
|
void ~ERR_NativePatrolHQProbe() { Stop(); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,13 @@ class ERR_ReconFlightRules
|
|||||||
if (distance >= 180) return velocity;
|
if (distance >= 180) return velocity;
|
||||||
if (distance < 1) away = "1 0 0";
|
if (distance < 1) away = "1 0 0";
|
||||||
away.Normalize();
|
away.Normalize();
|
||||||
float inward = vector.Dot(velocity, away);
|
// This is a minimum outward speed, not an impulse. The result is stored
|
||||||
if (inward < 0) velocity -= away * inward;
|
// in m_Velocity and reused next frame: adding the full correction on every
|
||||||
return velocity + away * Math.Clamp((180 - distance) * 0.5, 0, 25);
|
// simulation tick accelerated close encounters to hundreds of m/s.
|
||||||
|
float radial = vector.Dot(velocity, away);
|
||||||
|
float minimum = Math.Clamp((180 - distance) * 0.5, 0, 25);
|
||||||
|
if (radial < minimum) velocity = velocity + away * (minimum - radial);
|
||||||
|
return velocity;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
[ComponentEditorProps(category: "Reintroduced", description: "Airport reconnaissance helicopter")]
|
[ComponentEditorProps(category: "Reintroduced", description: "Airport reconnaissance helicopter")]
|
||||||
@@ -113,22 +117,55 @@ class ERR_ReconFlightComponent : ScriptComponent
|
|||||||
if (!m_Crew) { Fail("crew group unavailable"); return; }
|
if (!m_Crew) { Fail("crew group unavailable"); return; }
|
||||||
mode.Own(m_Crew); m_Crew.SetFaction(mode.EnemyFaction()); m_Crew.SetPermanentLOD(0);
|
mode.Own(m_Crew); m_Crew.SetFaction(mode.EnemyFaction()); m_Crew.SetPermanentLOD(0);
|
||||||
m_Crew.GetGroupUtilityComponent().SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
m_Crew.GetGroupUtilityComponent().SetCombatMode(EAIGroupCombatMode.FIRE_AT_WILL);
|
||||||
Seats(m_Aircraft);
|
m_Crew.ActivateAI();
|
||||||
if (m_Seats.Count() < 2 || m_Seats.Count() > 8) { Fail("unsupported pilot/gunner seat layout"); return; }
|
// Native compartments/simulation are not ready in the airframe spawn callback.
|
||||||
|
// Update owns one bounded allocation after initialization, including passengers.
|
||||||
|
}
|
||||||
|
protected bool PrepareCrew(float now)
|
||||||
|
{
|
||||||
|
if (!m_OriginalCrew.IsEmpty()) return true;
|
||||||
|
if (now - m_Started < 500) return false;
|
||||||
|
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(m_Aircraft.FindComponent(HelicopterControllerComponent));
|
||||||
|
VehicleHelicopterSimulation simulation;
|
||||||
|
if (controller) simulation = controller.GetSimulation();
|
||||||
|
FuelManagerComponent fuel = FuelManagerComponent.Cast(m_Aircraft.FindComponent(FuelManagerComponent));
|
||||||
|
if (!simulation || !simulation.IsValid() || !fuel || !fuel.HasFuel()) { m_Reason = "waiting for native airframe simulation and fuel"; return false; }
|
||||||
|
m_Seats.Clear(); Seats(m_Aircraft);
|
||||||
|
if (m_Seats.Count() < 2 || m_Seats.Count() > 8) { m_Reason = "waiting for pilot/gunner seat layout"; return false; }
|
||||||
|
foreach (BaseCompartmentSlot candidate : m_Seats)
|
||||||
|
{
|
||||||
|
if (candidate.IsOccupied()) { Fail("crew seat occupied before allocation; no replacement created"); return false; }
|
||||||
|
if (!candidate.IsCompartmentAccessible() || candidate.IsReserved() || candidate.IsGetInLocked())
|
||||||
|
{ m_Reason = "waiting for accessible crew seat " + candidate.GetCompartmentName(); return false; }
|
||||||
|
}
|
||||||
ResourceName pilot = "{C31F210ABE078197}Prefabs/ERR/ReconPilotEast.et";
|
ResourceName pilot = "{C31F210ABE078197}Prefabs/ERR/ReconPilotEast.et";
|
||||||
string faction = mode.EnemyFaction().GetFactionKey();
|
string faction = m_Mode.EnemyFaction().GetFactionKey();
|
||||||
if (faction == "US" || faction == "RHS_USAF") pilot = "{C31F210ABE078199}Prefabs/ERR/ReconPilotWest.et";
|
if (faction == "US" || faction == "RHS_USAF") pilot = "{C31F210ABE078199}Prefabs/ERR/ReconPilotWest.et";
|
||||||
foreach (BaseCompartmentSlot seat : m_Seats)
|
foreach (BaseCompartmentSlot seat : m_Seats)
|
||||||
{
|
{
|
||||||
if (seat.IsOccupied() || !seat.IsCompartmentAccessible()) { Fail("crew seat obstructed"); return; }
|
// Native SpawnCharacterInCompartment boards before assigning the group.
|
||||||
AIGroup nativeCrew = m_Crew;
|
// Its Soviet/US prefab affiliation can therefore reject an ION airframe.
|
||||||
IEntity initialCrew = seat.SpawnCharacterInCompartment(pilot, nativeCrew, "{C31F210ABE078195}Prefabs/ERR/ReconCrew.et");
|
// Assign the selected faction and original group BEFORE native boarding.
|
||||||
if (initialCrew) m_OriginalCrew.Insert(initialCrew);
|
vector staging = ERR_Actors.Ground(m_Home + Vector(20 + m_OriginalCrew.Count() * 2, 0, 0));
|
||||||
if (nativeCrew != m_Crew) { Fail("native boarding changed crew identity"); return; }
|
IEntity initialCrew = ERR_Actors.Spawn(pilot, staging);
|
||||||
|
if (!initialCrew) { Fail("pilot prefab allocation failed; no retry"); return false; }
|
||||||
|
m_Mode.Own(initialCrew); m_OriginalCrew.Insert(initialCrew);
|
||||||
|
AIControlComponent control = AIControlComponent.Cast(initialCrew.FindComponent(AIControlComponent));
|
||||||
|
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(initialCrew.FindComponent(CompartmentAccessComponent));
|
||||||
|
if (!control || !access || !ERR_Actors.SetFaction(initialCrew, m_Mode.EnemyFaction()))
|
||||||
|
{ Fail("crew faction/control unavailable; original actor retained"); return false; }
|
||||||
|
control.ActivateAI();
|
||||||
|
m_Crew.AddAgent(control.GetControlAIAgent());
|
||||||
|
if (!access.GetInVehicle(seat.GetOwner(), seat, true, -1, ECloseDoorAfterActions.INVALID, false))
|
||||||
|
{ Fail("native crew boarding refused at " + seat.GetCompartmentName() + "; faction=" + faction + "; original actor retained, no retry"); return false; }
|
||||||
}
|
}
|
||||||
m_Crew.ActivateAI(); Locks(true);
|
m_Crew.ActivateAI(); Locks(true);
|
||||||
if (!logistics) m_Assault = ERR_HQ.Get().AirAssault().Load(this, pad, serial);
|
if (!m_Logistics) m_Assault = ERR_HQ.Get().AirAssault().Load(this, m_Home, m_Serial);
|
||||||
|
m_Reason = "waiting for original crew seating and radio loadouts";
|
||||||
|
ERR_HQ.Event("HELICOPTER CREW", string.Format("airframe initialized; allocated %1 original pilot/gunner seats once", m_OriginalCrew.Count()));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool Captured()
|
protected bool Captured()
|
||||||
{
|
{
|
||||||
if (!m_Crew || m_Crew.GetPlayerCount(true) > 0 || m_Crew.GetFaction() != m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(m_Aircraft) != m_Mode.EnemyFaction()) return true;
|
if (!m_Crew || m_Crew.GetPlayerCount(true) > 0 || m_Crew.GetFaction() != m_Mode.EnemyFaction() || SCR_FactionManager.SGetFaction(m_Aircraft) != m_Mode.EnemyFaction()) return true;
|
||||||
@@ -172,11 +209,12 @@ class ERR_ReconFlightComponent : ScriptComponent
|
|||||||
}
|
}
|
||||||
protected bool FinishCrew(float now)
|
protected bool FinishCrew(float now)
|
||||||
{
|
{
|
||||||
|
if (now - m_Started > 45000) { Fail("crew preparation timed out: " + m_Reason + "; partial spawn retained"); return false; }
|
||||||
|
if (!PrepareCrew(now)) return false;
|
||||||
m_Crew.ActivateAI();
|
m_Crew.ActivateAI();
|
||||||
array<AIAgent> boardingAgents = {}; m_Crew.GetAgents(boardingAgents);
|
array<AIAgent> boardingAgents = {}; m_Crew.GetAgents(boardingAgents);
|
||||||
foreach (AIAgent boardingAgent : boardingAgents) { boardingAgent.SetPermanentLOD(0); boardingAgent.ActivateAI(); }
|
foreach (AIAgent boardingAgent : boardingAgents) { boardingAgent.SetPermanentLOD(0); boardingAgent.ActivateAI(); }
|
||||||
if (m_Assault && m_Assault.Preparing()) return false;
|
if (m_Assault && m_Assault.Preparing()) return false;
|
||||||
if (now - m_Started > 45000) { Fail("native crew boarding timed out; partial spawn retained"); return false; }
|
|
||||||
foreach (BaseCompartmentSlot seat : m_Seats)
|
foreach (BaseCompartmentSlot seat : m_Seats)
|
||||||
if (!seat || !seat.GetOccupant() || !ERR_Actors.CanFight(seat.GetOccupant()) || ERR_Actors.Group(seat.GetOccupant()) != m_Crew) return false;
|
if (!seat || !seat.GetOccupant() || !ERR_Actors.CanFight(seat.GetOccupant()) || ERR_Actors.Group(seat.GetOccupant()) != m_Crew) return false;
|
||||||
// Native loadouts can complete after seating; keep the original boarding deadline.
|
// Native loadouts can complete after seating; keep the original boarding deadline.
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
// One travel owner for the whole column. Native AI still steers and avoids
|
||||||
|
// obstacles; followers rendezvous with the preceding physical vehicle instead
|
||||||
|
// of independently racing to the same base marker.
|
||||||
|
class ERR_ConvoyColumnRules
|
||||||
|
{
|
||||||
|
static bool WaitForRear(float largestGap, bool waiting)
|
||||||
|
{
|
||||||
|
if (waiting) return largestGap > 90;
|
||||||
|
return largestGap > 160;
|
||||||
|
}
|
||||||
|
static bool TooClose(float gap, bool waiting)
|
||||||
|
{
|
||||||
|
if (waiting) return gap < 65;
|
||||||
|
return gap < 40;
|
||||||
|
}
|
||||||
|
static float Speed(float gap)
|
||||||
|
{
|
||||||
|
if (gap > 110) return 45;
|
||||||
|
if (gap > 70) return 35;
|
||||||
|
return 22;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_ConvoyDriving
|
||||||
|
{
|
||||||
|
static bool Restore(ERR_GameMode mode, ERR_ConvoyVehicle vehicle)
|
||||||
|
{
|
||||||
|
if (!mode || !vehicle || vehicle.Released || !vehicle.CrewValid(mode) || !ERR_ConvoyMountedCrew.Retain(vehicle.Driver, vehicle.Entity)) return false;
|
||||||
|
SCR_AIGroupUtilityComponent utility = vehicle.Group.GetGroupUtilityComponent();
|
||||||
|
if (!utility || !utility.m_VehicleMgr || utility.IsUsableVehicle(vehicle.Entity)) return false;
|
||||||
|
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||||
|
if (!usage || !usage.CanBePiloted()) return false;
|
||||||
|
// Native RemoveStuckVehicle removes this association after a failed path.
|
||||||
|
// Our retained seated crew cannot resume merely by receiving another waypoint.
|
||||||
|
utility.AddUsableVehicle(usage);
|
||||||
|
return utility.IsUsableVehicle(vehicle.Entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_ConvoyColumnVehicle
|
||||||
|
{
|
||||||
|
bool Holding;
|
||||||
|
vector Progress;
|
||||||
|
float LastProgress = -1;
|
||||||
|
float NextRetry;
|
||||||
|
float NextTarget;
|
||||||
|
float Speed = -1;
|
||||||
|
int Ahead = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_SupplyConvoyColumn
|
||||||
|
{
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected ERR_SupplyConvoyTask m_Task;
|
||||||
|
protected ref array<ref ERR_ConvoyColumnVehicle> m_State = {};
|
||||||
|
protected ref array<int> m_Order = {};
|
||||||
|
protected int m_Leader = -1;
|
||||||
|
protected int m_RouteIndex;
|
||||||
|
protected bool m_RearWait;
|
||||||
|
protected float m_NextLog;
|
||||||
|
void ERR_SupplyConvoyColumn(ERR_GameMode mode, ERR_SupplyConvoyTask task)
|
||||||
|
{
|
||||||
|
m_Mode = mode; m_Task = task;
|
||||||
|
for (int i = 0; i < task.Vehicles.Count(); i++)
|
||||||
|
{ m_State.Insert(new ERR_ConvoyColumnVehicle()); m_Order.Insert(i); }
|
||||||
|
}
|
||||||
|
protected void Speed(ERR_ConvoyVehicle vehicle, ERR_ConvoyColumnVehicle state, float speed)
|
||||||
|
{
|
||||||
|
if (state.Speed == speed) return;
|
||||||
|
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
|
||||||
|
if (movement) movement.SetCruiseSpeed(speed);
|
||||||
|
state.Speed = speed;
|
||||||
|
}
|
||||||
|
// Called before tactics take ownership and after every new base leg. A fresh
|
||||||
|
// progress clock prevents combat, reboarding and supply pauses counting as stuck.
|
||||||
|
protected float DistanceToTarget(int index, vector target)
|
||||||
|
{
|
||||||
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||||
|
if (vehicle.Released || !vehicle.Entity) return 1e20;
|
||||||
|
return vector.DistanceXZ(vehicle.Entity.GetOrigin(), target);
|
||||||
|
}
|
||||||
|
protected void Reorder()
|
||||||
|
{
|
||||||
|
vector target = m_Task.Route.At(Math.Min(m_RouteIndex, m_Task.Route.Count() - 1));
|
||||||
|
// Sort only at a new leg or after combat, never every tick. Keep the canonical
|
||||||
|
// vehicle array/roles intact; travel order follows current physical positions.
|
||||||
|
for (int i = 0; i < m_Order.Count(); i++)
|
||||||
|
for (int j = i + 1; j < m_Order.Count(); j++)
|
||||||
|
if (DistanceToTarget(m_Order[j], target) < DistanceToTarget(m_Order[i], target))
|
||||||
|
{ int swap = m_Order[i]; m_Order[i] = m_Order[j]; m_Order[j] = swap; }
|
||||||
|
string order;
|
||||||
|
foreach (int index : m_Order)
|
||||||
|
if (!m_Task.Vehicles[index].Released) order += (index + 1).ToString() + " ";
|
||||||
|
ERR_HQ.Event("CONVOY FORMATION", m_Task.Receipt.SourceId + ": travel order by physical approach to destination: " + order);
|
||||||
|
}
|
||||||
|
void Reset(float now, bool newLeg = false, bool reform = false)
|
||||||
|
{
|
||||||
|
if (newLeg) m_RouteIndex = 0;
|
||||||
|
if (newLeg || reform) { Reorder(); m_Leader = -1; }
|
||||||
|
m_RearWait = false;
|
||||||
|
for (int i = 0; i < m_State.Count(); i++)
|
||||||
|
{
|
||||||
|
ERR_ConvoyColumnVehicle state = m_State[i];
|
||||||
|
state.Speed = -1; // Tactics may have slowed this driver around dismounted infantry.
|
||||||
|
state.LastProgress = -1; state.Holding = false; state.Ahead = -1; state.NextTarget = now;
|
||||||
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[i];
|
||||||
|
if (!vehicle.Released && vehicle.Entity) Speed(vehicle, state, 35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected void Hold(ERR_ConvoyVehicle vehicle, ERR_ConvoyColumnVehicle state, float now)
|
||||||
|
{
|
||||||
|
vehicle.ClearOrder(); state.Holding = true;
|
||||||
|
state.Progress = vehicle.Entity.GetOrigin(); state.LastProgress = now; state.NextRetry = now + 45000;
|
||||||
|
}
|
||||||
|
protected bool Progress(ERR_ConvoyVehicle vehicle, ERR_ConvoyColumnVehicle state, float now)
|
||||||
|
{
|
||||||
|
vector position = vehicle.Entity.GetOrigin();
|
||||||
|
if ((state.LastProgress < 0 || now >= state.NextRetry) && ERR_ConvoyDriving.Restore(m_Mode, vehicle))
|
||||||
|
ERR_HQ.Event("CONVOY DRIVER RECOVERY", string.Format("%1 vehicle %2: restored native vehicle registration for its healthy original seated driver after a failed movement task", m_Task.Receipt.SourceId, vehicle.Index + 1));
|
||||||
|
if (state.LastProgress < 0 || vector.DistanceXZ(position, state.Progress) >= 5)
|
||||||
|
{
|
||||||
|
state.Progress = position; state.LastProgress = now; state.NextRetry = now + 45000;
|
||||||
|
}
|
||||||
|
if (now - state.LastProgress >= 180000)
|
||||||
|
{
|
||||||
|
ERR_HQ.Event("CONVOY STRANDED", string.Format("%1 vehicle %2 at %3: no 5 m movement in three minutes of commanded travel. Released from column; vehicle and crew retained, no replacement", m_Task.Receipt.SourceId, vehicle.Index + 1, position));
|
||||||
|
vehicle.Release(m_Mode); return false;
|
||||||
|
}
|
||||||
|
if (now >= state.NextRetry)
|
||||||
|
{
|
||||||
|
vehicle.ClearOrder(); state.NextRetry = now + 45000;
|
||||||
|
ERR_HQ.Event("CONVOY ROUTE RETRY", string.Format("%1 vehicle %2 at %3: stationary for %4 seconds; asking native driver to plan again", m_Task.Receipt.SourceId, vehicle.Index + 1, position, Math.Round((now - state.LastProgress) / 1000)));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
bool Tick(float now)
|
||||||
|
{
|
||||||
|
int leader = -1, previous = -1; float largestGap; bool supplyHold;
|
||||||
|
for (int slot = 0; slot < m_Order.Count(); slot++)
|
||||||
|
{
|
||||||
|
int i = m_Order[slot];
|
||||||
|
ERR_ConvoyVehicle scan = m_Task.Vehicles[i];
|
||||||
|
if (scan.Released || !scan.Entity) continue;
|
||||||
|
if (leader < 0) leader = i;
|
||||||
|
if (previous >= 0) largestGap = Math.Max(largestGap, vector.DistanceXZ(scan.Entity.GetOrigin(), m_Task.Vehicles[previous].Entity.GetOrigin()));
|
||||||
|
if (scan.SupplyHeld) supplyHold = true;
|
||||||
|
previous = i;
|
||||||
|
}
|
||||||
|
if (leader < 0) return false;
|
||||||
|
if (m_Leader != leader)
|
||||||
|
{
|
||||||
|
ERR_ConvoyVehicle promoted = m_Task.Vehicles[leader]; promoted.ClearOrder(); promoted.Arrived = false;
|
||||||
|
promoted.RouteIndex = m_RouteIndex;
|
||||||
|
m_Leader = leader;
|
||||||
|
ERR_HQ.Event("CONVOY LEADER", string.Format("%1: vehicle %2 leads surviving column", m_Task.Receipt.SourceId, leader + 1));
|
||||||
|
}
|
||||||
|
bool rearWait = ERR_ConvoyColumnRules.WaitForRear(largestGap, m_RearWait);
|
||||||
|
if (rearWait != m_RearWait)
|
||||||
|
ERR_HQ.Event("CONVOY SPACING", string.Format("%1: leader waiting=%2; largest gap between surviving neighbours=%3 m", m_Task.Receipt.SourceId, rearWait, Math.Round(largestGap)));
|
||||||
|
m_RearWait = rearWait;
|
||||||
|
previous = -1;
|
||||||
|
for (int ordinal = 0; ordinal < m_Order.Count(); ordinal++)
|
||||||
|
{
|
||||||
|
int index = m_Order[ordinal];
|
||||||
|
ERR_ConvoyVehicle vehicle = m_Task.Vehicles[index];
|
||||||
|
if (vehicle.Released || !vehicle.Entity) continue;
|
||||||
|
ERR_ConvoyColumnVehicle state = m_State[index];
|
||||||
|
if (vehicle.SupplyHeld || vehicle.Combat.Held)
|
||||||
|
{
|
||||||
|
// The external owner controls its own waypoint. Only suspend our watchdog.
|
||||||
|
state.LastProgress = -1; previous = index; continue;
|
||||||
|
}
|
||||||
|
if (previous < 0)
|
||||||
|
{
|
||||||
|
// A former leader may have finished this route before fighting elsewhere,
|
||||||
|
// or its successor may still be short of the endpoint. Rejoin it physically.
|
||||||
|
if (!vehicle.Arrived && vehicle.RouteIndex >= m_Task.Route.Count() && vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_Task.Route.At(m_Task.Route.Count() - 1)) > 35)
|
||||||
|
vehicle.RouteIndex = m_Task.Route.Count() - 1;
|
||||||
|
m_RouteIndex = vehicle.RouteIndex;
|
||||||
|
if (vehicle.Arrived) { Hold(vehicle, state, now); previous = index; continue; }
|
||||||
|
if (m_RearWait || supplyHold) { Hold(vehicle, state, now); previous = index; continue; }
|
||||||
|
state.Holding = false; Speed(vehicle, state, 35);
|
||||||
|
// Mounted groups can retain their spawn origin: arrival is always physical.
|
||||||
|
while (vehicle.RouteIndex < m_Task.Route.Count() && vector.DistanceXZ(vehicle.Entity.GetOrigin(), m_Task.Route.At(vehicle.RouteIndex)) <= 35)
|
||||||
|
{ vehicle.ClearOrder(); vehicle.RouteIndex++; }
|
||||||
|
m_RouteIndex = vehicle.RouteIndex;
|
||||||
|
if (vehicle.RouteIndex >= m_Task.Route.Count())
|
||||||
|
{ vehicle.Arrived = true; Hold(vehicle, state, now); previous = index; continue; }
|
||||||
|
if (!Progress(vehicle, state, now)) break; // promote once on the next tick
|
||||||
|
if (!vehicle.Waypoint)
|
||||||
|
{
|
||||||
|
// Leave tolerance between native completion and our vehicle-origin check.
|
||||||
|
// A seated driver's position is not exactly the vehicle origin.
|
||||||
|
vehicle.Waypoint = ERR_SupplyConvoyTactics.Move(m_Task.Route.At(vehicle.RouteIndex), 15);
|
||||||
|
if (vehicle.Waypoint) vehicle.Group.AddWaypoint(vehicle.Waypoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ERR_ConvoyVehicle ahead = m_Task.Vehicles[previous];
|
||||||
|
float gap = vector.DistanceXZ(vehicle.Entity.GetOrigin(), ahead.Entity.GetOrigin());
|
||||||
|
vehicle.Arrived = ahead.Arrived && gap <= 75;
|
||||||
|
if (vehicle.Arrived || ERR_ConvoyColumnRules.TooClose(gap, state.Holding)) Hold(vehicle, state, now);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.Holding = false; Speed(vehicle, state, ERR_ConvoyColumnRules.Speed(gap));
|
||||||
|
if (Progress(vehicle, state, now))
|
||||||
|
{
|
||||||
|
// Keep an existing target through small movements. Replanning every tick
|
||||||
|
// repeatedly cancels native manoeuvres, particularly around junctions.
|
||||||
|
bool changed = state.Ahead != previous;
|
||||||
|
if (vehicle.Waypoint && vector.DistanceXZ(vehicle.Waypoint.GetOrigin(), ahead.Entity.GetOrigin()) >= 60 && now >= state.NextTarget) changed = true;
|
||||||
|
if (vehicle.Waypoint && vector.DistanceXZ(vehicle.Entity.GetOrigin(), vehicle.Waypoint.GetOrigin()) <= 20) changed = true;
|
||||||
|
if (changed) vehicle.ClearOrder();
|
||||||
|
if (!vehicle.Waypoint)
|
||||||
|
{
|
||||||
|
vehicle.Waypoint = ERR_SupplyConvoyTactics.Move(ahead.Entity.GetOrigin(), 15);
|
||||||
|
if (vehicle.Waypoint) vehicle.Group.AddWaypoint(vehicle.Waypoint);
|
||||||
|
state.Ahead = previous; state.NextTarget = now + 8000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!vehicle.Released) previous = index;
|
||||||
|
}
|
||||||
|
if (now >= m_NextLog)
|
||||||
|
{
|
||||||
|
ERR_HQ.Event("CONVOY TRAVEL", string.Format("%1: leg %2, leader vehicle %3, largest neighbour gap %4 m, waiting for rear=%5, supplying infantry=%6", m_Task.Receipt.SourceId, m_Task.Legs, m_Leader + 1, Math.Round(largestGap), m_RearWait, supplyHold));
|
||||||
|
m_NextLog = now + 60000;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -90,6 +90,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService
|
|||||||
team.Group.SetPermanentLOD(0); team.Group.ActivateAI();
|
team.Group.SetPermanentLOD(0); team.Group.ActivateAI();
|
||||||
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
foreach (AIAgent active : agents) { active.SetPermanentLOD(0); active.ActivateAI(); }
|
||||||
}
|
}
|
||||||
|
supply.Column = new ERR_SupplyConvoyColumn(m_Mode, supply);
|
||||||
supply.Tactics = new ERR_SupplyConvoyTactics(m_Mode, supply);
|
supply.Tactics = new ERR_SupplyConvoyTactics(m_Mode, supply);
|
||||||
supply.TravelDeadline = now + 600000;
|
supply.TravelDeadline = now + 600000;
|
||||||
return true;
|
return true;
|
||||||
@@ -166,16 +167,10 @@ class ERR_SupplyConvoyService : ERR_ConvoyService
|
|||||||
}
|
}
|
||||||
protected void Circuit(ERR_SupplyConvoyTask task, float now)
|
protected void Circuit(ERR_SupplyConvoyTask task, float now)
|
||||||
{
|
{
|
||||||
bool surviving, pending;
|
if (!task.Column || !task.Column.Tick(now)) { task.Stop(m_Mode, "no surviving drivable convoy vehicles"); return; }
|
||||||
|
bool pending;
|
||||||
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
foreach (ERR_ConvoyVehicle vehicle : task.Vehicles)
|
||||||
{
|
if (!vehicle.Released && (!vehicle.Arrived || vehicle.SupplyHeld || vehicle.Combat.Held)) pending = true;
|
||||||
if (!vehicle.Released && now >= task.TravelDeadline && task.TravelDeadline > 0) { vehicle.ClearOrder(); vehicle.Arrived = true; }
|
|
||||||
if (!vehicle.Released && task.Legs > 0 && vector.Distance(vehicle.Entity.GetOrigin(), task.Bases[task.LastBase]) <= 250)
|
|
||||||
{ vehicle.ClearOrder(); vehicle.Arrived = true; }
|
|
||||||
vehicle.Tick(m_Mode, task.Route, now);
|
|
||||||
if (!vehicle.Released) { surviving = true; if (!vehicle.Arrived) pending = true; }
|
|
||||||
}
|
|
||||||
if (!surviving) { task.Stop(m_Mode, "no surviving drivable convoy vehicles"); return; }
|
|
||||||
if (pending) return;
|
if (pending) return;
|
||||||
if (!task.Waiting) { task.Waiting = true; task.Departure = now + Math.RandomFloat(45000, 90000); return; }
|
if (!task.Waiting) { task.Waiting = true; task.Departure = now + Math.RandomFloat(45000, 90000); return; }
|
||||||
if (now < task.Departure || task.Bases.Count() < 2) return;
|
if (now < task.Departure || task.Bases.Count() < 2) return;
|
||||||
@@ -189,6 +184,7 @@ class ERR_SupplyConvoyService : ERR_ConvoyService
|
|||||||
if (departure.Released) continue;
|
if (departure.Released) continue;
|
||||||
departure.ClearOrder(); departure.RouteIndex = 0; departure.Arrived = false;
|
departure.ClearOrder(); departure.RouteIndex = 0; departure.Arrived = false;
|
||||||
}
|
}
|
||||||
|
task.Column.Reset(now, true);
|
||||||
ERR_HQ.Event("CONVOY DEPARTURE", string.Format("%1 leg=%2 base=%3; native road navigation", task.Receipt.SourceId, task.Legs, task.Bases[next]));
|
ERR_HQ.Event("CONVOY DEPARTURE", string.Format("%1 leg=%2 base=%3; native road navigation", task.Receipt.SourceId, task.Legs, task.Bases[next]));
|
||||||
}
|
}
|
||||||
override void Tick(float now)
|
override void Tick(float now)
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ class ERR_SupplyConvoySystem
|
|||||||
if (attempt == 0) seed = m_Bases[(m_Attempts + m_Service.Count()) % m_Bases.Count()];
|
if (attempt == 0) seed = m_Bases[(m_Attempts + m_Service.Count()) % m_Bases.Count()];
|
||||||
seed = ERR_Actors.Ground(seed);
|
seed = ERR_Actors.Ground(seed);
|
||||||
array<vector> positions, angles; ERR_ForceRoute route;
|
array<vector> positions, angles; ERR_ForceRoute route;
|
||||||
if (!ERR_ConvoyRoadPlan.Build(seed, positions, angles, route) || !ERR_SupplyConvoyRules.Separated(positions[0], m_Starts, separation)) continue;
|
if (!ERR_ConvoyRoadPlan.Build(seed, positions, angles, route, m_Starts, separation) || !ERR_SupplyConvoyRules.Separated(positions[0], m_Starts, separation)) continue;
|
||||||
string source = "supply-convoy-" + (m_Service.Count() + 1).ToString();
|
string source = "supply-convoy-" + (m_Service.Count() + 1).ToString();
|
||||||
ERR_ConvoyReceipt receipt = m_Pool.Reserve(source);
|
ERR_ConvoyReceipt receipt = m_Pool.Reserve(source);
|
||||||
if (!receipt || receipt.Consumed) { m_Reason = "configured convoy capacity or shared force budget unavailable"; return false; }
|
if (!receipt || receipt.Consumed) { m_Reason = "configured convoy capacity or shared force budget unavailable"; return false; }
|
||||||
@@ -123,7 +123,7 @@ class ERR_SupplyConvoySystem
|
|||||||
if (!admitted) { m_Reason = "waiting for existing force admission"; return; }
|
if (!admitted) { m_Reason = "waiting for existing force admission"; return; }
|
||||||
m_Attempts++;
|
m_Attempts++;
|
||||||
StartOne();
|
StartOne();
|
||||||
if (m_Attempts == 120 && m_Service.Count() < m_Target) ERR_HQ.Event("CONVOY STARTUP", "bounded survey exhausted; deployed slots retained without replacement");
|
if (m_Attempts == 120 && m_Service.Count() < m_Target) ERR_HQ.Event("CONVOY STARTUP", string.Format("placement survey finished: %1/%2 convoys deployed; remaining slots could not find a clear separated road formation. Existing crews retained; no replacements", m_Service.Count(), m_Target));
|
||||||
}
|
}
|
||||||
string Status()
|
string Status()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ class ERR_SupplyTeam
|
|||||||
bool Registered;
|
bool Registered;
|
||||||
bool Released;
|
bool Released;
|
||||||
vector OrderedContact;
|
vector OrderedContact;
|
||||||
|
ref ERR_ConvoyBoardingProgress BoardingProgress;
|
||||||
|
float BoardingRetryAt;
|
||||||
void Clear()
|
void Clear()
|
||||||
{
|
{
|
||||||
foreach (AIWaypoint waypoint : Orders)
|
foreach (AIWaypoint waypoint : Orders)
|
||||||
@@ -36,6 +38,17 @@ class ERR_SupplyTeam
|
|||||||
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
array<AIAgent> agents = {}; Group.GetAgents(agents);
|
||||||
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
foreach (AIAgent agent : agents) if (agent) agent.SetPermanentLOD(-1);
|
||||||
}
|
}
|
||||||
|
int BoardingCount(Vehicle truck, out float remainingDistance)
|
||||||
|
{
|
||||||
|
int seated; remainingDistance = 0;
|
||||||
|
foreach (IEntity member : Members)
|
||||||
|
{
|
||||||
|
if (!ERR_Actors.CanFight(member) || ERR_Actors.Player(member) || ERR_Actors.Group(member) != Group) continue;
|
||||||
|
if (CompartmentAccessComponent.GetVehicleIn(member) == truck) seated++;
|
||||||
|
else remainingDistance = Math.Max(remainingDistance, vector.DistanceXZ(member.GetOrigin(), truck.GetOrigin()));
|
||||||
|
}
|
||||||
|
return seated;
|
||||||
|
}
|
||||||
bool Living()
|
bool Living()
|
||||||
{
|
{
|
||||||
foreach (IEntity member : Members)
|
foreach (IEntity member : Members)
|
||||||
@@ -125,6 +138,7 @@ class ERR_SupplyConvoyTask : ERR_ConvoyTask
|
|||||||
{
|
{
|
||||||
ref array<ref ERR_SupplyTeam> Teams = {};
|
ref array<ref ERR_SupplyTeam> Teams = {};
|
||||||
ref ERR_SupplyConvoyTactics Tactics;
|
ref ERR_SupplyConvoyTactics Tactics;
|
||||||
|
ref ERR_SupplyConvoyColumn Column;
|
||||||
ref array<vector> Bases = {};
|
ref array<vector> Bases = {};
|
||||||
ref array<vector> Starts = {};
|
ref array<vector> Starts = {};
|
||||||
int LastBase = -1;
|
int LastBase = -1;
|
||||||
@@ -154,6 +168,8 @@ class ERR_SupplyConvoyTactics
|
|||||||
protected vector m_Left;
|
protected vector m_Left;
|
||||||
protected float m_LastContact = -1;
|
protected float m_LastContact = -1;
|
||||||
protected float m_NextOrders;
|
protected float m_NextOrders;
|
||||||
|
protected vector m_LastOrderContact;
|
||||||
|
protected bool m_OrdersIssued;
|
||||||
protected float m_BoardDeadline;
|
protected float m_BoardDeadline;
|
||||||
protected bool m_TruckWithdrawn;
|
protected bool m_TruckWithdrawn;
|
||||||
protected float m_NextDismount;
|
protected float m_NextDismount;
|
||||||
@@ -302,9 +318,10 @@ class ERR_SupplyConvoyTactics
|
|||||||
if (SupportPosition(index, desired, chosen)) support = chosen;
|
if (SupportPosition(index, desired, chosen)) support = chosen;
|
||||||
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20) orders.Add(Move(support));
|
if (vector.DistanceXZ(vehicle.Entity.GetOrigin(), support) > 20) orders.Add(Move(support));
|
||||||
if (index == 1 || index == 2) return;
|
if (index == 1 || index == 2) return;
|
||||||
AIWaypoint defend = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), support));
|
// Mounted escorts engage using native vehicle combat after reaching cover.
|
||||||
if (defend) defend.SetCompletionRadius(20);
|
// Defend is an infantry occupation task; deleting it during regroup could
|
||||||
orders.Add(defend);
|
// leave ActivityDefend executing with an already deleted waypoint.
|
||||||
|
// Do not assign it to a crew whose role requires remaining in the vehicle.
|
||||||
}
|
}
|
||||||
protected void Clear()
|
protected void Clear()
|
||||||
{
|
{
|
||||||
@@ -313,13 +330,18 @@ class ERR_SupplyConvoyTactics
|
|||||||
}
|
}
|
||||||
protected void Orders(bool dismount, float now)
|
protected void Orders(bool dismount, float now)
|
||||||
{
|
{
|
||||||
|
m_NextOrders = now + 30000;
|
||||||
|
if (m_OrdersIssued && vector.DistanceXZ(m_LastOrderContact, m_Contact) < 40) return;
|
||||||
|
m_LastOrderContact = m_Contact; m_OrdersIssued = true;
|
||||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, !m_TruckWithdrawn);
|
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, !m_TruckWithdrawn);
|
||||||
for (int i = 0; i < m_Task.Teams.Count(); i++) Flank(i, dismount);
|
for (int i = 0; i < m_Task.Teams.Count(); i++) Flank(i, dismount);
|
||||||
m_NextOrders = now + 30000;
|
m_NextOrders = now + 30000;
|
||||||
}
|
}
|
||||||
protected void Start(vector contact, float now)
|
protected void Start(vector contact, float now)
|
||||||
{
|
{
|
||||||
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false;
|
Clear(); m_State = 1; m_Contact = contact; m_TruckWithdrawn = false; m_OrdersIssued = false;
|
||||||
|
if (m_Task.Column) m_Task.Column.Reset(now);
|
||||||
|
m_Task.Waiting = false;
|
||||||
vector origin;
|
vector origin;
|
||||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||||
{
|
{
|
||||||
@@ -339,13 +361,62 @@ class ERR_SupplyConvoyTactics
|
|||||||
Clear(); m_State = 0;
|
Clear(); m_State = 0;
|
||||||
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||||
{
|
{
|
||||||
vehicle.Combat.Held = false;
|
vehicle.Combat.Held = false; vehicle.Arrived = false;
|
||||||
// Combat time does not consume the driving deadline.
|
// Combat time does not consume the driving deadline.
|
||||||
vehicle.LegDeadline = now + 600000;
|
vehicle.LegDeadline = now + 600000;
|
||||||
}
|
}
|
||||||
m_Task.TravelDeadline = now + 600000;
|
m_Task.TravelDeadline = now + 600000;
|
||||||
|
if (m_Task.Column) m_Task.Column.Reset(now, false, true);
|
||||||
ERR_HQ.Event("CONVOY REGROUP", m_Task.Receipt.SourceId + ": surviving convoy resumes; no replacements");
|
ERR_HQ.Event("CONVOY REGROUP", m_Task.Receipt.SourceId + ": surviving convoy resumes; no replacements");
|
||||||
}
|
}
|
||||||
|
protected float InfantryDistance(IEntity vehicle)
|
||||||
|
{
|
||||||
|
float distance = 1e20;
|
||||||
|
if (!vehicle) return distance;
|
||||||
|
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||||
|
{
|
||||||
|
if (team.Released) continue;
|
||||||
|
foreach (IEntity member : team.Members)
|
||||||
|
{
|
||||||
|
if (!ERR_Actors.CanFight(member) || CompartmentAccessComponent.GetVehicleIn(member)) continue;
|
||||||
|
distance = Math.Min(distance, vector.DistanceXZ(member.GetOrigin(), vehicle.GetOrigin()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return distance;
|
||||||
|
}
|
||||||
|
protected void ProtectInfantry()
|
||||||
|
{
|
||||||
|
foreach (ERR_ConvoyVehicle vehicle : m_Task.Vehicles)
|
||||||
|
{
|
||||||
|
if (vehicle.Released || !vehicle.Entity) continue;
|
||||||
|
AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));
|
||||||
|
if (!movement) continue;
|
||||||
|
float speed = 35;
|
||||||
|
if (InfantryDistance(vehicle.Entity) < 45) speed = 8;
|
||||||
|
movement.SetCruiseSpeed(speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected void RefreshBoarding(float now)
|
||||||
|
{
|
||||||
|
foreach (ERR_SupplyTeam team : m_Task.Teams)
|
||||||
|
{
|
||||||
|
if (team.Released || team.Aboard(m_Task.Vehicles[2].Entity, true)) continue;
|
||||||
|
if (team.BoardingRetryAt > 0)
|
||||||
|
{
|
||||||
|
if (now < team.BoardingRetryAt) continue;
|
||||||
|
team.BoardingRetryAt = 0;
|
||||||
|
team.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!team.BoardingProgress) team.BoardingProgress = new ERR_ConvoyBoardingProgress();
|
||||||
|
float distance; int seated = team.BoardingCount(m_Task.Vehicles[2].Entity, distance);
|
||||||
|
if (!team.BoardingProgress.Retry(seated, distance, now)) continue;
|
||||||
|
// Let the old native activity release reservations before asking for seats
|
||||||
|
// again. Occupants remain seated; no direct compartment move is performed.
|
||||||
|
team.Clear(); team.BoardingRetryAt = now + 1000;
|
||||||
|
ERR_HQ.Event("CONVOY BOARDING RETRY", string.Format("%1: team has %2 aboard; farthest remaining soldier %3 m from truck. No approach/seat progress for 30 s; retry %4/3", m_Task.Receipt.SourceId, seated, Math.Round(distance), team.BoardingProgress.Retries));
|
||||||
|
}
|
||||||
|
}
|
||||||
bool Tick(float now)
|
bool Tick(float now)
|
||||||
{
|
{
|
||||||
if (m_Stopped) return false;
|
if (m_Stopped) return false;
|
||||||
@@ -367,10 +438,10 @@ class ERR_SupplyConvoyTactics
|
|||||||
if (found) Report(observed, now, now);
|
if (found) Report(observed, now, now);
|
||||||
if (m_State == 1)
|
if (m_State == 1)
|
||||||
{
|
{
|
||||||
if (!m_TruckWithdrawn && InfantryAboard(false)) { m_TruckWithdrawn = true; Support(2, false); }
|
if (!m_TruckWithdrawn && InfantryAboard(false) && InfantryDistance(m_Task.Vehicles[2].Entity) >= 25) { m_TruckWithdrawn = true; Support(2, false); }
|
||||||
// Native exit attempts can fail while the truck brakes or a door is busy.
|
// Native exit attempts can fail while the truck brakes or a door is busy.
|
||||||
// Retry only still-mounted teams, never teleport actors or reset quiet time.
|
// Retry only still-mounted teams, never teleport actors or reset quiet time.
|
||||||
if (!m_TruckWithdrawn && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
|
if (!m_TruckWithdrawn && !InfantryAboard(false) && Usable(2) && now >= m_NextDismount && m_DismountAttempts < 6)
|
||||||
{
|
{
|
||||||
m_DismountAttempts++; m_NextDismount = now + 10000;
|
m_DismountAttempts++; m_NextDismount = now + 10000;
|
||||||
for (int retry = 0; retry < m_Task.Teams.Count(); retry++)
|
for (int retry = 0; retry < m_Task.Teams.Count(); retry++)
|
||||||
@@ -387,17 +458,23 @@ class ERR_SupplyConvoyTactics
|
|||||||
if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }
|
if (!Usable(2)) { foreach (ERR_SupplyTeam lost : m_Task.Teams) lost.Release(m_Mode); Finish(now); return false; }
|
||||||
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, true);
|
for (int i = 0; i < m_Task.Vehicles.Count(); i++) Support(i, true);
|
||||||
foreach (ERR_SupplyTeam boarding : m_Task.Teams)
|
foreach (ERR_SupplyTeam boarding : m_Task.Teams)
|
||||||
if (!boarding.Released && boarding.Living()) boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
|
if (!boarding.Released && boarding.Living())
|
||||||
m_State = 2; m_BoardDeadline = now + 120000;
|
{
|
||||||
|
boarding.BoardingProgress = new ERR_ConvoyBoardingProgress(); boarding.BoardingRetryAt = 0;
|
||||||
|
boarding.Add(Board(m_Task.Vehicles[2].Entity, true));
|
||||||
|
}
|
||||||
|
m_State = 2; m_BoardDeadline = now + 300000;
|
||||||
ERR_HQ.Event("CONVOY BOARDING", m_Task.Receipt.SourceId + ": both teams return under escort cover");
|
ERR_HQ.Event("CONVOY BOARDING", m_Task.Receipt.SourceId + ": both teams return under escort cover");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (m_State == 2 && Usable(2) && now < m_BoardDeadline) RefreshBoarding(now);
|
||||||
if (m_State == 2 && (!Usable(2) || InfantryAboard(true) || now >= m_BoardDeadline))
|
if (m_State == 2 && (!Usable(2) || InfantryAboard(true) || now >= m_BoardDeadline))
|
||||||
{
|
{
|
||||||
foreach (ERR_SupplyTeam stranded : m_Task.Teams)
|
foreach (ERR_SupplyTeam stranded : m_Task.Teams)
|
||||||
if (!stranded.Aboard(m_Task.Vehicles[2].Entity, true)) stranded.Release(m_Mode);
|
if (!stranded.Aboard(m_Task.Vehicles[2].Entity, true)) stranded.Release(m_Mode);
|
||||||
Finish(now);
|
Finish(now);
|
||||||
}
|
}
|
||||||
|
if (m_State != 0) ProtectInfantry();
|
||||||
return m_State != 0;
|
return m_State != 0;
|
||||||
}
|
}
|
||||||
void Stop() { if (m_Stopped) return; m_Stopped = true; Clear(); }
|
void Stop() { if (m_Stopped) return; m_Stopped = true; Clear(); }
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ modded class ERR_HQ
|
|||||||
if (!route.Nodes[i] || !route.Nodes[i].Valid(i == route.Nodes.Count() - 1)) return false;
|
if (!route.Nodes[i] || !route.Nodes[i].Valid(i == route.Nodes.Count() - 1)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
SCR_AIGroup ERR_AwarenessFixtureSource()
|
||||||
|
{
|
||||||
|
if (!ERR_NativeHQMemoryProbe.Enabled()) return null;
|
||||||
|
foreach (ERR_HQUnit unit : m_Units) if (unit.Group && unit.Utility) return unit.Group;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
ERR_HQRoute ERR_MemoryFixtureQueue(ERR_HQReport report)
|
ERR_HQRoute ERR_MemoryFixtureQueue(ERR_HQReport report)
|
||||||
{
|
{
|
||||||
if (!ERR_NativeHQMemoryProbe.Enabled()) return null;
|
if (!ERR_NativeHQMemoryProbe.Enabled()) return null;
|
||||||
@@ -34,10 +40,15 @@ modded class ERR_HQ
|
|||||||
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
ERR_HQRoute route = m_Network.Route(unit.Radio);
|
||||||
if (!ERR_MemoryFixtureHealthy(route)) continue;
|
if (!ERR_MemoryFixtureHealthy(route)) continue;
|
||||||
report.Source = unit.Group;
|
report.Source = unit.Group;
|
||||||
route.MessageId = report.MessageId;
|
// Arrange an isolated inbox so natural contacts cannot replace this fixture
|
||||||
route.Payload = "FIXTURE synthetic observed position; " + report.Description(report.ObservedAt);
|
// payload before it receives its message ID. Radio delivery remains native.
|
||||||
unit.PendingReport = report.Snapshot(); unit.Uplink = route;
|
unit.LocalAwareness.Clear();
|
||||||
return route;
|
unit.LocalAwareness.Remember(report, report.ObservedAt);
|
||||||
|
unit.NextContact = 0;
|
||||||
|
ContactReport(unit, GetGame().GetWorld().GetWorldTime());
|
||||||
|
if (!unit.Uplink || !unit.PendingReport || unit.PendingReport.Target != report.Target) return null;
|
||||||
|
report.MessageId = unit.PendingReport.MessageId;
|
||||||
|
return unit.Uplink;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -95,7 +106,7 @@ class ERR_NativeHQMemoryProbe
|
|||||||
{
|
{
|
||||||
string enabled, outcome;
|
string enabled, outcome;
|
||||||
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
return Replication.IsServer() && System.GetCLIParam("errSmoke", enabled) && enabled == "1"
|
||||||
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "hq-memory";
|
&& System.GetCLIParam("errSmokeOutcome", outcome) && (outcome == "hq-memory" || outcome == "hq-awareness");
|
||||||
}
|
}
|
||||||
void ERR_NativeHQMemoryProbe(ERR_GameMode mode) { m_Mode = mode; }
|
void ERR_NativeHQMemoryProbe(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
protected bool Check(bool condition, string description)
|
protected bool Check(bool condition, string description)
|
||||||
@@ -187,6 +198,47 @@ class ERR_NativeHQMemoryProbe
|
|||||||
}
|
}
|
||||||
return Check(false, "bounded road fixture found no suitable native candidate; no fallback or force mutation");
|
return Check(false, "bounded road fixture found no suitable native candidate; no fallback or force mutation");
|
||||||
}
|
}
|
||||||
|
protected bool LocalAwarenessPolicy(ERR_HQ hq, float now)
|
||||||
|
{
|
||||||
|
ERR_HQLocalAwareness cache = new ERR_HQLocalAwareness();
|
||||||
|
ERR_HQObservationHistory history = new ERR_HQObservationHistory();
|
||||||
|
SCR_AIGroup group = hq.ERR_AwarenessFixtureSource();
|
||||||
|
if (!Check(group != null, "awareness fixture has a native registered source squad")) return false;
|
||||||
|
ERR_HQReport visual = new ERR_HQReport();
|
||||||
|
visual.Target = m_Mode; visual.Source = group; visual.ObservedAt = now;
|
||||||
|
visual.Position = "123 4 456"; visual.SeenWeapons = "rifle";
|
||||||
|
cache.Remember(visual, now); visual.Position = "999 0 999";
|
||||||
|
ERR_HQReport delayed = cache.Peek(now + 2000, history);
|
||||||
|
if (!Check(delayed && delayed.Position == "123 4 456" && delayed.ObservedAt == now && delayed.SeenWeapons == "rifle",
|
||||||
|
"brief sighting keeps original position/time/equipment after two seconds")) return false;
|
||||||
|
history.Queued(delayed.Target, delayed.ObservedAt, now + 2000); cache.Queued(delayed, now + 2000);
|
||||||
|
if (!Check(!cache.Peek(now + 3000, history), "sent cached sighting cannot replay")) return false;
|
||||||
|
cache.Remember(visual, now);
|
||||||
|
if (!Check(!cache.Peek(now + 30001, new ERR_HQObservationHistory()), "local evidence expires after thirty seconds")) return false;
|
||||||
|
cache.Clear();
|
||||||
|
SCR_AIGroupUtilityComponent utility = SCR_AIGroupUtilityComponent.Cast(group.FindComponent(SCR_AIGroupUtilityComponent));
|
||||||
|
// An isolated native perception object exercises the actual callback without
|
||||||
|
// injecting the fake shooter into the real squad's combat targets.
|
||||||
|
SCR_AIGroupPerception perception = new SCR_AIGroupPerception(utility, group);
|
||||||
|
perception.ERR_BindAwareness(cache);
|
||||||
|
perception.AddOrUpdateGunshot(m_Mode, "123 4 456", m_Mode.PlayerFaction(), GetGame().GetPerceptionManager().GetTime(), false);
|
||||||
|
ERR_HQReport sound = cache.Peek(GetGame().GetWorld().GetWorldTime(), history);
|
||||||
|
if (!Check(sound && !sound.Target && sound.Source == group && sound.Position == "75 4 525"
|
||||||
|
&& sound.TravelMode == "heard gunfire" && sound.SeenWeapons == "unknown" && sound.SeenEscapists == -1,
|
||||||
|
"native heard-fire callback captures only a coarse area and original time")) return false;
|
||||||
|
sound.MessageId = 1;
|
||||||
|
ERR_HQMemory memory = new ERR_HQMemory();
|
||||||
|
if (!Check(memory.Ingest(sound, GetGame().GetWorld().GetWorldTime()), "heard-fire value report accepted separately from visual identity")) return false;
|
||||||
|
ERR_HQBelief noise = memory.At(0);
|
||||||
|
if (!Check(noise.Kind == ERR_HQMemoryKind.GUNFIRE && !noise.Target && !noise.HasVelocity && !noise.Actionable()
|
||||||
|
&& noise.Confidence <= 0.4 && noise.Radius >= 150, "sound permits uncertain search, not precision targeting or motion inference")) return false;
|
||||||
|
if (!Check(ERR_HQPursuitRules.Role(noise.Confidence, 0, noise.Radius, noise.HasVelocity, 0) == ERR_HQPursuitRole.SEARCH,
|
||||||
|
"heard gunfire selects a search role")) return false;
|
||||||
|
cache.Clear(); perception.ERR_BindAwareness(null);
|
||||||
|
perception.AddOrUpdateGunshot(m_Mode, "123 4 456", m_Mode.PlayerFaction(), GetGame().GetPerceptionManager().GetTime(), false);
|
||||||
|
if (!Check(!cache.Peek(GetGame().GetWorld().GetWorldTime(), history), "unbound perception cannot retain mission reports")) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
protected bool LocalPolicies()
|
protected bool LocalPolicies()
|
||||||
{
|
{
|
||||||
ERR_ForceLedger ledger = new ERR_ForceLedger();
|
ERR_ForceLedger ledger = new ERR_ForceLedger();
|
||||||
@@ -292,18 +344,27 @@ class ERR_NativeHQMemoryProbe
|
|||||||
if (now > m_Deadline) { Check(false, "fixture deadline"); return -1; }
|
if (now > m_Deadline) { Check(false, "fixture deadline"); return -1; }
|
||||||
if (m_Step == 0)
|
if (m_Step == 0)
|
||||||
{
|
{
|
||||||
if (!hq.ERR_ProbeSquadsReady()) return 0;
|
string outcome;
|
||||||
bool automaticWasChecked = m_AutoChecked;
|
System.GetCLIParam("errSmokeOutcome", outcome);
|
||||||
int automatic = AutomaticConvoy(hq);
|
bool awarenessOnly = outcome == "hq-awareness";
|
||||||
if (automatic <= 0) return automatic;
|
if (!awarenessOnly)
|
||||||
// Startup admission and native crew readiness have their own bounded wait.
|
{
|
||||||
// Start the original memory/radio deadline once, after automatic proof.
|
if (!hq.ERR_ProbeSquadsReady()) return 0;
|
||||||
if (!automaticWasChecked) m_Deadline = now + 120000;
|
bool automaticWasChecked = m_AutoChecked;
|
||||||
|
int automatic = AutomaticConvoy(hq);
|
||||||
|
if (automatic <= 0) return automatic;
|
||||||
|
// Legacy combined convoy/memory qualification retains its original checks.
|
||||||
|
if (!automaticWasChecked) m_Deadline = now + 120000;
|
||||||
|
}
|
||||||
if (!m_ValueChecked)
|
if (!m_ValueChecked)
|
||||||
{
|
{
|
||||||
if (!Check(hq.ERR_MemoryFixtureRegisteredForces() == 6,
|
if (!awarenessOnly)
|
||||||
"actual six site groups are registered once in the Core force ledger")) return -1;
|
{
|
||||||
if (!LocalPolicies() || !LocalRoadPolicy()) return -1;
|
if (!Check(hq.ERR_MemoryFixtureRegisteredForces() == 6,
|
||||||
|
"actual six site groups are registered once in the Core force ledger")) return -1;
|
||||||
|
if (!LocalPolicies() || !LocalRoadPolicy()) return -1;
|
||||||
|
}
|
||||||
|
if (!LocalAwarenessPolicy(hq, now)) return -1;
|
||||||
m_ValueChecked = true;
|
m_ValueChecked = true;
|
||||||
}
|
}
|
||||||
m_FirstPosition = m_Mode.PrisonPosition() + "300 0 300";
|
m_FirstPosition = m_Mode.PrisonPosition() + "300 0 300";
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ modded class ERR_GameMode
|
|||||||
protected bool m_ERRTestRadioNetwork;
|
protected bool m_ERRTestRadioNetwork;
|
||||||
protected bool m_ERRTestHQResponse;
|
protected bool m_ERRTestHQResponse;
|
||||||
protected bool m_ERRTestHQMemory;
|
protected bool m_ERRTestHQMemory;
|
||||||
|
protected bool m_ERRTestHQAwareness;
|
||||||
protected bool m_ERRTestLocalHandoff;
|
protected bool m_ERRTestLocalHandoff;
|
||||||
protected ref ERR_NativeLocalHandoffProbe m_ERRLocalHandoff;
|
protected ref ERR_NativeLocalHandoffProbe m_ERRLocalHandoff;
|
||||||
protected ERR_PlayerLife m_ERRLocalHandoffLife;
|
protected ERR_PlayerLife m_ERRLocalHandoffLife;
|
||||||
@@ -74,7 +75,8 @@ modded class ERR_GameMode
|
|||||||
m_ERRTestLostHelicopter = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "lost-helicopter";
|
m_ERRTestLostHelicopter = System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "lost-helicopter";
|
||||||
m_ERRTestRadioNetwork = outcome == "radio-network";
|
m_ERRTestRadioNetwork = outcome == "radio-network";
|
||||||
m_ERRTestHQResponse = outcome == "hq-response";
|
m_ERRTestHQResponse = outcome == "hq-response";
|
||||||
m_ERRTestHQMemory = outcome == "hq-memory";
|
m_ERRTestHQAwareness = outcome == "hq-awareness";
|
||||||
|
m_ERRTestHQMemory = outcome == "hq-memory" || m_ERRTestHQAwareness;
|
||||||
m_ERRTestLocalHandoff = outcome == "hq-local-handoff";
|
m_ERRTestLocalHandoff = outcome == "hq-local-handoff";
|
||||||
m_ERRTestHQBody = outcome == "hq-body";
|
m_ERRTestHQBody = outcome == "hq-body";
|
||||||
m_ERRTestInfantryTransfer = outcome == "infantry-transfer";
|
m_ERRTestInfantryTransfer = outcome == "infantry-transfer";
|
||||||
@@ -323,6 +325,24 @@ modded class ERR_GameMode
|
|||||||
Print("ERR SMOKE COMPLETE: native HQ patrol preemption and physical resumption");
|
Print("ERR SMOKE COMPLETE: native HQ patrol preemption and physical resumption");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (m_ERRTestHQAwareness)
|
||||||
|
{
|
||||||
|
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 291, "awareness and radio checks completed before explicit fixture reset")) return;
|
||||||
|
m_ERRSmokeDone = true;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||||
|
ERR_StopHQPatrol();
|
||||||
|
Print("ERR SMOKE COMPLETE: captured awareness, native heard-fire callback, physical radio delivery and memory; explicit fixture reset");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (m_ERRTestConvoy)
|
||||||
|
{
|
||||||
|
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 1061, "convoy checks completed before explicit fixture reset")) return;
|
||||||
|
m_ERRSmokeDone = true;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SmokeTick);
|
||||||
|
ERR_StopHQPatrol();
|
||||||
|
Print("ERR SMOKE COMPLETE: native convoy readiness, cargo and road movement; explicit fixture reset");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (m_ERRTestPlayerDeath)
|
if (m_ERRTestPlayerDeath)
|
||||||
{
|
{
|
||||||
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 91, "death exhausts round life and reaches defeat after spectator grace")) return;
|
if (!ERR_Check(current == ERR_Phase.DEFEAT && m_ERRSmokeStep == 91, "death exhausts round life and reaches defeat after spectator grace")) return;
|
||||||
@@ -522,6 +542,13 @@ modded class ERR_GameMode
|
|||||||
if (m_ERRTestMortarSalvo)
|
if (m_ERRTestMortarSalvo)
|
||||||
{
|
{
|
||||||
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
DamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
|
// Keep the isolated firing range clear of automatic UAZ responders.
|
||||||
|
// Otherwise they pursue the inert test report into its impact area.
|
||||||
|
if (ERR_HQ.Get().Hunters()) ERR_HQ.Get().Hunters().Stop();
|
||||||
|
if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "mortar fixture unlocks prison to start normal crew AI")) return;
|
||||||
|
// The player is not this fixture's target. Keep the newly released
|
||||||
|
// prisoner away from the battery while its inert report is tested.
|
||||||
|
m_ERRSmokePlayer.SetOrigin(ERR_Actors.Ground("4500 0 11500"));
|
||||||
m_ERRMortarSalvo = new ERR_NativeMortarSalvoProbe(this);
|
m_ERRMortarSalvo = new ERR_NativeMortarSalvoProbe(this);
|
||||||
m_ERRSmokeStep = 190;
|
m_ERRSmokeStep = 190;
|
||||||
return;
|
return;
|
||||||
@@ -586,6 +613,12 @@ modded class ERR_GameMode
|
|||||||
if (convoyOutcome == "supply-convoys")
|
if (convoyOutcome == "supply-convoys")
|
||||||
{
|
{
|
||||||
if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "supply fixture unlocks prison to start normal mission AI")) return;
|
if (!ERR_Check(UnlockPrison(m_ERRSmokePlayer), "supply fixture unlocks prison to start normal mission AI")) return;
|
||||||
|
if (ERR_HQ.Get().Hunters()) ERR_HQ.Get().Hunters().Stop();
|
||||||
|
m_ERRSmokePlayer.SetOrigin(ERR_Actors.Ground("8800 0 2700"));
|
||||||
|
PerceivableComponent convoyObserver = PerceivableComponent.Cast(m_ERRSmokePlayer.FindComponent(PerceivableComponent));
|
||||||
|
if (!ERR_Check(convoyObserver != null, "convoy fixture observer has native perception component")) return;
|
||||||
|
convoyObserver.SetPerceivedFactionOverride(EnemyFaction());
|
||||||
|
Print("ERR SUPPLY FIXTURE: observer perceived as convoy-friendly; only the labelled contact injection exercises this fixture's combat cycle");
|
||||||
m_ERRConvoyProbe = new ERR_NativeSupplyConvoyProbe(this);
|
m_ERRConvoyProbe = new ERR_NativeSupplyConvoyProbe(this);
|
||||||
}
|
}
|
||||||
else m_ERRConvoyProbe = new ERR_NativeConvoyProbe(this);
|
else m_ERRConvoyProbe = new ERR_NativeConvoyProbe(this);
|
||||||
@@ -836,18 +869,20 @@ modded class ERR_GameMode
|
|||||||
int result = m_ERRConvoyProbe.Tick(ERR_HQ.Cast(Command()), now);
|
int result = m_ERRConvoyProbe.Tick(ERR_HQ.Cast(Command()), now);
|
||||||
if (result == 0) return;
|
if (result == 0) return;
|
||||||
if (!ERR_Check(result == 1, "native six-vehicle convoy readiness, cargo and road movement")) return;
|
if (!ERR_Check(result == 1, "native six-vehicle convoy readiness, cargo and road movement")) return;
|
||||||
m_ERRSmokeStep = 9;
|
m_ERRSmokeStep = 1061;
|
||||||
SCR_CharacterDamageManagerComponent damage;
|
Transition(ERR_Phase.DEFEAT);
|
||||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
|
||||||
damage.EnableDamageHandling(true);
|
|
||||||
damage.Kill(Instigator.CreateInstigator(m_ERRSmokePlayer));
|
|
||||||
m_ERRSmokeWait = now + 4000;
|
|
||||||
}
|
}
|
||||||
else if (m_ERRSmokeStep == 105)
|
else if (m_ERRSmokeStep == 105)
|
||||||
{
|
{
|
||||||
int result = m_ERRHQMemoryProbe.Tick(ERR_HQ.Cast(Command()), now);
|
int result = m_ERRHQMemoryProbe.Tick(ERR_HQ.Cast(Command()), now);
|
||||||
if (result == 0) return;
|
if (result == 0) return;
|
||||||
if (!ERR_Check(result == 1, "native HQ memory delivery and search lineage")) return;
|
if (!ERR_Check(result == 1, "native HQ memory delivery and search lineage")) return;
|
||||||
|
if (m_ERRTestHQAwareness)
|
||||||
|
{
|
||||||
|
m_ERRSmokeStep = 291;
|
||||||
|
Transition(ERR_Phase.DEFEAT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
m_ERRSmokeStep = 9;
|
m_ERRSmokeStep = 9;
|
||||||
SCR_CharacterDamageManagerComponent damage;
|
SCR_CharacterDamageManagerComponent damage;
|
||||||
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
damage = SCR_CharacterDamageManagerComponent.Cast(m_ERRSmokePlayer.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
|
|||||||
@@ -52,9 +52,22 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
|||||||
}
|
}
|
||||||
if (m_CombatTask)
|
if (m_CombatTask)
|
||||||
{
|
{
|
||||||
|
for (int car = 0; car < m_CombatTask.Vehicles.Count(); car++)
|
||||||
|
{
|
||||||
|
ERR_ConvoyVehicle diagnosticCar = m_CombatTask.Vehicles[car];
|
||||||
|
vector target; if (diagnosticCar.Waypoint) target = diagnosticCar.Waypoint.GetOrigin();
|
||||||
|
PrintFormat("ERR SUPPLY COLUMN: car=%1 released=%2 arrived=%3 position=%4 target=%5", car + 1, diagnosticCar.Released, diagnosticCar.Arrived, diagnosticCar.Entity.GetOrigin(), target);
|
||||||
|
if (diagnosticCar.Waypoint) PrintFormat("ERR SUPPLY WAYPOINT: car=%1 distance=%2 nativeCurrent=%3 nativeComplete=%4 routeIndex=%5", car + 1, vector.DistanceXZ(diagnosticCar.Entity.GetOrigin(), target), diagnosticCar.Group.GetCurrentWaypoint() == diagnosticCar.Waypoint, diagnosticCar.Waypoint.IsWithinCompletionRadius(diagnosticCar.Group), diagnosticCar.RouteIndex);
|
||||||
|
}
|
||||||
ERR_ConvoyVehicle truck = m_CombatTask.Vehicles[2];
|
ERR_ConvoyVehicle truck = m_CombatTask.Vehicles[2];
|
||||||
SCR_VehicleDamageManagerComponent health = SCR_VehicleDamageManagerComponent.Cast(truck.Entity.FindComponent(SCR_VehicleDamageManagerComponent));
|
SCR_VehicleDamageManagerComponent health = SCR_VehicleDamageManagerComponent.Cast(truck.Entity.FindComponent(SCR_VehicleDamageManagerComponent));
|
||||||
PrintFormat("ERR SUPPLY TRUCK: released=%1 driverAlive=%2 driverGroup=%3 driverAboard=%4 health=%5 retained=%6 position=%7", truck.Released, ERR_Actors.CanFight(truck.Driver), ERR_Actors.Group(truck.Driver) == truck.Group, CompartmentAccessComponent.GetVehicleIn(truck.Driver) == truck.Entity, health.GetHealthScaled(), ERR_ConvoyMountedCrew.Retain(truck.Driver, truck.Entity), truck.Entity.GetOrigin());
|
PrintFormat("ERR SUPPLY TRUCK: released=%1 driverAlive=%2 driverGroup=%3 driverAboard=%4 health=%5 retained=%6 position=%7", truck.Released, ERR_Actors.CanFight(truck.Driver), ERR_Actors.Group(truck.Driver) == truck.Group, CompartmentAccessComponent.GetVehicleIn(truck.Driver) == truck.Entity, health.GetHealthScaled(), ERR_ConvoyMountedCrew.Retain(truck.Driver, truck.Entity), truck.Entity.GetOrigin());
|
||||||
|
SCR_AIGroupUtilityComponent driverGroup = truck.Group.GetGroupUtilityComponent();
|
||||||
|
SCR_ChimeraAIAgent truckDriver = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(truck.Driver));
|
||||||
|
string groupAction = "unavailable", driverAction = "unavailable"; bool registeredVehicle;
|
||||||
|
if (driverGroup) { groupAction = MovementAction(driverGroup.GetCurrentAction()); registeredVehicle = driverGroup.IsUsableVehicle(truck.Entity); }
|
||||||
|
if (truckDriver && truckDriver.m_UtilityComponent) driverAction = MovementAction(truckDriver.m_UtilityComponent.GetCurrentBehavior());
|
||||||
|
PrintFormat("ERR SUPPLY DRIVER: registeredVehicle=%1 groupAction=%2 driverAction=%3", registeredVehicle, groupAction, driverAction);
|
||||||
foreach (ERR_SupplyTeam diagnostic : m_CombatTask.Teams)
|
foreach (ERR_SupplyTeam diagnostic : m_CombatTask.Teams)
|
||||||
{
|
{
|
||||||
int aboard;
|
int aboard;
|
||||||
@@ -62,9 +75,13 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
|||||||
PrintFormat("ERR SUPPLY INFANTRY: aboard=%1 members=%2 released=%3 groupPosition=%4 orders=%5", aboard, diagnostic.Members.Count(), diagnostic.Released, diagnostic.Group.GetOrigin(), diagnostic.Orders.Count());
|
PrintFormat("ERR SUPPLY INFANTRY: aboard=%1 members=%2 released=%3 groupPosition=%4 orders=%5", aboard, diagnostic.Members.Count(), diagnostic.Released, diagnostic.Group.GetOrigin(), diagnostic.Orders.Count());
|
||||||
foreach (IEntity remaining : diagnostic.Members)
|
foreach (IEntity remaining : diagnostic.Members)
|
||||||
{
|
{
|
||||||
|
if (!remaining) { Print("ERR SUPPLY SEAT: original actor removed"); continue; }
|
||||||
AIControlComponent control = AIControlComponent.Cast(remaining.FindComponent(AIControlComponent));
|
AIControlComponent control = AIControlComponent.Cast(remaining.FindComponent(AIControlComponent));
|
||||||
SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent());
|
SCR_ChimeraAIAgent agent;
|
||||||
|
if (control) agent = SCR_ChimeraAIAgent.Cast(control.GetControlAIAgent());
|
||||||
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(remaining.FindComponent(CompartmentAccessComponent));
|
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(remaining.FindComponent(CompartmentAccessComponent));
|
||||||
|
if (!agent || !agent.m_UtilityComponent || !access)
|
||||||
|
{ PrintFormat("ERR SUPPLY SEAT: original actor has no live AI/compartment utility; alive=%1 fit=%2", ERR_Actors.Alive(remaining), ERR_Actors.CanFight(remaining)); continue; }
|
||||||
PrintFormat("ERR SUPPLY SEAT: name=%1 fit=%2 active=%3 canExit=%4 exiting=%5 action=%6 waypoint=%7", CompartmentAccessComponent.GetVehicleIn(remaining) == truck.Entity, ERR_Actors.CanFight(remaining), agent.IsAIActivated(), access.CanGetOutVehicle(), access.IsGettingOut(), agent.m_UtilityComponent.GetCurrentAction(), diagnostic.Group.GetCurrentWaypoint());
|
PrintFormat("ERR SUPPLY SEAT: name=%1 fit=%2 active=%3 canExit=%4 exiting=%5 action=%6 waypoint=%7", CompartmentAccessComponent.GetVehicleIn(remaining) == truck.Entity, ERR_Actors.CanFight(remaining), agent.IsAIActivated(), access.CanGetOutVehicle(), access.IsGettingOut(), agent.m_UtilityComponent.GetCurrentAction(), diagnostic.Group.GetCurrentWaypoint());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,12 +127,13 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
|||||||
if (original.Starts.Count() != 6) return -1;
|
if (original.Starts.Count() != 6) return -1;
|
||||||
foreach (vector start : original.Starts) { m_StartPositions.Insert(start); m_Moved.Insert(false); }
|
foreach (vector start : original.Starts) { m_StartPositions.Insert(start); m_Moved.Insert(false); }
|
||||||
}
|
}
|
||||||
|
m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0));
|
||||||
Print("ERR SUPPLY PASS: production auto-started three six-vehicle convoys, exact ledger admission, two separate seated teams each, finite native cargo");
|
Print("ERR SUPPLY PASS: production auto-started three six-vehicle convoys, exact ledger admission, two separate seated teams each, finite native cargo");
|
||||||
m_Phase = 1; m_PhaseBegan = now;
|
m_Phase = 1; m_PhaseBegan = now;
|
||||||
}
|
}
|
||||||
if (m_Phase == 1)
|
if (m_Phase == 1)
|
||||||
{
|
{
|
||||||
if (!m_AllMoved && now - m_PhaseBegan < 45000) return 0;
|
if (!m_AllMoved) return 0;
|
||||||
m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0));
|
m_CombatTask = ERR_SupplyConvoyTask.Cast(hq.Convoys().At(0));
|
||||||
vector contact = m_CombatTask.Vehicles[2].Entity.GetOrigin() + "0 0 200";
|
vector contact = m_CombatTask.Vehicles[2].Entity.GetOrigin() + "0 0 200";
|
||||||
m_CombatTask.Tactics.Report(contact, now, now);
|
m_CombatTask.Tactics.Report(contact, now, now);
|
||||||
@@ -137,18 +155,29 @@ class ERR_NativeSupplyConvoyProbe : ERR_NativeConvoyProbe
|
|||||||
if (boarded.Released || !boarded.Aboard(m_CombatTask.Vehicles[2].Entity, true)) return -1;
|
if (boarded.Released || !boarded.Aboard(m_CombatTask.Vehicles[2].Entity, true)) return -1;
|
||||||
Print("ERR SUPPLY PASS: both original teams reboarded and production travel resumed after quiet interval");
|
Print("ERR SUPPLY PASS: both original teams reboarded and production travel resumed after quiet interval");
|
||||||
m_TestLegs = m_CombatTask.Legs; m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin();
|
m_TestLegs = m_CombatTask.Legs; m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin();
|
||||||
m_CombatTask.TravelDeadline = now; m_CombatTask.Waiting = false;
|
Print("ERR SUPPLY FIXTURE: waiting for physical column arrival, production pause and next base leg; no forced timeout arrival");
|
||||||
Print("ERR SUPPLY FIXTURE: expire this trip's travel deadline to exercise the real pause and next base leg without a ten-minute test delay");
|
|
||||||
m_Phase = 3;
|
m_Phase = 3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (m_Phase == 3 && m_CombatTask.Legs > m_TestLegs && vector.DistanceXZ(m_CombatTask.Vehicles[2].Entity.GetOrigin(), m_RouteResumePosition) >= 20)
|
if (m_Phase >= 3) return Departure();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
protected int Departure()
|
||||||
|
{
|
||||||
|
if (m_CombatTask.Stopped || m_CombatTask.Vehicles[2].Released || m_CombatTask.Bases.Count() < 2) return -1;
|
||||||
|
if (m_Phase == 3 && m_CombatTask.Legs > m_TestLegs)
|
||||||
{
|
{
|
||||||
if (m_CombatTask.Stopped || m_CombatTask.Vehicles[2].Released || m_CombatTask.Bases.Count() < 2) return -1;
|
m_RouteResumePosition = m_CombatTask.Vehicles[2].Entity.GetOrigin();
|
||||||
Print("ERR SUPPLY PASS: original truck drives a new native base leg after the production pause; no replacement convoy");
|
|
||||||
m_Phase = 4;
|
m_Phase = 4;
|
||||||
|
Print("ERR SUPPLY FIXTURE: next leg started; record original truck position now, excluding all earlier regroup movement");
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
if (m_Phase == 4 && m_AllMoved) return 1;
|
if (m_Phase == 4 && vector.DistanceXZ(m_CombatTask.Vehicles[2].Entity.GetOrigin(), m_RouteResumePosition) >= 20)
|
||||||
|
{
|
||||||
|
Print("ERR SUPPLY PASS: original truck drives twenty metres AFTER departure on the new native base leg; no replacement convoy");
|
||||||
|
m_Phase = 5;
|
||||||
|
}
|
||||||
|
if (m_Phase == 5 && m_AllMoved) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
override void Stop() {}
|
override void Stop() {}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ GameProject {
|
|||||||
}
|
}
|
||||||
Configurations {
|
Configurations {
|
||||||
GameProjectConfig PC {
|
GameProjectConfig PC {
|
||||||
|
InputManagerSettings InputManagerSettings "{50A48858902D5F64}" {
|
||||||
|
Default "{A7B19F416E541000}Configs/ERR/ObserverInput.conf"
|
||||||
|
}
|
||||||
MenuManagerSettings MenuManagerSettings "{50C890C996B0A6FC}" {
|
MenuManagerSettings MenuManagerSettings "{50C890C996B0A6FC}" {
|
||||||
MenuConfigs {
|
MenuConfigs {
|
||||||
"{C747AFB6B750CE9A}Configs/System/chimeraMenus.conf"
|
"{C747AFB6B750CE9A}Configs/System/chimeraMenus.conf"
|
||||||
|
|||||||
@@ -33,12 +33,14 @@ The mission currently has a **medical supply stash**, not a spawned hospital bui
|
|||||||
|
|
||||||
## Skill, accuracy and perception
|
## Skill, accuracy and perception
|
||||||
|
|
||||||
Mission-owned hostile military AI use `EAISkill.CYLON` for maximum skill while the native aim-error node uses the `REGULAR` random-error distribution. This avoids CYLON's zero-error shooting. Recognition uses the native alerted factor (2.5) even in safe/suppressed states, preserving equipment factors and the engine's visibility/occlusion checks. This does not create through-wall vision or knowledge of unseen players.
|
Mission-owned hostile military AI use `EAISkill.CYLON` for maximum skill while the native aim-error node uses the `REGULAR` random-error distribution with deviations from the mean reduced by 10%. This avoids CYLON's zero-error shooting. Recognition uses the native alerted factor (2.5) even in safe/suppressed states, preserving equipment factors and the engine's visibility/occlusion checks. This does not create through-wall vision or knowledge of unseen players.
|
||||||
|
|
||||||
The authorized original AI code supplied the scoped skill/equipment and detection-hook approach. The separate aim-error reduction is new: no corresponding independent accuracy hook was found in the inspected original. Native `SCR_AIGetAimErrorOffset` and `SCR_AICombatComponent` implementations were used to separate these behaviours.
|
The authorized original AI code supplied the scoped skill/equipment and detection-hook approach. The separate aim-error reduction is new: no corresponding independent accuracy hook was found in the inspected original. Native `SCR_AIGetAimErrorOffset` and `SCR_AICombatComponent` implementations were used to separate these behaviours.
|
||||||
|
|
||||||
Actual enemy-detection events activate local native combat and bring forward the next HQ report; report delivery still obeys radio routing. Primary-enemy units share HQ. Other hostile factions receive the same scoped skill/aim policy but do not leak intelligence to a mutually hostile faction. Specialist scouts and flight crews retain their authored equipment. Captured/ineligible actors are excluded from the profile, and normal teardown restores prior skill and native perception.
|
Actual enemy-detection events activate local native combat and bring forward the next HQ report; report delivery still obeys radio routing. Primary-enemy units share HQ. Other hostile factions receive the same scoped skill/aim policy but do not leak intelligence to a mutually hostile faction. Specialist scouts and flight crews retain their authored equipment. Captured/ineligible actors are excluded from the profile, and normal teardown restores prior skill and native perception.
|
||||||
|
|
||||||
|
See [local awareness and heard fighting](ai-local-awareness.md) for immediate visual snapshots, prioritized reports and uncertain gunfire investigation.
|
||||||
|
|
||||||
## Audit trail and adjustment
|
## Audit trail and adjustment
|
||||||
|
|
||||||
The complete HQ journal is `$logs:rereintroduced-hq.log` (existing filename retained). Relevant events:
|
The complete HQ journal is `$logs:rereintroduced-hq.log` (existing filename retained). Relevant events:
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Local observations and HQ response
|
||||||
|
|
||||||
|
Mission-owned primary-enemy squads now capture visual evidence in the native
|
||||||
|
`SCR_AIGroupPerception.AddOrUpdateTarget` callback, before the next one-second HQ
|
||||||
|
update. Capture still requires an enemy target seen within 0.5 seconds, a clear
|
||||||
|
native trace and positive exposure. The source, last-seen position, observation
|
||||||
|
time and visible equipment are copied immediately. Subsequent movement or gear
|
||||||
|
changes do not alter that report.
|
||||||
|
|
||||||
|
Each squad keeps at most 16 visual snapshots and one sound report, for at most
|
||||||
|
30 seconds. Repeated visual capture is limited to once per second per retained
|
||||||
|
target. Sending uses the existing observation watermark, so polling cannot resend
|
||||||
|
the same observation. This is local squad knowledge; only successful physical
|
||||||
|
radio delivery creates HQ memory. A missing radio route leaves the snapshot
|
||||||
|
pending until it expires. An accepted transmission is not duplicated if it fails.
|
||||||
|
|
||||||
|
Captured contacts can use the next free uplink, with at most one attempt per five
|
||||||
|
seconds. They can precede routine status, but status overdue by 15 seconds gets
|
||||||
|
the next free slot. Existing transmissions are allowed to finish. The saved
|
||||||
|
report interval still controls the ordinary polling path; the new event path
|
||||||
|
works even on an existing server profile with a ten-second report interval.
|
||||||
|
A delivered report brings forward the next HQ planning pass.
|
||||||
|
|
||||||
|
## Heard fighting
|
||||||
|
|
||||||
|
The native `AddOrUpdateGunshot` callback supplies hostile-fire evidence. Native
|
||||||
|
weapon danger reactions invoke it for audible shots or incoming fire; grenade
|
||||||
|
danger can also notify this native group path. The mission does not scan player
|
||||||
|
positions or invent a hearing radius. Capture is scoped to bound, server-owned,
|
||||||
|
primary-enemy AI groups during the active post-prison mission. Unregistration
|
||||||
|
and round teardown detach the hook and discard pending observations.
|
||||||
|
|
||||||
|
A sound report carries a 150-metre grid area's centre, its observation time and
|
||||||
|
the reporting squad. Shooter identity, player count, equipment and movement are
|
||||||
|
not sent. Sound updates are coalesced to one per second and radio reports to one
|
||||||
|
per 30 seconds per squad. Visual evidence takes precedence in the local inbox.
|
||||||
|
|
||||||
|
Delivered sound has its own memory kind, keyed by reporting squad rather than an
|
||||||
|
unseen shooter. Initial confidence is 0.4, halves every 60 seconds, and positional
|
||||||
|
uncertainty starts at 150 metres and grows by 2 metres per second. Repeated sounds
|
||||||
|
never infer target velocity. A visual sighting creates a separate stronger track.
|
||||||
|
|
||||||
|
HQ can allocate one existing mobile search squad per sound task, within the
|
||||||
|
current response radius, total assignment limit and hunt cooldown. This uses the
|
||||||
|
ordinary radio downlink, search waypoint and negative-search report. Sound alone
|
||||||
|
cannot authorize mortar/aircraft targeting, paratrooper targeting or a hunter
|
||||||
|
UAZ precision interception. Permanent objective defenders, mortar crews and
|
||||||
|
other reserved specialists remain excluded from mobile assignments. Native
|
||||||
|
local combat and the existing defensive waypoints remain responsible for their
|
||||||
|
immediate defensive movement.
|
||||||
|
|
||||||
|
Maximum AI skill, alerted recognition and Regular aim error are unchanged. No
|
||||||
|
additional soldiers or vehicles are spawned by this change.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`test_hq_local_awareness.py` executes the production cache and contact scheduler
|
||||||
|
with engine/radio adapters: frozen evidence, expiry, coalescing, capacity, duplicate
|
||||||
|
rejection, route loss/retry, captured-contact priority and bounded status delay.
|
||||||
|
The existing memory, radio fairness and local handoff tests cover the new sound
|
||||||
|
kind and retained observation age. The dedicated `hq-awareness` native fixture exercises the
|
||||||
|
cache, isolated native gunfire callback, sound confidence/targeting restrictions,
|
||||||
|
and production cached-contact intake followed by physical radio delivery. Its
|
||||||
|
injected observations do not prove natural hearing/visibility or combat tactics
|
||||||
|
in a live multiplayer session.
|
||||||
|
|
||||||
|
Validation on 2026-09-25: repository checks passed; the dedicated Workbench
|
||||||
|
fixture compiled and completed its awareness, physical radio, memory and teardown
|
||||||
|
assertions. The launcher encountered a terminating-process classification race
|
||||||
|
while verifying cleanup; a subsequent private-prefix idle check passed. Logs and
|
||||||
|
source manifests, including earlier failed fixture attempts, are retained under
|
||||||
|
`.local/analysis/ai-awareness-2026-09-25/`. The fixture arranges an isolated local
|
||||||
|
inbox so concurrent natural contacts cannot replace its injected payload.
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# RHS airborne validation — 27 September 2026
|
||||||
|
|
||||||
|
The tests use the installed RHS/WCS dependencies, US prisoners and RHS ION enemies in an isolated Workbench game. They do not publish the mod or change RKS 2.
|
||||||
|
|
||||||
|
## Physical drop
|
||||||
|
|
||||||
|
The first run used the existing controlled-order fixture. Eight original RHS passengers boarded the aircraft, all eight left in two-second intervals, all eight boarded their native parachute compartments, all eight finished their descents alive, and both original four-person groups received the carried HQ search order. The original aircrew remained aboard and post-drop cover became active. The run was deliberately stopped after these checks so the next tests could exercise automatic authorization; it did not qualify the reload cycle.
|
||||||
|
|
||||||
|
The archived journal contains exactly eight `AIR DROP JUMP`, eight `PARACHUTE BOARDED`, eight `PARACHUTE FINISHED` and two `AIR TEAM LANDED` events.
|
||||||
|
|
||||||
|
## Automatic HQ decision
|
||||||
|
|
||||||
|
The upgraded fixture places only the test player in a visible runway-sector encounter and protects that player from damage for the duration of the test. Crew perception, sighting reports, physical radio delivery, HQ memory, airborne policy, corridor selection and passenger transfer remain production behavior. It no longer calls `Begin` with a fabricated drop order.
|
||||||
|
|
||||||
|
The first automatic run correctly withheld a drop with `ground forces already committed in sufficient strength`. That is intended policy, not a boarding or parachute failure. This run was stopped after observing the refusal.
|
||||||
|
|
||||||
|
For the complementary shortage scenario, the fixture reserves other ground squads using the existing `ReserveGroup` API while waiting for authorization. It does not fabricate sightings, change the airborne policy, bypass artillery safety, or issue a drop command. After authorization, it stops reserving squads so landed teams can receive normal HQ search orders. It also checks cover, return, servicing, another eight seated passengers and the separate airport cargo helicopter. The existing fixture advances the remaining cover timer after confirming cover, avoiding a five-minute idle wait; policy tests check the actual cover deadline independently.
|
||||||
|
|
||||||
|
The shortage run exposed a real starvation issue: each artillery veto reset the 45-second confirmation timer. Its first clear window ran from mission time 189.3 to 224.5 seconds (35.2 seconds), too short for confirmation before another mortar order reset it again.
|
||||||
|
|
||||||
|
`ERR_AirSupportPolicy.Consider` now validates and retains fresh contact/shortage history while artillery blocks the approach. It still refuses authorization while artillery is busy. Invalid/stale intelligence, a new target, sufficient ground support and the existing cooldown keep their previous checks. The order receiver still revalidates airspace using `Refusal` with its default airspace check enabled. A regression fails on the previous implementation and passes with the correction, including stale-target and cooldown cases.
|
||||||
|
|
||||||
|
The corrected-timer run then exposed a second blocker: successive reports from multiple observers could be less than two seconds apart. `ERR_HQMemoryTrack.Evaluate` used only the last two samples, repeatedly discarding a valid stationary-target history as unknown movement. Its bounded history now selects the nearest older sample with the existing two-second minimum separation. It stops at travel-mode changes and never skips an implausible eligible pair to find a convenient older estimate. Existing speed, freshness and prediction bounds are retained. A regression reproduces the previous failure and covers stationary sightings, movement, mode changes, implausible jumps and insufficient history.
|
||||||
|
|
||||||
|
The final corrected shortage run completed all seven native assertions:
|
||||||
|
|
||||||
|
- HQ automatically authorized the drop from native sightings and delivered radio reports at 20:01:23.
|
||||||
|
- All eight original passengers jumped at two-second intervals, boarded their parachutes, and completed descent without damage. Both original squads were released by 20:02:52 and received HQ search assignments for the carried target.
|
||||||
|
- Post-drop cover became active with the original aircraft and aircrew retained.
|
||||||
|
- The same helicopter returned to the airfield, serviced, boarded another eight passengers and departed at 20:06:46. The separate cargo helicopter was waiting at the airport.
|
||||||
|
|
||||||
|
The runner recorded `smoke-completed`, confirmed cleanup and verified that source and external dependencies remained unchanged throughout the run. The journal contains eight jumps, eight parachute boardings, eight completed descents, two landed teams and two completed passenger loads.
|
||||||
|
|
||||||
|
The generic native report still exits nonzero for existing dependency/resource errors: RHS/WCS localization GUIDs, prefab keywords and two inherited parachute material remaps referring to nonexistent source material names. The same errors were present in the initial controlled-drop run; there are no new engine errors or script/runtime failures in the final run. These warnings did not prevent boarding, descent or landing, but this headless test does not establish the canopy's visual appearance on multiplayer clients.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The repository checks and production airborne-policy tests passed. They cover confidence, persistent sightings, ground-force availability, artillery veto, cooldown, cover and invalid inputs. Native results establish local Workbench behavior; dedicated multiplayer replication and client-side canopy rendering still require a server playthrough.
|
||||||
|
|
||||||
|
Evidence: `.local/analysis/parachutes-2026-09-27/`.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Convoy coordination review — 27 September 2026
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
The latest RKS 2 logs available for this review were from 26 September. That session contains a native `Defend Waypoint not provided to the node!` exception in a supply-convoy escort. The existing local escort correction removes infantry Defend orders from mounted crews; it was not running in that logged game.
|
||||||
|
|
||||||
|
The supply controller also had coordination problems independent of that exception:
|
||||||
|
|
||||||
|
- Each vehicle independently drove to the same route/base destination at the same cruise speed. There was no spacing controller or leader wait for slower vehicles.
|
||||||
|
- Being within 250 metres of a base immediately counted as arrival. A shared ten-minute deadline also marked every remaining vehicle arrived regardless of location.
|
||||||
|
- Combat refreshed support and infantry waypoints every thirty seconds even when the contact had not moved, interrupting native manoeuvres.
|
||||||
|
- Native move completion and a check against the vehicle origin need different tolerances. The first new-controller test reproduced lead vehicles stopping short of a physical completion check when both used the same radius.
|
||||||
|
|
||||||
|
## Current behavior
|
||||||
|
|
||||||
|
`ERR_SupplyConvoyColumn` owns ordinary travel. The initial lead vehicle follows the existing road route; each follower targets the preceding surviving vehicle rather than the distant base. Targets remain stable through small position changes. Followers regulate cruise speed, leave a spacing gap, and can catch up. The leader waits when a gap between neighbours exceeds 160 metres and resumes below 90 metres. After combat and at a new base leg, a stable travel order is formed from surviving vehicles’ physical distance to the next destination. This prevents restoring spawn order by sending trucks toward each other; canonical vehicle identities and combat roles remain unchanged. Released vehicles are skipped and leadership preserves route progress. A replacement leader still has to physically reach the endpoint, even if its predecessor had already completed that route.
|
||||||
|
|
||||||
|
The leader uses a 15-metre native waypoint radius and a 35-metre physical arrival check. Followers finish in sequence within 75 metres of their predecessor after it arrives. The normal 45–90 second base pause starts only after this physical column arrival. Timeouts do not advance the trip.
|
||||||
|
|
||||||
|
The troop truck waits until dismounted infantry are at least 25 metres away before withdrawing. Owned convoy vehicles slow to 8 km/h within 45 metres of their fit dismounted teams; column travel restores the cruising speed after regroup.
|
||||||
|
|
||||||
|
Travel pauses during combat and boarding; the original tactical controller retains those orders. Supply transactions retain their own control. Intentionally waiting vehicles do not accrue stuck time. During commanded travel, a missing native usable-vehicle association can be restored for the healthy original seated crew. Stock `SCR_AIRemoveStuckVehicle` removes that association after failed movement; a retained crew needs it to accept driving orders again. Restoration is checked when travel starts and on bounded retries, with an explicit journal event. No five-metre progress triggers a native route retry every 45 seconds; three minutes without progress releases that vehicle from the column and logs its position. Physical vehicles, surviving crew and loot remain; no teleport or replacement spawn is used.
|
||||||
|
|
||||||
|
Reboarding tracks occupied seats and the farthest remaining soldier’s distance. Thirty seconds without either kind of progress permits a retry for only that unfinished team, at most three times. The old waypoint is cancelled and its replacement waits until a subsequent tick, allowing native reservations to clear. Seated soldiers are not moved. A five-minute hard deadline retains the finite stranded-team policy; active approaches no longer lose their squad at an unconditional two-minute cutoff.
|
||||||
|
|
||||||
|
Unchanged contacts no longer recreate tactical orders. A changed contact can refresh them after the existing thirty-second limit; dismount retries and emergency abandonment retain their separate policies.
|
||||||
|
|
||||||
|
The HQ journal now records the selected lead vehicle, spacing waits/resumes, periodic neighbour-gap and supply state, route retries, stranded vehicles and departures.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`tests/test_convoy_column.py` executes the production controller against doubles of native entities and waypoints. It covers spacing hysteresis, stable targets, physical arrival, supply-order ownership, route-preserving leader promotion, held-time exclusion, retries and loss. It also exercises the actual unchanged-contact order policy and rejects native-test success based on movement before departure.
|
||||||
|
|
||||||
|
`tests/test_supply_convoys.py` verifies that expired travel deadlines and proximity to the old 250-metre base boundary no longer create arrival, and that supply/combat holds prevent departure.
|
||||||
|
|
||||||
|
The native fixture starts three production convoys (18 vehicles, 75 soldiers), checks physical cargo and seating, observes all vehicles moving, injects one labelled contact, verifies both infantry teams dismount and reboard, waits for real arrival and the normal pause, and measures truck movement from a new position captured after the next leg starts. The contact is a test input, not proof of native enemy perception. The final fixture parks its observer away from the convoy route and gives that observer a convoy-friendly native perceived faction to prevent unrelated combat from contaminating the injected-contact sequence. The fixture explicitly resets the scenario at completion; it no longer tries to kill a protected prisoner as cleanup.
|
||||||
|
|
||||||
|
Native iteration evidence is preserved under `.local/analysis/convoy-coordination-2026-09-27/`. The second run completed convoy movement, dismount, boarding, regroup and physical arrival, but failed obsolete player-death cleanup; its runner result remains `test-failed`. Its old next-leg assertion also counted earlier regroup displacement, so the final fixture requires a fresh post-departure movement sample.
|
||||||
|
|
||||||
|
The third run exposed crossed vehicle ordering after combat and also stopped on a native `No suppression volume provided!` exception while unrelated units were reporting the observer near the airfield. Its failed result is preserved; the suppression exception is not claimed fixed by the convoy ordering change. The isolated repeat separates that engine/ambient-combat issue from the controlled convoy sequence.
|
||||||
|
|
||||||
|
The fourth run had all 18 vehicles moving and both teams dismounted, but only 13 of 14 passengers had reboarded when the old two-minute deadline released a team. This is preserved as a failed native test and motivated the progress-aware boarding policy.
|
||||||
|
|
||||||
|
The fifth run stopped in the test-only seat logger after an infantry actor lost its AI agent during dismounted movement. The diagnostic dereference is now guarded. Vehicles moving near dismounted infantry also motivated the clearance and low-speed protection; the log does not identify a definitive damage instigator.
|
||||||
|
|
||||||
|
The sixth run reboarded all 14 passengers and formed a physical travel order, then reproduced a healthy seated troop-truck driver remaining stationary despite three waypoint retries. The three-minute watchdog correctly exposed/released it instead of inventing arrival. The subsequent change adds bounded usable-vehicle restoration and native driver/group-action diagnostics; the old failure itself does not prove which native action discarded or blocked driving.
|
||||||
|
|
||||||
|
The seventh run completed the strengthened fixture: all 18 vehicles moved, both infantry teams dismounted, all 14 passengers reboarded, all three convoys departed for another base, and the original troop truck moved twenty metres measured strictly after its next-leg departure. There were no script errors, VM exceptions or stranded-vehicle releases in that run. Native boarding emitted temporary stuck/reset warnings before completing. The runner records `smoke-completed`, unchanged source/external-addon hashes, and confirmed cleanup. All repository checks passed. Generic qualification still exits nonzero for existing external localization/resource and inherited-prefab keyword errors; the raw report is preserved in `r7-completed/qualification.log`. The successful run did not exercise missing-registration restoration, so it does not establish that as the cause of the sixth run’s stall.
|
||||||
|
|
||||||
|
Dedicated multiplayer and long-session navigation across all road layouts remain separate validation requirements. Native obstacle/pathfinding failures are recoverable only within the engine's capabilities; the controller now exposes and bounds stationary failures rather than reporting them as arrival.
|
||||||
@@ -140,6 +140,36 @@
|
|||||||
"guid": "962C400354E4BD6C",
|
"guid": "962C400354E4BD6C",
|
||||||
"addon": "629B2BA37EFFD577",
|
"addon": "629B2BA37EFFD577",
|
||||||
"verification": "Installed WCS USSR AA character references this exact prefab; inspected launcher has infrared guidance and a loaded disposable muzzle, 2026-09-23."
|
"verification": "Installed WCS USSR AA character references this exact prefab; inspected launcher has infrared guidance and a loaded disposable muzzle, 2026-09-23."
|
||||||
|
},
|
||||||
|
"Prefabs/Characters/Factions/BLUFOR/RHS_USAF/RHS_USAF_MARSOC/RandomSoldiers/Character_RHS_USMC_MARSOC_Rifleman_Random.et": {
|
||||||
|
"guid": "844936B168DFAB8E",
|
||||||
|
"addon": "595F2BF2F44836FB",
|
||||||
|
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
|
||||||
|
},
|
||||||
|
"Prefabs/Characters/Factions/BLUFOR/RHS_USAF/RHS_USAF_MARSOC/RandomSoldiers/Character_RHS_USMC_MARSOC_Medic_Random.et": {
|
||||||
|
"guid": "DBB8D1F13930220E",
|
||||||
|
"addon": "595F2BF2F44836FB",
|
||||||
|
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
|
||||||
|
},
|
||||||
|
"Prefabs/Characters/Factions/OPFOR/RHS_AFRF/MSV/Flora/Character_RHS_RF_MSV_Flora_SL.et": {
|
||||||
|
"guid": "B3C496CCFA72D25E",
|
||||||
|
"addon": "595F2BF2F44836FB",
|
||||||
|
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
|
||||||
|
},
|
||||||
|
"Prefabs/Characters/Factions/OPFOR/RHS_AFRF/MSV/Flora/Character_RHS_RF_MSV_Flora_Medic.et": {
|
||||||
|
"guid": "D9AF475A4E5DC52C",
|
||||||
|
"addon": "595F2BF2F44836FB",
|
||||||
|
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
|
||||||
|
},
|
||||||
|
"Prefabs/Characters/Factions/INDFOR/RHS_ION_Coy/RandomSoldiers/Character_RHS_ION_Coy_Rifleman_Random.et": {
|
||||||
|
"guid": "140001FC383DF9A2",
|
||||||
|
"addon": "595F2BF2F44836FB",
|
||||||
|
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
|
||||||
|
},
|
||||||
|
"Prefabs/Characters/Factions/INDFOR/RHS_ION_Coy/RandomSoldiers/Character_RHS_ION_Coy_Medic_Random.et": {
|
||||||
|
"guid": "0FA8ACE19A772327",
|
||||||
|
"addon": "595F2BF2F44836FB",
|
||||||
|
"verification": "Exact path/type/GUID verified in installed owning resourceDatabase.rdb, 2026-09-26. Response roles and medic stock inspected in RHS character/group prefabs."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
# Random field hospitals and camps
|
# Random field hospitals and camps
|
||||||
|
|
||||||
Everon now has **three field hospitals and five field camps**. This replaces the old medical stash and eastern outpost and adds six locations. Their positions and headings are chosen again at round start. The prison, radio, four mortars and existing supply/extraction objectives remain separate.
|
Everon requests **three field hospitals and five field camps**. This replaces the old medical stash and eastern outpost and adds six locations. Their positions and headings are chosen again at round start. Optional sites with no safe placement are skipped for that round. The prison, radio, four mortars and existing supply/extraction objectives remain separate.
|
||||||
|
|
||||||
Each field site has four permanent defenders and a separate two-person mobile team, drawn from the selected enemy faction's group roster. Both groups are created once, registered with the force ledger, and receive the existing AI skill, equipment, intel and reporting systems. HQ reserves the defenders against hunting, support, courier and logistics assignments. The mobile team is eligible for delivered HQ hunting orders and otherwise defends its home assembly area. Its standing home waypoint remains available after a temporary HQ assignment ends. Losing mobile soldiers does not turn the permanent guards into replacements. Guards still fight locally and can be killed; the existing objective-defence system permits one finite relief squad after a garrison is lost.
|
Each field site has four permanent defenders and a separate two-person mobile team, drawn from the selected enemy faction's group roster. Both groups are created once, registered with the force ledger, and receive the existing AI skill, equipment, intel and reporting systems. HQ reserves the defenders against hunting, support, courier and logistics assignments. The mobile team is eligible for delivered HQ hunting orders and otherwise defends its home assembly area. Its standing home waypoint remains available after a temporary HQ assignment ends. Losing mobile soldiers does not turn the permanent guards into replacements. Guards still fight locally and can be killed; the existing objective-defence system permits one finite relief squad after a garrison is lost.
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ The original medical clue route remains marked after prison escape. The other fi
|
|||||||
|
|
||||||
Edit `addons/core/Configs/ERR/EveronSites.conf`. `GuardCount` selects the permanent roster size (1–8, or zero for the full faction group). `ResponseCount` selects a separate mobile roster (0–8). `GuardOffset` and `ResponseOffset` are rotated with the composition. Do not put a roaming `Patrol` on a permanent garrison; use its separate response team instead. The selected profile must provide usable infantry members.
|
Edit `addons/core/Configs/ERR/EveronSites.conf`. `GuardCount` selects the permanent roster size (1–8, or zero for the full faction group). `ResponseCount` selects a separate mobile roster (0–8). `GuardOffset` and `ResponseOffset` are rotated with the composition. Do not put a roaming `Patrol` on a permanent garrison; use its separate response team instead. The selected profile must provide usable infantry members.
|
||||||
|
|
||||||
`RandomClear` supports hospitals, outposts and depots as well as the existing generated objectives. Each new field site is at least 600 metres from other committed locations; unresolved fallback seeds do not reserve ground. The survey checks water, terrain variation, physical obstacles, the composition footprint, and both squad assembly areas. Each site has a bounded 12,000-candidate/30-second search and a validated authored fallback. Invalid terrain is never accepted just to satisfy a requested count; preparation reports the site's ID if placement fails. Custom scenery still needs to fit the surveyed footprint (ten metres either side of its centre).
|
`RandomClear` supports hospitals, outposts and depots as well as the existing generated objectives. Each new field site is at least 600 metres from other committed locations; unresolved fallback seeds do not reserve ground. The survey checks water, terrain variation, physical obstacles, the composition footprint, and both squad assembly areas. Each site has a bounded 12,000-candidate/30-second search and a validated authored fallback. Invalid terrain is never accepted just to satisfy a requested count. If both search and fallback fail, a dispensable site is removed before spawning and logged under `[site skipped]`; preparation continues. A site providing indispensable accessible radio intel cannot be skipped. Custom scenery still needs to fit the surveyed footprint (ten metres either side of its centre).
|
||||||
|
|
||||||
Scenario bounds are 32 sites, 24 initial groups including mobile teams, 512 stock items and 128 scenery props. The default field sites total 32 fixed defenders and 16 mobile soldiers, in addition to other mission forces. These finite limits are not performance measurements.
|
Scenario bounds are 32 sites, 24 initial groups including mobile teams, 512 stock items and 128 scenery props. The default field sites total 32 fixed defenders and 16 mobile soldiers, in addition to other mission forces. These finite limits are not performance measurements.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Corrections from the 26 September RKS 2 game
|
||||||
|
|
||||||
|
The source review and downloaded evidence are in `.local/analysis/rks2-game-2026-09-26/analysis.md`. These changes do not publish the Workshop mod or restart RKS 2.
|
||||||
|
|
||||||
|
- Helicopters defer crew allocation until native airframe simulation, fuel and accessible compartments have initialized. They allocate the original crew once, assign the selected faction and group before native boarding, and only then start passenger loading. This ordering matters for ION: the native convenience spawn function attempted to board a Soviet pilot before changing its faction. A failed allocation terminates that attempt with the seat name; it cannot create replacement crew on subsequent ticks. The same path serves search and ammunition helicopters.
|
||||||
|
- Convoy road selection applies spacing exclusions before choosing its best candidate, so a nearby occupied deployment area cannot hide other valid formations. Failed bounded placement still retains existing convoys and reports the actual deployed/requested counts; it never forces an unsafe spawn or replaces a lost convoy.
|
||||||
|
- Mounted convoy escorts move to cover and retain native vehicle combat. They no longer receive infantry Defend waypoints, eliminating that convoy path into the missing-Defend-waypoint exception during regroup.
|
||||||
|
- Mission-owned hunter and mortar boarding tasks only receive completion callbacks while still present in their actor's native utility and while `m_AIInfo` exists. Teardown/dead-actor references cannot invoke the native null-pointer completion callback.
|
||||||
|
- Hunters preserve their existing route for fresh reports within 75 m of the area actually delivered to them. A materially changed contact still causes an HQ radio order. Delivered road destinations within 50 m of the current waypoint also preserve navigation. Pending or failed radio packets never advance the delivered-area anchor. The original finite vehicle-radio fix remains required.
|
||||||
|
- Stale native perception entries no longer suspend stuck-vehicle recovery. The same recent-contact predicate controls both combat priority and the movement watchdog. Actual fresh combat retains priority.
|
||||||
|
- Mortar HQ selection predicts the reported impact area and checks settlement/friendly exclusions before issuing an order. Rejected native aim/fire attempts receive a 60-second cooldown. Exact ballistic and per-shot authorization remain in place, including the eight-round salvo and finite ammunition; these changes do not allow firing through friendly forces or settlements.
|
||||||
|
- While prisoners are unconscious, medical diagnostics log blood, resilience, bleeding, observed improvement, treatment and natural recovery at most every ten seconds. Defeat additionally records the current per-prisoner snapshot at the decision. Medical damage/recovery rules are unchanged.
|
||||||
|
|
||||||
|
Regression tests execute route retention, stale-contact retry, safe callback cleanup and mortar preflight/cooldown behavior. The complete repository check suite passed. Native compilation passed with the installed RHS/WCS dependencies.
|
||||||
|
|
||||||
|
Native Workbench evidence is archived under `.local/analysis/gameplay-fixes-2026-09-27/`:
|
||||||
|
|
||||||
|
- The vanilla-enemy fixture completed launch, native player sighting and radio delivery, return/landing, destruction delay, replacement and faction takeover before the final faction-before-boarding correction.
|
||||||
|
- The RHS ION fixture first reproduced the pilot boarding failure. With the correction, all four original flight crew and eight passengers boarded; the helicopter took off and flew its search route. A second ammunition helicopter also seated its crew.
|
||||||
|
- The final RHS ION fixture did **not** complete: at 18:24:51 it reported `pilot or airframe lost`. The engine also reported an extreme particle-parent velocity and coordinates during the helicopter explosion. That run did not identify the cause. A subsequent close-contact reproduction isolated repeated outward acceleration in `StandOff`; the correction and successful extended RHS flight/return regression are documented in [helicopter-standoff.md](helicopter-standoff.md). The earlier failed run is retained as evidence.
|
||||||
|
- The runner could not certify cleanup while an exiting `enfMain` process had empty arguments. A subsequent independent `assert_prefix_idle` check passed; the disposable prefix is idle.
|
||||||
|
|
||||||
|
Long-session UAZ navigation, convoy combat/placement and mortar eligibility still need a dedicated multiplayer playthrough. Unit/behavior tests and Workbench compilation do not certify those live behaviors. These edits remain local and have not been published or applied to RKS 2.
|
||||||
+30
-6
@@ -1,13 +1,15 @@
|
|||||||
# Generated starts and radio camps
|
# Generated starts and radio camps
|
||||||
|
|
||||||
Everon now chooses a new prison, radio camp and four mortar sites at preparation time. Other caches,
|
Everon chooses a new prison, radio camp, four mortar sites and optional field
|
||||||
outposts retain their authored positions. The extraction helicopter uses the validated
|
hospitals/camps at preparation time. The supply cache retains its authored position.
|
||||||
|
The extraction helicopter uses the validated
|
||||||
Everon airfield pad (4800.953 / 11487.412), with no off-airfield fallback. The server
|
Everon airfield pad (4800.953 / 11487.412), with no off-airfield fallback. The server
|
||||||
resolves the prison first, then the radio, before creating any site entities.
|
resolves the prison first, then the radio, before creating any site entities.
|
||||||
Joining clients receive the real replicated locations; intel keeps the same
|
Joining clients receive the real replicated locations; intel keeps the same
|
||||||
stable destination IDs.
|
stable destination IDs.
|
||||||
|
|
||||||
Site authors can use `RandomClear 1` on a prison, radio or mortar definition. `Heading`
|
Site authors can use `RandomClear 1` on a prison, radio, mortar, hospital, outpost
|
||||||
|
or depot definition. `Heading`
|
||||||
sets the authored fallback heading; `RootHeight` offsets a composition above its
|
sets the authored fallback heading; `RootHeight` offsets a composition above its
|
||||||
surveyed ground (the radio/table composition uses 0.8 m). Runtime placement never
|
surveyed ground (the radio/table composition uses 0.8 m). Runtime placement never
|
||||||
rewrites the configuration file. Existing definitions default to authored sites.
|
rewrites the configuration file. Existing definitions default to authored sites.
|
||||||
@@ -17,11 +19,17 @@ terrain varying by more than 0.6 m, nearby roads (12 m), and unsafe guard footin
|
|||||||
Prisons avoid physical runway/control-tower anchors by 1,500 m. The radio stays
|
Prisons avoid physical runway/control-tower anchors by 1,500 m. The radio stays
|
||||||
at least 1 km from the prison and 300 m from other sites. The extraction pickup
|
at least 1 km from the prison and 300 m from other sites. The extraction pickup
|
||||||
must remain inside the prison-relative escape boundary with 100 m clearance.
|
must remain inside the prison-relative escape boundary with 100 m clearance.
|
||||||
Sampling yields after an 8 ms work budget, with a 30-second overall deadline and
|
Sampling yields after an 8 ms work budget, with a 30-second deadline and
|
||||||
12,000 candidate limit per site. The budget is checked between native terrain
|
12,000 candidate limit per site. The budget is checked between native terrain
|
||||||
queries, so a single engine query can exceed it. Authored fallback positions
|
queries, so a single engine query can exceed it. Authored fallback positions
|
||||||
must pass the same checks; otherwise preparation reports failure instead of
|
must pass the same checks. If both search and fallback fail, an optional outpost,
|
||||||
spawning an incomplete camp or waiting forever.
|
hospital or depot is omitted for that round and preparation continues. The
|
||||||
|
`[site skipped]` log identifies its label, ID and attempt count. Removal happens
|
||||||
|
before spawning, map registration or AI intel assignment, leaving no empty site.
|
||||||
|
Every following site gets a fresh search budget. Required prison/radio/extraction
|
||||||
|
and mortar sites remain mandatory. An otherwise optional site also remains
|
||||||
|
mandatory if removing it would invalidate the scenario, for example by losing
|
||||||
|
the minimum accessible radio-intel sources. No unsafe placement is accepted.
|
||||||
|
|
||||||
The existing prison composition, gate, backpack and guards move together.
|
The existing prison composition, gate, backpack and guards move together.
|
||||||
Player slots and escape-boundary tests use the same rotation. Admission still
|
Player slots and escape-boundary tests use the same rotation. Admission still
|
||||||
@@ -63,6 +71,22 @@ definitions. Other Workbench smoke outcomes explicitly load
|
|||||||
fixtures, including the retired western cache and old extraction position.
|
fixtures, including the retired western cache and old extraction position.
|
||||||
Ordinary Workbench play and dedicated servers use the live definitions.
|
Ordinary Workbench play and dedicated servers use the live definitions.
|
||||||
|
|
||||||
|
`--project core --smoke optional-site-skip` uses current randomized definitions,
|
||||||
|
forces camp 5 to fail both random and authored terrain checks, and verifies that
|
||||||
|
the camp has no runtime, required objectives/mortars remain and a player is
|
||||||
|
admitted. The failure injection exists only in the opt-in Workbench fixture.
|
||||||
|
|
||||||
|
On 2026-09-26, this fixture compiled and completed after camp 5 exhausted 12,000
|
||||||
|
candidates and failed its fallback. The camp was omitted and player admission
|
||||||
|
succeeded with prison, radio, extraction and all four mortars retained. The full
|
||||||
|
repository check passed. The runner reported a terminating-process classification
|
||||||
|
race at shutdown; independent follow-up confirmed the private prefix idle and
|
||||||
|
the addon source manifest unchanged. External inputs were unchanged. Existing
|
||||||
|
loaded asset errors remain; the completed scenario had no script/runtime failures. Evidence,
|
||||||
|
including the earlier test-selector compile failure, is retained under
|
||||||
|
`.local/analysis/optional-site-skip-2026-09-26/`. This validates local startup,
|
||||||
|
not a published or dedicated-server update.
|
||||||
|
|
||||||
On 2026-09-22, the full `python3 tools/check.py` suite passed. The final native
|
On 2026-09-22, the full `python3 tools/check.py` suite passed. The final native
|
||||||
`generated-sites` fixture passed 39 assertions at a randomized Everon start:
|
`generated-sites` fixture passed 39 assertions at a randomized Everon start:
|
||||||
prison at approximately (5100, 9137), heading 89 degrees; radio at (5991, 5659).
|
prison at approximately (5100, 9137), heading 89 degrees; radio at (5991, 5659).
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Helicopter close-contact speed correction — 27 September 2026
|
||||||
|
|
||||||
|
`ERR_ReconFlightRules.StandOff` receives the persistent, smoothed flight velocity. Inside 180 metres of an observed player, it previously added the full outward correction on every simulation frame. Even a helicopter already moving away at the requested speed received another impulse. This made acceleration depend on frame rate and could send the native airframe physics into extreme velocities.
|
||||||
|
|
||||||
|
The correction restores the original mission policy: calculate the existing radial speed, calculate the required minimum outward speed, and add only the positive difference. Tangential motion and already sufficient outward motion are preserved. The 180-metre radius and 25 m/s maximum outward correction target are unchanged. No health, damage, player takeover, replacement timing or airframe position is overridden by this fix.
|
||||||
|
|
||||||
|
## Reproduction and validation
|
||||||
|
|
||||||
|
- A regression executing the production function fails before the correction: 600 repeated evaluations keep accelerating an already safe outward velocity. The corrected function passes repeated-call and 30/60/120 Hz cases, alongside the existing orbit, return and loss-timer tests.
|
||||||
|
- Ordinary native RHS ION flight telemetry stayed below 40 m/s, with angular velocity below 0.056 rad/s; that diagnostic was deliberately stopped after entering landing. It was not a completed qualification run.
|
||||||
|
- The extended native Workbench fixture first requires real crew perception and radio delivery to HQ. It then relocates only the test player 100 metres ahead of the aircraft to exercise a sudden close sighting. Aircraft movement, perception, radio, crew and damage remain production behavior.
|
||||||
|
- With the unfixed controller, that encounter failed at 19:19:46 with commanded speed **573.703 m/s** and measured speed **45,378.7 m/s**. This reproduced the flight instability without changing flight physics in the test.
|
||||||
|
- With the correction, the **same native close-contact fixture completed** at 19:27:49: actual close sighting, 60 seconds of bounded flight with the original living pilot, return/landing with the same intact aircraft and crew, retained parked aircraft, intentional destruction/replacement delay, replacement and faction takeover. Intact-flight telemetry peaked at 41.094 m/s.
|
||||||
|
- All repository checks passed. Native compilation and scripted flight assertions passed, and the runner confirmed cleanup and unchanged source/external inputs. The final generic native-error report returned nonzero for existing RHS/WCS localization and prefab keyword errors also present in the unfixed run; there were no new engine-error signatures. This is a successful flight regression, not a claim of an error-free dependency installation.
|
||||||
|
|
||||||
|
Evidence is retained in `.local/analysis/helicopter-standoff-2026-09-27/`. The native fixture is Workbench-only; these results do not substitute for a dedicated multiplayer playthrough. Nothing has been published or restarted on RKS 2.
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Infantry contact and search orders
|
||||||
|
|
||||||
|
HQ-controlled enemy infantry now suspend their current HQ mobile search when
|
||||||
|
native group perception has a detected/identified target updated within 15
|
||||||
|
seconds. This is evaluated on the existing one-second server tick, before radio
|
||||||
|
downlink delivery. Local reactions do not wait for a report to reach HQ.
|
||||||
|
At the transition, the group and its combat-capable agents are activated without
|
||||||
|
changing their permanent simulation LOD; casualties are not awakened.
|
||||||
|
|
||||||
|
The mission removes only its own search waypoint from the native queue, retaining
|
||||||
|
the assignment and waypoint identity. Native waypoint deselection cancels that
|
||||||
|
search activity's child investigation orders. The searching squad is set to fire
|
||||||
|
at will, and pending infantry task orders are discarded. Status/check-in and
|
||||||
|
vehicle transports remain independent. Individual native combat, visibility,
|
||||||
|
cover, suppression, ammunition and aiming still determine whether soldiers fire.
|
||||||
|
|
||||||
|
After qualifying contacts stop, a 15-second grace period prevents immediate
|
||||||
|
retasking. The same search is reinserted once only if its assignment is still
|
||||||
|
current, unexpired and eligible. Released/replaced/expired orders cannot return.
|
||||||
|
Old, destroyed or disarmed target records no longer block HQ indefinitely.
|
||||||
|
|
||||||
|
Defensive site and road-interception waypoints stay in place. Baseline home patrol
|
||||||
|
routes and other owners' waypoints are not removed. Permanent objective guards,
|
||||||
|
specialists, logistics reservations, vehicle crews and player-controlled groups
|
||||||
|
retain their existing ownership/eligibility restrictions. No extra AI are spawned.
|
||||||
|
|
||||||
|
Journal events under `INFANTRY COMBAT` identify the squad and spawn source, contact,
|
||||||
|
search pause, search resumption or completion without a surviving search. This is
|
||||||
|
an order-conflict safeguard, not proof that every reported hesitation was caused
|
||||||
|
by HQ. Native attacks already have a higher default priority than investigation;
|
||||||
|
visibility, suppression, weapon state and other native behavior need a live
|
||||||
|
playtest if soldiers still fail to fire.
|
||||||
|
|
||||||
|
`test_infantry_combat.py` executes the actual state machine with native waypoint
|
||||||
|
adapters, including renewal, quiet delay, expiry, replacement, defensive orders,
|
||||||
|
and ownership changes. The focused `infantry-combat` Workbench fixture exercises
|
||||||
|
the actual mobile outpost responders' search and home-defence queue with an
|
||||||
|
explicitly injected contact. It does not certify natural target acquisition or
|
||||||
|
multiplayer shooting behavior.
|
||||||
|
|
||||||
|
The older `patrol-hq` scenario's traversal stage runs before unlocking the prison;
|
||||||
|
its stationary enemies are consistent with the intentional prison AI hold. Its
|
||||||
|
old search helper also selected permanent objective defenders, which correctly
|
||||||
|
reject mobile HQ assignments. The shared queue probe now selects the separate
|
||||||
|
mobile responder roster. The focused scenario leaves the prison hold in place
|
||||||
|
and tests queue ownership only; it does not claim physical patrol traversal.
|
||||||
|
|
||||||
|
Validation on 25 September 2026: the full repository check passed. Targeted checks
|
||||||
|
after the wake-up and fixture updates also passed. Native run 5 compiled and
|
||||||
|
completed 15 assertions for the actual mobile squad's search pause/resume and
|
||||||
|
home-defence queue. Source/external inputs were unchanged and cleanup confirmed.
|
||||||
|
Loaded localization/world/controller asset errors still make the strict runner
|
||||||
|
exit nonzero; no script/runtime failure was reported in that completed run.
|
||||||
|
Logs, source manifests and earlier failed fixture setups are retained in
|
||||||
|
`.local/analysis/infantry-combat-2026-09-25/`.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Rapid mortar barrage — 27 September 2026
|
||||||
|
|
||||||
|
An authorized HQ fire mission retains eight HE rounds distributed over an 80-metre radius. Mission-owned AI mortar crews use a dedicated behavior tree with the two fixed 1.5-second firing-loop delays removed. The native loading animation, physical projectile event, ammunition transfer, per-round ballistic solve and spread still determine the real firing rate. The 0.75-second settling pause after leaving the aiming seat is retained.
|
||||||
|
|
||||||
|
The tree is derived from the installed Arma Reforger 1.8.0.13 `AI/BehaviorTrees/Chimera/Soldier/StaticArtilleryBehavior.bt`. Only its resource identity and the two delay values differ. It is selected only for a mortar registered with the mission mortar service; unrelated artillery keeps its stock tree. Player firing actions are unchanged.
|
||||||
|
|
||||||
|
The existing shooting-cycle gate waits for an actual projectile before advancing the native shot counter. Removing a fixed delay does not replace that event, create ammunition, skip the loading animation, or manufacture explosions. The final transfer still checks the delivered report, current ballistic aim, friendly exclusions, mission state and eight-round limit. Orders retain their finite ammunition, report freshness, cooldown and helicopter airspace coordination.
|
||||||
|
|
||||||
|
The old salvo fixture now unlocks the prison through the normal mission API before waiting for crew readiness. Previously it stayed in the prison phase while HQ was paused. The fixture moves its player to the distant airfield after unlocking, keeping the participant away from the crew while testing the synthetic range target. It also stops automatic UAZ responders before unlocking: the first live rapid-fire trial correctly withheld fire when those vehicles pursued its synthetic report into the impact area. This arrangement is restricted to the opt-in Workbench fixture; production friendly-fire checks are unchanged.
|
||||||
|
|
||||||
|
The unchanged single-report baseline fired once and then correctly expired its target report. The first unobstructed rapid-fire run fired seven actual rounds, then correctly withheld the eighth because the older refresh fixture stopped generating sightings halfway through a salvo. That fixture now continues observations until six rounds have fired, leaving two rounds for the final downlink to arrive; its existing 90-second update bound remains. Freshness and the production order deadline are not relaxed. The fixture also waits within its existing deadline when normal crew/status traffic occupies the radio: a zero queue result creates no observation and no longer fails the test immediately.
|
||||||
|
|
||||||
|
A further run refused a radio update because the original native aim owner was no longer available, amid nearby combat involving the released test player. The test player is now placed away from the battery; the production rejection remains intact.
|
||||||
|
|
||||||
|
The unobstructed eight-round run exposed a completion race after the fixed waits were removed: the native eighth-shot callback completed the activity, but the zero-delay repeater began a ninth aim before HQ observed the completion event. Its normal eight-round guard then marked the already-completed barrage rejected. The shooting-cycle node now returns RUNNING after superclass processing of the last confirmed shot, waiting for normal HQ cleanup. Repeated ticks cannot increment the native counter again. An executable regression covers eight physical events, pending supply, exactly one completion, no ninth aiming request and unchanged unrelated artillery behavior.
|
||||||
|
|
||||||
|
Final native run passed all 57 assertions with eight distinct physical projectiles and eight matching commits. The original stock decreased from 27 to 19 shells; the native activity completed naturally and the 25-second replay watch observed no extra projectile or replenishment. Launch intervals measured from mission journal time were 5.1, 4.9, 4.9, 5.9, 6.0, 6.0, 6.1 seconds (5.557 seconds on average; 38.9 seconds from first to eighth launch). This is one isolated firing-range result, not a guaranteed cadence under combat or server load. Initial order delivery and barrel setup occur before the first shot.
|
||||||
|
|
||||||
|
The runner recorded `smoke-completed`, source/dependency hashes unchanged, and cleanup confirmed. The generic report still returns nonzero for the same 13 installed dependency/prefab errors seen in the stock baseline; it reports no script/runtime failures or new engine errors. All repository checks passed, including the executable last-shot regression. Removing just the completion wait makes that regression fail.
|
||||||
|
|
||||||
|
These results qualify local native Workbench launch behavior and ammunition accounting. Multiplayer replication, actual impact positions and damage are not measured by this fixture. Evidence: `.local/analysis/mortar-cadence-2026-09-27/`.
|
||||||
+48
-34
@@ -1,40 +1,54 @@
|
|||||||
# Revivable players and all-unconscious mission failure
|
# Player injuries and team failure
|
||||||
|
|
||||||
Admitted player bodies receive server-owned casualty protection. Ordinary damage
|
Admitted players keep the same living, treatable body. Otherwise lethal damage
|
||||||
still causes wounds and bleeding. Damage to physical, overall-health and blood
|
is capped just above hit-zone destruction and causes unconsciousness. Continued
|
||||||
hitzones is limited above their configured destruction threshold; an otherwise
|
bleeding still drains blood down to the protected minimum; protection never adds
|
||||||
lethal hit knocks the prisoner unconscious. Resilience can still reach zero, and
|
health or blood. AI retain native lethal damage and recovery rules.
|
||||||
healing damage is passed through unchanged. Further hits and bleeding cannot
|
|
||||||
finish the casualty. AI retain their normal damage rules. Protection stays with
|
|
||||||
the original body across reconnects and does not grant a new life or respawn.
|
|
||||||
|
|
||||||
The round fails after **30 uninterrupted seconds** with every remaining prisoner
|
Passive blood, wound and resilience recovery use one quarter of their native
|
||||||
unconscious, no conscious reserved prisoner available to reconnect, and no
|
rates. Bleeding prevents passive blood and consciousness recovery. Bandaging
|
||||||
ongoing treatment or observed blood/resilience recovery. Waking up, a treatment
|
stops the bleeding and lets blood improve gradually. With sufficient blood,
|
||||||
in progress, or increasing blood/resilience resets that timer. A passive recovery
|
resilience recovers slowly and the player can eventually wake without an injector.
|
||||||
effect that is present but makes no progress does not prevent failure. An empty
|
This is not an instant or periodic full heal. Saline restores blood through its
|
||||||
server does not start this timer. Mission status explains whether recovery is
|
finite native infusion; epinephrine improves resilience over 30 seconds without
|
||||||
continuing or the team is in the failure grace period.
|
replacing blood or treating wounds. Medicine is not slowed by the passive-rate
|
||||||
|
setting. Duplicate epinephrine dosing is refused while its effect is active.
|
||||||
|
|
||||||
Treatment uses the existing medical system. Bandaging and saline remain relevant;
|
The round fails with **“Everybody died.”** after 30 uninterrupted seconds when
|
||||||
ACE Medical epinephrine heals resilience and is not blocked by casualty protection.
|
all remaining prisoners are unconscious, no conscious reserved prisoner can
|
||||||
ACE's own treatment prerequisites still apply (including stopping bleeding and
|
reconnect to help, and nobody has treatment, observed recovery, or viable native
|
||||||
restoring sufficient health). Installing this mission does not itself install
|
passive recovery. A bandaged casualty with a blood/resilience regeneration effect
|
||||||
ACE Medical or add an unlimited epinephrine action.
|
also counts during the engine's initial regeneration delay. Full hit zones,
|
||||||
|
zero-rate effects, insufficient blood for resilience, and bleeding do not falsely
|
||||||
|
count as self-recovery. Waking, viable recovery, or treatment resets the timer.
|
||||||
|
An empty server does not start it. Existing all-dead/missing-body and exhausted
|
||||||
|
admission handling remains active.
|
||||||
|
|
||||||
`/esc mission abandon` remains an administrator-only way to end a round explicitly.
|
The `recovery` fixture exercises the real team-failure timer and treatment on an
|
||||||
The all-dead/missing-body fallback and exhausted-admission handling also remain,
|
admitted prisoner. `medical-combat` checks an actual hunter medic's inventory,
|
||||||
for deleted bodies and administrative/native paths outside ordinary damage.
|
bleeding, consumed bandages, slow blood recovery without saline, natural waking,
|
||||||
|
finite saline/epinephrine and protected fatal blood loss. Fixtures inject initial
|
||||||
|
injuries and enter at native item-effect completion; they do not certify manual
|
||||||
|
interactions or remote multiplayer play. ACE medical overrides require separate
|
||||||
|
compatibility testing. The 25 September lethal-death result is historical and
|
||||||
|
has been superseded by the restored revivable-player policy.
|
||||||
|
|
||||||
The isolated `recovery` Workbench fixture exercises damage exceeding full health
|
## Local validation, 26 September 2026
|
||||||
on health, blood and head hitzones, continued survival while treatment is flagged
|
|
||||||
active, restoration by native resilience healing, and the actual all-unconscious
|
|
||||||
failure timer. This is separate from remote-client/manual treatment acceptance.
|
|
||||||
|
|
||||||
Validation on 2026-09-23: all repository checks passed after updating the existing
|
Repository checks passed. The updated `medical-combat` engine fixture completed
|
||||||
ServerTick test adapters. The isolated `recovery` run at
|
32 assertions, including the actual SF UAZ medic's backpack and six injectors,
|
||||||
`/tmp/erri-revivable-native2/logs/logs_2026-09-23_20-02-04` completed 66 assertions
|
native bleeding, bandaging, blood recovery without saline, natural waking after
|
||||||
with no script/runtime failures, unchanged source and verified cleanup. Existing
|
blood replacement, finite epinephrine, and a living unconscious body after fatal
|
||||||
RHS/WCS resource and inherited prefab-keyword errors remain in that engine log.
|
blood damage. No script compilation or medical assertion failed in the final run.
|
||||||
The recovery effect was a native timed resilience-healing effect; an actual ACE
|
The strict runner reported a transient process-cleanup classification failure;
|
||||||
injector action and remote multiplayer treatment were not exercised by this run.
|
a subsequent independent check confirmed the private prefix idle and unchanged
|
||||||
|
source hashes. Original reports and the unsuccessful diagnostic run are retained
|
||||||
|
under `.local/analysis/revivable-medical-2026-09-26/`. Loaded external/base assets
|
||||||
|
still emit their existing warnings; this does not certify remote multiplayer.
|
||||||
|
|
||||||
|
The subsequent `recovery` engine run verified natural recovery and active
|
||||||
|
medical treatment each keeping the full mission alive beyond 30 seconds,
|
||||||
|
waking the same admitted prisoner, resetting the failure timer, and automatic
|
||||||
|
team defeat after 30 seconds of unrecoverable unconsciousness. Source remained
|
||||||
|
unchanged and cleanup was confirmed. The strict asset-error scan still reports
|
||||||
|
existing inherited resource errors; all recovery assertions passed.
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
# Response-team medicine and combat priority
|
||||||
|
|
||||||
|
RKS 2 session `1790341620-1` (25 September 2026), journal part 1, confirms the old
|
||||||
|
casualty floor repeatedly prevented lethal injury. Events 23102–23118 also show
|
||||||
|
UAZ 6 switching to a routine road patrol while its combat flag was active; events
|
||||||
|
23162–23371 show subsequent travel with that flag still set. These establish an
|
||||||
|
order conflict, not the exact reason for every individual failure to fire.
|
||||||
|
The read-only SSH excerpt is retained in
|
||||||
|
`.local/analysis/medical-combat-2026-09-25/rks2-journal-excerpt.log`.
|
||||||
|
|
||||||
|
Hunter UAZs now fill accessible driver, turret and cargo seats (bounded at 12),
|
||||||
|
with exactly one cargo medic. US uses Green Berets and USSR uses Spetsnaz native
|
||||||
|
presets. Modded factions first use optional `ResponseSoldier` and `ResponseMedic`
|
||||||
|
resources in `Configs/ERR/Content/Registry.conf`, then their enabled character
|
||||||
|
catalog. The registry supplies RHS ION, AFRF and MARSOC profiles because installed
|
||||||
|
RHS character catalogs can be empty despite the characters being available.
|
||||||
|
Catalog fallback prefers SF/recon presets and a medic; the ordinary soldier
|
||||||
|
fallback comes from the selected faction's group. No wrong-faction medic is
|
||||||
|
substituted. Missing crew resources defer deployment for 30 seconds before any
|
||||||
|
vehicle/body is created or allowance consumed. Genuine partial physical spawn
|
||||||
|
failures still consume their allowance to prevent duplicate forces. Native medic presets retain their authored medical bag, saline, bandages,
|
||||||
|
tourniquets and morphine. Admission waits for every original occupant to be
|
||||||
|
seated. The crew's native combat mode is explicitly fire at will.
|
||||||
|
|
||||||
|
Each physical soldier receives a finite startup allowance: two epinephrine and
|
||||||
|
at least two morphine for SF/hunter troops; six of each for medics. Twenty-five
|
||||||
|
percent of ordinary hostile soldiers receive one of each. Existing stock counts
|
||||||
|
toward the allowance. Medic additions target the existing backpack when present, with overflow into free
|
||||||
|
personal pouches only after the bag refuses insertion. Medics do not receive the
|
||||||
|
optional mine/anti-air grants that could consume their treatment storage.
|
||||||
|
Insertion failures and completion are logged; completed, pending or looted grants
|
||||||
|
are never periodically replenished. This does not create AI healers for every
|
||||||
|
ordinary squad; native medic roles are guaranteed specifically in hunter UAZs.
|
||||||
|
|
||||||
|
A native detected/identified contact no older than 15 seconds suspends that
|
||||||
|
hunter's travel waypoint and pending travel radio order. Driver recovery cancels
|
||||||
|
its own high-priority boarding action while combat is active. Existing native
|
||||||
|
combat behavior remains free to fight/dismount. After contacts stop qualifying,
|
||||||
|
a further 15 quiet seconds permits HQ travel and driver recovery again. Lost,
|
||||||
|
destroyed, disarmed and old remembered targets cannot hold the patrol forever.
|
||||||
|
The existing permanent objective-defender exclusions remain unchanged.
|
||||||
|
|
||||||
|
Full crews increase the number of physical AI per hunter vehicle. The existing
|
||||||
|
three initial UAZs and maximum fifteen vehicle allowance remain unchanged.
|
||||||
|
|
||||||
|
Historical validation on 25 September 2026 (the lethal-player medical policy
|
||||||
|
has since been superseded; see [recovery.md](recovery.md)): the `medical-combat` Workbench run completed
|
||||||
|
31 assertions, including the actual three-seat native UAZ crew and medic stock,
|
||||||
|
bleeding/bandaging, timed saline, finite epinephrine and lethal blood loss.
|
||||||
|
Source stayed unchanged during the run and private-prefix cleanup was confirmed.
|
||||||
|
The strict runner still returned failure for loaded asset errors (localization
|
||||||
|
references and inherited world/controller properties); there were no script or
|
||||||
|
runtime failures. Repository medical/equipment/hunter checks passed. This is
|
||||||
|
local scripted integration evidence, not manual or multiplayer combat acceptance.
|
||||||
|
Logs, manifests and failed earlier attempts are retained under
|
||||||
|
`.local/analysis/medical-combat-2026-09-25/`.
|
||||||
|
|
||||||
|
## RKS 2 vehicle deployment failure, 26 September 2026
|
||||||
|
|
||||||
|
The 14:08 UTC server log contained two rounds. In both, all three opening
|
||||||
|
UAZ attempts failed with “selected faction has no usable soldier/medic profile”;
|
||||||
|
the second round also lost the fourth allowance the same way. Enemy faction was
|
||||||
|
RHS ION. The same journal contains delivered sightings and infantry search orders,
|
||||||
|
so HQ was operating; no hunter crew reached deployment. The installed ION
|
||||||
|
character catalog is empty, although RHS supplies stocked ION medic prefabs.
|
||||||
|
The downloaded journal is retained in `.local/analysis/rks2-hunters-2026-09-26/`.
|
||||||
|
|
||||||
|
Response profile overrides resolve this without changing faction identity.
|
||||||
|
The `hunter-faction` Workbench fixture explicitly selects ION, unlocks the actual
|
||||||
|
prison gate, checks the seated crew/medic stock, supplies a sighting at HQ's
|
||||||
|
already-delivered intake boundary, and requires a radio-delivered hunter order
|
||||||
|
and more than 30 metres of native vehicle movement. It does not certify a live
|
||||||
|
multiplayer attack, visual detection, or combat accuracy percentages.
|
||||||
|
|
||||||
|
The ION native regression exposed a second issue: the crew's modded loadouts
|
||||||
|
refused the required personal manpack. Each hunter UAZ now receives exactly one
|
||||||
|
physical R-107M in vehicle cargo before admission. Its original seated crew can
|
||||||
|
use that manpack as their HQ link; looting, capture, vehicle destruction,
|
||||||
|
dismounting or mission teardown removes the vehicle link. No medic backpack is
|
||||||
|
replaced and no lost radio is replenished. Personal radios retain their existing
|
||||||
|
rules. Radio-fitting and HQ routing both recognize the physical vehicle radio.
|
||||||
|
|
||||||
|
Final native validation: all three ION crews admitted with their vehicle radio;
|
||||||
|
the medic had at least four saline/bandages, six morphine and six epinephrine.
|
||||||
|
All three UAZs received a contact order through the production radio route;
|
||||||
|
the fixture confirmed native driving beyond 30 metres. Source hashes were
|
||||||
|
unchanged during the run and cleanup was confirmed. All fixture assertions
|
||||||
|
passed; the strict runner's asset scan still reports existing inherited asset
|
||||||
|
errors. Earlier failed/stalled runs are preserved beside the successful run.
|
||||||
|
|
||||||
|
The final complete repository check suite and the targeted vehicle-radio/aim
|
||||||
|
policy regressions also passed after the radio changes. No server restart,
|
||||||
|
Workshop publication or Git push was performed.
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
# Spectator reason and controls
|
# Spectator controls and recovery
|
||||||
|
|
||||||
The local observer hint now explains the replicated reason for spectating: the player has used their round's life, joined after extraction began, or could not be spawned. It tells a spectator with a selected survivor about the existing `/esc spectate next` and `/esc spectate previous` commands. Those commands already use the ordinary nonadministrator path; the change does not grant administration, change spawn/identity policy or alter camera control.
|
While spectating, **Left / Right arrow** selects the previous/next eligible teammate. **Enter** (including numpad Enter) toggles between the orbit camera and a player view at the teammate's head, looking along their replicated head-aim direction. Orbit retains mouse-look and zoom. Player view follows posture and vehicle seating; it does not possess the teammate, reproduce their private HUD/ADS camera, or move the unconscious body. With no available/streamed target the camera falls back to the observer anchor. `/esc spectate next|previous` remains available without administrator permission.
|
||||||
|
|
||||||
When the server has selected no survivor, the hint says that nobody is currently available to follow and that the view will follow a survivor when one becomes available. It omits ineffective cycling advice in that state. Availability is read from the replicated selected entity ID, not from whether that entity has finished streaming to this client, so streaming delay is not mistaken for an empty survivor set.
|
The camera activates `ERR_ObserverContext`, inherited from vanilla's manual-camera context. Dedicated input actions bind the arrows and Enter; normal gameplay input remains inherited from `chimeraInputCommon.conf`. Held keys trigger once; opposing arrows cancel. Camera input is disabled while ordinary menus/dialogs are open. These controls replace raw `Debug.KeyState` polling; the old bit-15 mask is documented by the engine, so inspection alone does not establish it as the cause of the reported missed arrows.
|
||||||
|
|
||||||
The existing 250 ms observer monitor refreshes the twelve-second hint on first display, a reason change or a change between having/no selected survivor. Stable state does not repeat hints, and switching between two available survivors does not produce another hint. Closing the camera clears the shown flag. If the native hint manager is temporarily absent, the monitor retries without consuming the first display. There are no new timers, input bindings, RPCs, permission checks or server mutations.
|
The 250 ms local monitor records an unconscious controlled body independently of whichever camera is active. When that same body becomes conscious, it asks the native editor manager (Game Master/ArmaVision) to close and explicitly selects the controller's player camera. This also handles a separate manual camera taking over from the mission's observer camera. Asynchronous editor closure or unavailable player cameras are retried for up to ten seconds; refusal is logged rather than permanently forcing the camera. Once returned, conscious admins can open their cameras normally. Round end, body loss, or changed possession cancels the pending handoff. No remote player is possessed and no administrative privileges change.
|
||||||
|
|
||||||
`tests/test_spectator_guidance.py` executes the actual message and refresh methods for every current reason, a fallback reason, both availability states and all prior/current refresh combinations. It also checks the existing monitoring/command boundaries: no extra timer, state recorded only after a hint exists, close/reopen reset, cycling handled before the admin transport and unchanged admin rejection. These are source-local policy and integration-contract checks. Native compilation, real hint rendering, actual chat input and remote target streaming still require their own tests.
|
The spectator hint explains why the player is observing and how to switch targets/views. Availability uses the replicated selected entity ID, not local streaming completion. The hint refreshes on first display, a reason change, or having/no selected survivor; stable state does not repeatedly show it. Missing/refusing hint managers retain the retry.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
`test_unconscious_controls.py`, `test_spectator_recovery.py`, `test_spectator_guidance.py`, `test_hint_delivery.py`, and `test_participant_selection.py` exercise production input dispatch, held/opposing/menu keys, Enter toggling, camera/editor recovery retries and cancellation, guidance, and canonical server selection. Native APIs and remote clients are doubled in these tests.
|
||||||
|
|
||||||
|
`ERR_UnconsciousCameraProbe` tests native incapacitation, unchanged possession, physical camera tracking, loaded input actions including inherited orbit input, toggling to/from head view, a different manual camera taking over before healing, return to the normal camera, and repeated knockout. It uses a physical stand-in for another player and programmatic input-handler calls; it does not prove physical keyboard delivery or dedicated multiplayer target streaming. Native run results are recorded separately under `.local/analysis/spectator-2026-09-27/`.
|
||||||
|
|
||||||
|
On 27 September, the isolated native fixture completed 25 assertions without script errors or VM exceptions. It verified both camera views, the loaded controls, a separate manual camera takeover, restoration after healing, and repeated knockout. The runner subsequently hit a transient cleanup-classification failure; separate follow-up confirmed the private runtime idle and authored sources unchanged. Existing external resource errors remain. Raw results and failed diagnostic iterations are preserved in the evidence directory above. Physical keyboard delivery, remote client cycling, and native Game Master/ArmaVision menu closure remain manual multiplayer checks.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Supply convoys
|
# Supply convoys
|
||||||
|
|
||||||
The production HQ now uses `ERR_SupplyConvoySystem`, `ERR_SupplyConvoyService` and `ERR_SupplyConvoyTactics`, adapting the owned original `EAI_SupplyConvoys.c` to the combined mod's native AI and force ledger.
|
The production HQ now uses `ERR_SupplyConvoySystem`, `ERR_SupplyConvoyService`, `ERR_SupplyConvoyColumn` and `ERR_SupplyConvoyTactics`, adapting the owned original `EAI_SupplyConvoys.c` to the combined mod's native AI and force ledger.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -20,15 +20,15 @@ The ammo truck receives eight random weapons from the convoy faction's native ar
|
|||||||
|
|
||||||
After prison escape and existing force admission, automatic deployment begins after a 30-second grace period. Every five seconds it tests up to eight road formations, for at most 120 startup passes. It uses friendly military outpost, depot and radio-site road approaches within 500 metres, deduplicated at 300 metres. Starting formations are separated by up to one kilometre and retain all existing road, terrain and obstruction checks.
|
After prison escape and existing force admission, automatic deployment begins after a 30-second grace period. Every five seconds it tests up to eight road formations, for at most 120 startup passes. It uses friendly military outpost, depot and radio-site road approaches within 500 metres, deduplicated at 300 metres. Starting formations are separated by up to one kilometre and retain all existing road, terrain and obstruction checks.
|
||||||
|
|
||||||
The initial road route feeds an ongoing circuit between different friendly bases. Arrival accepts surviving vehicles within 250 metres; a ten-minute travel deadline advances a stuck trip. Convoys pause 45–90 seconds before selecting their next destination. The original carried supplies between bases but did not unload or regenerate them; no artificial unloading is added here.
|
The initial road route feeds an ongoing circuit between different friendly bases. One travel controller paces each column: the lead vehicle follows the route and each follower targets the preceding surviving vehicle. After combat or a new base leg, travel order follows physical approach to the destination while vehicle identities and combat roles remain unchanged. The leader waits when a neighbour gap exceeds 160 metres and resumes below 90 metres; followers regulate their speed and retain stable movement targets. Arrival requires the lead vehicle to physically reach the route endpoint and the followers to close up in order. A timeout never counts as arrival. Commanded vehicles with no five-metre progress retry native navigation after 45 seconds; after three minutes without progress they leave the column, preserving their physical crew and cargo. Combat, boarding, intentional spacing waits and supply stops do not consume that timer. Convoys pause 45–90 seconds after physical arrival before selecting their next destination. The original carried supplies between bases but did not unload or regenerate them; no artificial unloading is added here.
|
||||||
|
|
||||||
Combat starts from fresh identified native group perception within 400 metres of a surviving convoy vehicle or a delivered HQ sighting. Mounted group origins are not used for this distance, because native groups can retain their original spawn position. Infantry dismounts, approaches the left and right flanks, then searches those sectors. Armed escorts provide cover while the ammo truck withdraws; the troop truck waits until the passengers have dismounted. Orders refresh at a bounded thirty-second interval. Still-mounted infantry receives at most six dismount retries, ten seconds apart, to recover from busy doors or a braking truck. Native boarding waypoints require all surviving members and use a 100-metre approach radius; actual compartment membership determines readiness. Crew retention suppresses both group and individual abandonment priorities only for the original mounted vehicle crew and releases on critical damage or drivetrain failure, allowing native emergency abandonment.
|
Combat starts from fresh identified native group perception within 400 metres of a surviving convoy vehicle or a delivered HQ sighting. Mounted group origins are not used for this distance, because native groups can retain their original spawn position. Infantry dismounts, approaches the left and right flanks, then searches those sectors. Armed escorts provide cover while the ammo truck withdraws; the troop truck waits until the passengers have dismounted and cleared it by 25 metres. Convoy vehicles slow to 8 km/h within 45 metres of their fit dismounted teams and restore cruising speed for travel. Contact-driven tactical orders refresh no more often than every thirty seconds and only after the contact moves at least forty metres; unchanged sightings retain the existing manoeuvres. Combat and boarding suspend column travel, and surviving vehicles regroup before resuming. Still-mounted infantry receives at most six dismount retries, ten seconds apart, to recover from busy doors or a braking truck. Native boarding waypoints require all surviving members and use a 100-metre approach radius; actual compartment membership determines readiness. Crew retention suppresses both group and individual abandonment priorities only for the original mounted vehicle crew and releases on critical damage or drivetrain failure, allowing native emergency abandonment.
|
||||||
|
|
||||||
After sixty seconds without a fresh contact, the teams reboard under escort cover. A new contact interrupts boarding. After a 120-second boarding deadline, stranded survivors are released and the remaining vehicles continue. A lost troop truck releases the infantry. Player capture stops convoy control permanently, preserving the actors and loot.
|
After sixty seconds without a fresh contact, the teams reboard under escort cover. A new contact interrupts boarding. While soldiers approach or board, their native orders are preserved. A team with no approach or seat progress for thirty seconds can receive up to three boarding retries, with one tick between cancellation and reissue so native reservations can settle. A five-minute hard deadline releases genuinely stranded survivors and lets the remaining vehicles continue. A lost troop truck releases the infantry. Player capture stops convoy control permanently, preserving the actors and loot.
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
`tests/test_supply_convoys.py` executes production startup, base selection, arrival, circuit timing, contact freshness and perception boundaries, scoped crew-abandonment priorities, combat quiet time, boarding interruption, timeout and truck-loss policies. The receipt tests cover all eight groups and configured fleets through twenty convoys, including no replacement after losses. Existing dispatch, native seat allocation, cargo, HQ radio and force-ledger regressions remain in the main check suite.
|
`tests/test_convoy_column.py` executes the production spacing, stable-target, physical-arrival, leader-promotion, supply-ownership and stuck-recovery controller against native-boundary doubles. `tests/test_supply_convoys.py` executes production startup, base selection, arrival, circuit timing, contact freshness and perception boundaries, scoped crew-abandonment priorities, combat quiet time, boarding interruption, timeout and truck-loss policies. The receipt tests cover all eight groups and configured fleets through twenty convoys, including no replacement after losses. Existing dispatch, native seat allocation, cargo, HQ radio and force-ledger regressions remain in the main check suite.
|
||||||
|
|
||||||
`tools/headless_workbench.py --project chat --smoke supply-convoys --rounds 1 --timeout 900` exercises production automatic startup and physical AI in an isolated Workbench profile. Its final controlled contact explicitly tests native dismount/reboarding rather than claiming that an injected report proves perception. Remote-client replication and twenty-convoy performance require dedicated-server testing. Earlier one-convoy evidence in `convoy-migration.md` and `convoy-startup.md` remains historical evidence for those earlier sources.
|
`tools/headless_workbench.py --project chat --smoke supply-convoys --rounds 1 --timeout 900` exercises production automatic startup and physical AI in an isolated Workbench profile. Its final controlled contact explicitly tests native dismount/reboarding rather than claiming that an injected report proves perception. Remote-client replication and twenty-convoy performance require dedicated-server testing. Earlier one-convoy evidence in `convoy-migration.md` and `convoy-startup.md` remains historical evidence for those earlier sources.
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,8 @@
|
|||||||
"Configs/Map/MapPlain.conf": "A786DD4868598F15",
|
"Configs/Map/MapPlain.conf": "A786DD4868598F15",
|
||||||
"Configs/Navmesh/Navmesh_GM_Eden_Soldier.conf": "8BBC73472092902E",
|
"Configs/Navmesh/Navmesh_GM_Eden_Soldier.conf": "8BBC73472092902E",
|
||||||
"Configs/Navmesh/Navmesh_GM_Eden_Vehicle.conf": "0228A79846FD8A97",
|
"Configs/Navmesh/Navmesh_GM_Eden_Vehicle.conf": "0228A79846FD8A97",
|
||||||
|
"Configs/System/ActionContext/ManualCameraContext.conf": "C538807EEE48BED8",
|
||||||
|
"Configs/System/chimeraInputCommon.conf": "795184CF9AD764DB",
|
||||||
"Configs/System/chimeraMenus.conf": "C747AFB6B750CE9A",
|
"Configs/System/chimeraMenus.conf": "C747AFB6B750CE9A",
|
||||||
"Configs/Systems/MissionSystems.conf": "C50579A2EC48E8EF",
|
"Configs/Systems/MissionSystems.conf": "C50579A2EC48E8EF",
|
||||||
"Prefabs/AI/Groups/Group_Base.et": "000CD338713F2B5A",
|
"Prefabs/AI/Groups/Group_Base.et": "000CD338713F2B5A",
|
||||||
@@ -50,6 +52,8 @@
|
|||||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_HeliPilot.et": "42A502E3BB727CEB",
|
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_HeliPilot.et": "42A502E3BB727CEB",
|
||||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Sniper.et": "0F6689B491641155",
|
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Sniper.et": "0F6689B491641155",
|
||||||
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et": "2F912ED6E399FF47",
|
"Prefabs/Characters/Factions/BLUFOR/US_Army/Character_US_Unarmed.et": "2F912ED6E399FF47",
|
||||||
|
"Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF.et": "36E28C628A2F83CB",
|
||||||
|
"Prefabs/Characters/Factions/BLUFOR/US_Army/GreenBerets/Character_US_SF_Medic.et": "9A234C5857D92187",
|
||||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et": "23ADBBC31B6A3DC6",
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AR.et": "23ADBBC31B6A3DC6",
|
||||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et": "1C78331E156A3D65",
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_AT.et": "1C78331E156A3D65",
|
||||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_HeliPilot.et": "A62FA97C4EC64F14",
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_HeliPilot.et": "A62FA97C4EC64F14",
|
||||||
@@ -59,6 +63,8 @@
|
|||||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et": "5436629450D8387A",
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et": "5436629450D8387A",
|
||||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et": "333DA6244C7DA34C",
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SR.et": "333DA6244C7DA34C",
|
||||||
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Sharpshooter.et": "976AC400219898FA",
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_Sharpshooter.et": "976AC400219898FA",
|
||||||
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF.et": "2DB452B3EC386B92",
|
||||||
|
"Prefabs/Characters/Factions/OPFOR/USSR_Army/Spetsnaz/Character_USSR_SF_Medic.et": "8CA70597606992EC",
|
||||||
"Prefabs/Characters/Footwear/CombatBoots_US_01.et": "DAAFD15478BDE1C3",
|
"Prefabs/Characters/Footwear/CombatBoots_US_01.et": "DAAFD15478BDE1C3",
|
||||||
"Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et": "F545C1BE7C99954A",
|
"Prefabs/Characters/HeadGear/Hat_Patrol_US_01/Hat_Patrol_US_01.et": "F545C1BE7C99954A",
|
||||||
"Prefabs/Characters/HeadGear/Helmet_PASGT_01/Helmet_PASGT_01_base.et": "8A2B2A544F3ED831",
|
"Prefabs/Characters/HeadGear/Helmet_PASGT_01/Helmet_PASGT_01_base.et": "8A2B2A544F3ED831",
|
||||||
|
|||||||
+20
-1
@@ -29,9 +29,28 @@ int main(){
|
|||||||
vector away=StandOff({100,120,0},{0,0,0},{-40,0,0});assert(away.x>0);
|
vector away=StandOff({100,120,0},{0,0,0},{-40,0,0});assert(away.x>0);
|
||||||
vector clear=StandOff({300,120,0},{0,0,0},{-40,0,0});assert(clear.x==-40);
|
vector clear=StandOff({300,120,0},{0,0,0},{-40,0,0});assert(clear.x==-40);
|
||||||
vector coincident=StandOff({0,120,0},{0,0,0},{0,0,0});assert(std::isfinite(coincident.x)&&coincident.x>0);
|
vector coincident=StandOff({0,120,0},{0,0,0},{0,0,0});assert(std::isfinite(coincident.x)&&coincident.x>0);
|
||||||
|
// Clearing the target area specifies a minimum outward SPEED, not a
|
||||||
|
// per-frame impulse. Repeated calls must not accelerate an already-safe
|
||||||
|
// radial velocity, even while the aircraft remains inside the radius.
|
||||||
|
vector outward{25,0,28};
|
||||||
|
for(int i=0;i<600;i++)outward=StandOff({100,120,0},{0,0,0},outward);
|
||||||
|
assert(std::abs(outward.x-25)<.001&&outward.z==28);
|
||||||
|
vector correction=StandOff({170,120,0},{0,0,0},{-40,0,28});
|
||||||
|
assert(std::abs(correction.x-5)<.001&&correction.z==28);
|
||||||
|
// The same stationary close-contact observation at different frame rates
|
||||||
|
// must not inject unbounded speed into the persistent smoothed velocity.
|
||||||
|
for(int hz: {30,60,120}){
|
||||||
|
vector v{-40,0,28};
|
||||||
|
for(int frame=0;frame<hz*5;frame++){
|
||||||
|
vector desired{-10,0,28};v=v+(desired-v)*(0.6f/hz);
|
||||||
|
v=StandOff({100,120,0},{0,0,0},v);
|
||||||
|
assert(v.Length()<40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: original 15-minute launch/loss cadence, active/captured timer disarm, saved delay reset, stop, 300m orbit and close-contact standoff')
|
print('PASS: original 15-minute launch/loss cadence, active/captured timer disarm, saved delay reset, stop, 300m orbit and bounded close-contact standoff at 30/60/120 Hz')
|
||||||
|
|
||||||
# Explicit entity deletion must not leave an inactive launch deadline behind.
|
# Explicit entity deletion must not leave an inactive launch deadline behind.
|
||||||
removed=body(path,'void Removed(')
|
removed=body(path,'void Removed(')
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ int main(){
|
|||||||
p.Committed(45000);assert(!p.Consider(&s,50000));assert(!p.Consider(&s,525000));assert(p.Consider(&s,570000));
|
p.Committed(45000);assert(!p.Consider(&s,50000));assert(!p.Consider(&s,525000));assert(p.Consider(&s,570000));
|
||||||
s.CommittedFit=6;assert(!p.Consider(&s,600000));s.CommittedFit=0;s.GroundFit=6;assert(!p.Consider(&s,600000));
|
s.CommittedFit=6;assert(!p.Consider(&s,600000));s.CommittedFit=0;s.GroundFit=6;assert(!p.Consider(&s,600000));
|
||||||
s.ConfirmedLosses=2;assert(!p.Consider(&s,600000));assert(p.Consider(&s,620000));
|
s.ConfirmedLosses=2;assert(!p.Consider(&s,600000));assert(p.Consider(&s,620000));
|
||||||
s.ArtilleryBusy=true;assert(!p.Consider(&s,630000));s.ArtilleryBusy=false;assert(!p.Consider(&s,640000));assert(p.Consider(&s,660000));
|
s.ArtilleryBusy=true;assert(!p.Consider(&s,630000));s.ArtilleryBusy=false;assert(p.Consider(&s,640000));assert(p.Consider(&s,660000));
|
||||||
s.BeliefId=2;assert(!p.Consider(&s,670000));assert(!p.Consider(&s,689999));assert(p.Consider(&s,690000));
|
s.BeliefId=2;assert(!p.Consider(&s,670000));assert(!p.Consider(&s,689999));assert(p.Consider(&s,690000));
|
||||||
auto good=s;
|
auto good=s;
|
||||||
s.Age=91;assert(!p.Consider(&s,700000));s=good;s.Confidence=.69;assert(!p.Consider(&s,700000));
|
s.Age=91;assert(!p.Consider(&s,700000));s=good;s.Confidence=.69;assert(!p.Consider(&s,700000));
|
||||||
@@ -30,6 +30,26 @@ int main(){
|
|||||||
s=good;s.MotionKnown=false;assert(!p.Consider(&s,700000));s=good;s.AircraftReady=false;assert(!p.Consider(&s,700000));
|
s=good;s.MotionKnown=false;assert(!p.Consider(&s,700000));s=good;s.AircraftReady=false;assert(!p.Consider(&s,700000));
|
||||||
s=good;s.Age=std::numeric_limits<float>::quiet_NaN();assert(!p.Consider(&s,700000));
|
s=good;s.Age=std::numeric_limits<float>::quiet_NaN();assert(!p.Consider(&s,700000));
|
||||||
assert(!p.Consider(nullptr,700000));assert(!p.Consider(&good,std::numeric_limits<float>::infinity()));
|
assert(!p.Consider(nullptr,700000));assert(!p.Consider(&good,std::numeric_limits<float>::infinity()));
|
||||||
|
// Artillery blocks authorization, not the history of fresh sightings and
|
||||||
|
// ground-force shortage. Repeated <45-second clear windows used to starve
|
||||||
|
// airborne support by restarting its confirmation timer on every salvo.
|
||||||
|
ERR_AirSupportPolicy blocked;auto contact=good;contact.BeliefId=9;contact.GroundFit=0;contact.ConfirmedLosses=0;
|
||||||
|
assert(!blocked.Consider(&contact,0));
|
||||||
|
contact.ArtilleryBusy=true;
|
||||||
|
assert(!blocked.Consider(&contact,10000));
|
||||||
|
assert(!blocked.Consider(&contact,46000));
|
||||||
|
contact.ArtilleryBusy=false;assert(blocked.Consider(&contact,47000));
|
||||||
|
// Stale intelligence and a changed target still invalidate confirmation.
|
||||||
|
contact.Age=91;assert(!blocked.Consider(&contact,48000));
|
||||||
|
contact.Age=5;assert(!blocked.Consider(&contact,49000));
|
||||||
|
contact.BeliefId=10;assert(!blocked.Consider(&contact,90000));
|
||||||
|
assert(!blocked.Consider(&contact,134999));assert(blocked.Consider(&contact,135000));
|
||||||
|
// Busy airspace must not preconfirm a new drop during the 8-minute cooldown.
|
||||||
|
blocked.Committed(135000);contact.ArtilleryBusy=true;
|
||||||
|
assert(!blocked.Consider(&contact,614999));
|
||||||
|
contact.ArtilleryBusy=false;assert(!blocked.Consider(&contact,615000));
|
||||||
|
assert(!blocked.Consider(&contact,659999));assert(blocked.Consider(&contact,660000));
|
||||||
|
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: production airborne memory confidence, persistence, ground coverage, confirmed losses, artillery veto, cooldown and invalid-input rules')
|
print('PASS: production airborne memory confidence, persistence, ground coverage, confirmed losses, artillery veto, cooldown and invalid-input rules')
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
"""Run the production column controller against physical-position/native-order doubles."""
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
import re
|
||||||
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyColumn.c'
|
||||||
|
|
||||||
|
def cpp(s):
|
||||||
|
s=re.sub(r'//[^\n]*', '', s).replace('protected ', '').replace('out ', '')
|
||||||
|
s=s.replace('foreach (', 'for (').replace('null','nullptr')
|
||||||
|
for typ in ('ERR_ConvoyVehicle','ERR_ConvoyColumnVehicle'):
|
||||||
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||||
|
for obj in ('m_Task','vehicle','state','scan','promoted','ahead','m_State[i]','m_Task->Vehicles[previous]'):
|
||||||
|
s=s.replace(obj+'.',obj+'->')
|
||||||
|
for obj in ('Entity','Group','Waypoint','Route','Receipt','Combat'):
|
||||||
|
s=s.replace(obj+'.',obj+'->')
|
||||||
|
for cls in ('ERR_ConvoyColumnRules','ERR_ConvoyDriving','ERR_SupplyConvoyTactics','ERR_HQ','Math','vector','string'):
|
||||||
|
s=s.replace(cls+'.',cls+'::')
|
||||||
|
s=s.replace('m_Task->Vehicles[index].', 'm_Task->Vehicles[index]->').replace('(index + 1).ToString()', 'std::to_string(index + 1)')
|
||||||
|
s=s.replace('float largestGap; bool supplyHold;', 'float largestGap=0; bool supplyHold=false;')
|
||||||
|
return s
|
||||||
|
rules='\n'.join(cpp(body(p, x)) for x in ('static bool WaitForRear(', 'static bool TooClose(', 'static float Speed('))
|
||||||
|
methods='\n'.join(cpp(body(p,x)) for x in ('protected float DistanceToTarget(', 'protected void Reorder(', 'void Reset(', 'protected void Hold(', 'protected bool Progress(', 'bool Tick('))
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}template<class...T>static string Format(T...){return "event";}};
|
||||||
|
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||||
|
namespace Math{float Max(float a,float b){return a>b?a:b;}int Min(int a,int b){return a<b?a:b;}float Round(float x){return std::round(x);}}
|
||||||
|
namespace ERR_HQ{int stranded=0,retries=0;void Event(string type,string){if(type=="CONVOY STRANDED")stranded++;if(type=="CONVOY ROUTE RETRY")retries++;}}
|
||||||
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
||||||
|
struct ERR_ConvoyColumnRules{'''+rules+r'''};
|
||||||
|
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||||
|
struct Waypoint:Entity{};
|
||||||
|
struct Group{int added=0;void AddWaypoint(Waypoint*){added++;}};
|
||||||
|
struct Route{Array<vector>points={{0},{400}};int Count(){return points.Count();}vector At(int i){return points[i];}};
|
||||||
|
struct Combat{bool Held=false;};
|
||||||
|
struct ERR_ConvoyVehicle{
|
||||||
|
int Index=0,RouteIndex=0,clears=0;bool Released=false,Arrived=false,SupplyHeld=false;
|
||||||
|
struct Entity entity;struct Entity* Entity=&entity;struct Group group;struct Group* Group=&group;
|
||||||
|
struct Combat combat;struct Combat* Combat=&combat;struct Waypoint* Waypoint=nullptr;
|
||||||
|
void ClearOrder(){if(Waypoint)clears++;delete Waypoint;Waypoint=nullptr;}
|
||||||
|
void Release(void*){Released=true;ClearOrder();}
|
||||||
|
bool Order(struct Route* route,float){if(!Waypoint){Waypoint=new struct Waypoint();Waypoint->position=route->At(RouteIndex);Group->AddWaypoint(Waypoint);}return true;}
|
||||||
|
~ERR_ConvoyVehicle(){ClearOrder();}
|
||||||
|
};
|
||||||
|
namespace ERR_ConvoyDriving{bool Restore(void*,ERR_ConvoyVehicle*){return false;}}
|
||||||
|
namespace ERR_SupplyConvoyTactics{Waypoint* Move(vector p,float){auto w=new Waypoint();w->position=p;return w;}}
|
||||||
|
struct Receipt{string SourceId="test";};
|
||||||
|
struct ERR_SupplyConvoyTask{Array<ERR_ConvoyVehicle*>Vehicles;struct Route route;struct Route* Route=&route;int Legs=0;struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||||
|
struct ERR_ConvoyColumnVehicle{bool Holding=false;vector Progress;float LastProgress=-1,NextRetry=0,NextTarget=0,Speed=-1;int Ahead=-1;};
|
||||||
|
struct Column{
|
||||||
|
void* m_Mode=nullptr;ERR_SupplyConvoyTask* m_Task;Array<ERR_ConvoyColumnVehicle*>m_State;
|
||||||
|
Array<int>m_Order={0,1,2};int m_Leader=-1,m_RouteIndex=0;bool m_RearWait=false;float m_NextLog=0;
|
||||||
|
void Speed(ERR_ConvoyVehicle*,ERR_ConvoyColumnVehicle* state,float speed){state->Speed=speed;}
|
||||||
|
'''+methods+r'''};
|
||||||
|
int main(){
|
||||||
|
assert(!ERR_ConvoyColumnRules::WaitForRear(160,false));assert(ERR_ConvoyColumnRules::WaitForRear(161,false));
|
||||||
|
assert(ERR_ConvoyColumnRules::WaitForRear(91,true));assert(!ERR_ConvoyColumnRules::WaitForRear(90,true));
|
||||||
|
assert(ERR_ConvoyColumnRules::TooClose(39,false));assert(!ERR_ConvoyColumnRules::TooClose(40,false));
|
||||||
|
assert(ERR_ConvoyColumnRules::TooClose(64,true));assert(!ERR_ConvoyColumnRules::TooClose(65,true));
|
||||||
|
ERR_ConvoyVehicle a,b,c;b.Index=1;c.Index=2;b.entity.position={-35};c.entity.position={-70};
|
||||||
|
ERR_ConvoyColumnVehicle sa,sb,sc;ERR_SupplyConvoyTask task;task.Vehicles={&a,&b,&c};Column column{nullptr,&task,{&sa,&sb,&sc}};
|
||||||
|
assert(column.Tick(0));assert(a.RouteIndex==1&&a.Waypoint&&a.Waypoint->position.x==400);assert(!b.Waypoint&&!c.Waypoint);
|
||||||
|
a.entity.position={80};column.Tick(1000);assert(b.Waypoint&&b.Waypoint->position.x==80&&sb.Speed==45);assert(!c.Waypoint);
|
||||||
|
int additions=b.group.added;a.entity.position={110};column.Tick(2000);assert(b.group.added==additions); // stable target, no per-second churn
|
||||||
|
a.entity.position={200};column.Tick(3000);assert(column.m_RearWait&&!a.Waypoint&&b.Waypoint); // leader waits for laggard
|
||||||
|
b.entity.position={130};c.entity.position={60};column.Tick(11000);assert(!column.m_RearWait&&a.Waypoint);
|
||||||
|
assert(b.Waypoint->position.x==200); // native target follows predecessor, never remote base marker
|
||||||
|
b.SupplyHeld=true;auto supplyOrder=b.Waypoint;column.Tick(12000);assert(!a.Waypoint&&b.Waypoint==supplyOrder&&sb.LastProgress==-1);
|
||||||
|
b.SupplyHeld=false;b.combat.Held=true;column.Tick(13000);assert(b.Waypoint==supplyOrder&&sb.LastProgress==-1);b.combat.Held=false;
|
||||||
|
// Real vehicle positions determine arrival; no stale seated-group origin is consulted.
|
||||||
|
a.entity.position={400};b.entity.position={330};c.entity.position={260};column.Tick(20000);
|
||||||
|
assert(a.Arrived&&b.Arrived&&c.Arrived&&!a.Waypoint&&!b.Waypoint&&!c.Waypoint);
|
||||||
|
// Loss promotes the next surviving vehicle at the current route cursor, no replacement/restart at spawn.
|
||||||
|
column.Reset(20500);assert(column.m_Leader==0&&column.m_RouteIndex==2); // combat reset preserves route progress for a later promotion
|
||||||
|
a.Release(nullptr);b.Arrived=c.Arrived=false;column.Tick(21000);assert(column.m_Leader==1&&b.RouteIndex==1&&!b.Arrived&&b.Waypoint->position.x==400);
|
||||||
|
// Commanded stationary time alone retries and eventually releases; holds reset this clock.
|
||||||
|
ERR_ConvoyVehicle stuck;ERR_ConvoyColumnVehicle ss;
|
||||||
|
assert(column.Progress(&stuck,&ss,0));assert(column.Progress(&stuck,&ss,45000));assert(ERR_HQ::retries==1&&!stuck.Released);
|
||||||
|
column.Hold(&stuck,&ss,170000);assert(column.Progress(&stuck,&ss,180000));assert(!stuck.Released);
|
||||||
|
stuck.entity.position={6};assert(column.Progress(&stuck,&ss,300000));assert(ss.LastProgress==300000);
|
||||||
|
assert(column.Progress(&stuck,&ss,479999));assert(!column.Progress(&stuck,&ss,480000)&&stuck.Released&&ERR_HQ::stranded==1);
|
||||||
|
column.Reset(500000,true);assert(sa.LastProgress==-1&&sb.LastProgress==-1&&!column.m_RearWait&&column.m_Leader==-1);
|
||||||
|
// Post-combat physical order avoids sending the front truck toward a rear vehicle coming toward it.
|
||||||
|
b.entity.position={100};c.entity.position={300};b.Arrived=c.Arrived=false;column.m_RouteIndex=1;column.Reset(500001,false,true);
|
||||||
|
assert(column.m_Order[0]==2&&column.m_Order[1]==1&&column.m_Order[2]==0); // released vehicle last
|
||||||
|
assert(task.Vehicles[1]==&b&&task.Vehicles[2]==&c); // combat roles and canonical identities unchanged
|
||||||
|
column.Tick(500002);assert(column.m_Leader==2&&!c.Waypoint&&b.Waypoint&&b.Waypoint->position.x==300);
|
||||||
|
b.entity.position={230};column.Tick(500003);assert(c.Waypoint&&c.Waypoint->position.x==400);
|
||||||
|
b.Release(nullptr);c.Release(nullptr);assert(!column.Tick(510000));
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: production convoy spacing, physical arrival, stable follower targets, supply ownership, leader promotion and bounded stuck recovery')
|
||||||
|
|
||||||
|
q=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
||||||
|
orders=body(q,'protected void Orders(').replace('protected ','').replace('m_Task.','m_Task->').replace('vector.','vector::')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||||
|
struct Items{int count;int Count(){return count;}};
|
||||||
|
struct Task{Items Vehicles{6},Teams{2};};
|
||||||
|
struct Controller{Task task;Task* m_Task=&task;float m_NextOrders=0;vector m_LastOrderContact,m_Contact;bool m_OrdersIssued=false,m_TruckWithdrawn=false;int support=0,flank=0;
|
||||||
|
void Support(int,bool){support++;}void Flank(int,bool){flank++;}
|
||||||
|
'''+orders+r'''};
|
||||||
|
int main(){
|
||||||
|
Controller c;c.Orders(true,0);assert(c.support==6&&c.flank==2);
|
||||||
|
c.Orders(false,30000);assert(c.support==6&&c.flank==2&&c.m_NextOrders==60000);
|
||||||
|
c.m_Contact={39};c.Orders(false,60000);assert(c.support==6&&c.flank==2);
|
||||||
|
c.m_Contact={40};c.Orders(false,90000);assert(c.support==12&&c.flank==4);
|
||||||
|
c.m_OrdersIssued=false;c.Orders(true,100000);assert(c.support==18&&c.flank==6); // new engagement can issue at same location
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: convoy combat keeps native manoeuvres for unchanged contacts and refreshes when contact moves')
|
||||||
|
|
||||||
|
probe=ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSupplyConvoyProbe.c'
|
||||||
|
departure=body(probe,'protected int Departure(').replace('protected ','').replace('m_CombatTask.','m_CombatTask->').replace('Vehicles[2].','Vehicles[2]->').replace('Entity.','Entity->').replace('vector.','vector::')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||||
|
void Print(const char*){}
|
||||||
|
struct Entity{vector position;vector GetOrigin(){return position;}};
|
||||||
|
struct Vehicle{bool Released=false;struct Entity entity;struct Entity* Entity=&entity;};
|
||||||
|
struct Bases{int Count(){return 2;}};
|
||||||
|
struct Task{bool Stopped=false;int Legs=0;Vehicle* Vehicles[3];struct Bases Bases;};
|
||||||
|
struct Probe{Task* m_CombatTask;int m_Phase=3,m_TestLegs=0;vector m_RouteResumePosition;bool m_AllMoved=true;
|
||||||
|
'''+departure+r'''};
|
||||||
|
int main(){
|
||||||
|
Task task;Vehicle truck;task.Vehicles[2]=&truck;Probe p{&task};truck.entity.position={200};
|
||||||
|
assert(p.Departure()==0&&p.m_Phase==3); // earlier regroup displacement cannot pass
|
||||||
|
task.Legs=1;assert(p.Departure()==0&&p.m_Phase==4&&p.m_RouteResumePosition.x==200);
|
||||||
|
assert(p.Departure()==0);truck.entity.position={219};assert(p.Departure()==0);
|
||||||
|
truck.entity.position={220};assert(p.Departure()==1&&p.m_Phase==5);
|
||||||
|
truck.Released=true;assert(p.Departure()==-1);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
smoke=(ROOT/'addons/core/Scripts/Game/ERR/40_Chat/ERR_NativeSmoke.c').read_text()
|
||||||
|
convoy=smoke.split('else if (m_ERRSmokeStep == 106)')[1].split('else if (m_ERRSmokeStep == 105)')[0]
|
||||||
|
assert 'Transition(ERR_Phase.DEFEAT)' in convoy and 'm_ERRSmokeStep = 1061' in convoy
|
||||||
|
assert 'damage.Kill' not in convoy and 'm_ERRSmokeStep = 9' not in convoy
|
||||||
|
print('PASS: native convoy proof excludes pre-departure movement and ends via explicit fixture reset, not prohibited player death')
|
||||||
|
|
||||||
|
boarding=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBoardingProgress.c'
|
||||||
|
retry=body(boarding,'bool Retry(')
|
||||||
|
refresh=body(q,'protected void RefreshBoarding(').replace('protected ','').replace('foreach (','for (').replace('m_Task.','m_Task->').replace('team.','team->').replace('Vehicles[2].','Vehicles[2]->').replace('BoardingProgress.','BoardingProgress->').replace('Receipt.','Receipt->').replace('ERR_HQ.','ERR_HQ::').replace('Math.','Math::').replace('string.','string::').replace('ERR_SupplyTeam team','ERR_SupplyTeam* team')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
struct string{template<class...T>static string Format(T...){return {};}};
|
||||||
|
namespace Math{float Round(float x){return std::round(x);}}
|
||||||
|
namespace ERR_HQ{void Event(const char*,string){}}
|
||||||
|
struct ERR_ConvoyBoardingProgress{int Seated=-1,Retries=0;float Distance=-1,LastProgress=0,NextRetry=0;
|
||||||
|
'''+retry+r'''};
|
||||||
|
struct ERR_SupplyTeam{bool Released=false,aboard=false;float BoardingRetryAt=0,distance=200;int seated=0,clears=0,adds=0;ERR_ConvoyBoardingProgress* BoardingProgress=nullptr;
|
||||||
|
bool Aboard(void*,bool){return aboard;}int BoardingCount(void*,float& d){d=distance;return seated;}void Clear(){clears++;}void Add(int){adds++;}};
|
||||||
|
struct Vehicle{void* Entity=nullptr;};struct Receipt{const char* SourceId="test";};
|
||||||
|
struct Task{std::vector<ERR_SupplyTeam*>Teams;Vehicle* Vehicles[3];struct Receipt receipt;struct Receipt* Receipt=&receipt;};
|
||||||
|
struct Tactics{Task* m_Task;int Board(void*,bool){return 1;}
|
||||||
|
'''+refresh+r'''};
|
||||||
|
int main(){
|
||||||
|
ERR_ConvoyBoardingProgress p;assert(!p.Retry(0,200,0));
|
||||||
|
assert(!p.Retry(0,194,29000));assert(!p.Retry(0,194,58000));assert(p.Retry(0,194,59000));
|
||||||
|
assert(!p.Retry(1,100,60000));assert(!p.Retry(1,100,89999));assert(p.Retry(1,100,90000));
|
||||||
|
assert(p.Retry(1,100,120000));assert(!p.Retry(1,100,150000)&&p.Retries==3); // bounded even if no progress
|
||||||
|
Task task;Vehicle truck;task.Vehicles[2]=&truck;ERR_SupplyTeam team,finished,released;finished.aboard=true;released.Released=true;
|
||||||
|
task.Teams={&team,&finished,&released};Tactics t{&task};t.RefreshBoarding(0);assert(team.BoardingProgress&&!finished.BoardingProgress&&!released.BoardingProgress);
|
||||||
|
t.RefreshBoarding(30000);assert(team.clears==1&&team.adds==0&&team.BoardingRetryAt==31000);
|
||||||
|
t.RefreshBoarding(30999);assert(team.adds==0);t.RefreshBoarding(31000);assert(team.adds==1&&team.BoardingRetryAt==0);
|
||||||
|
team.seated=6;team.distance=10;t.RefreshBoarding(40000);assert(team.clears==1);t.RefreshBoarding(69999);assert(team.clears==1);
|
||||||
|
t.RefreshBoarding(70000);assert(team.clears==2);team.aboard=true;t.RefreshBoarding(71000);assert(team.adds==1); // do not disturb completed boarding
|
||||||
|
delete team.BoardingProgress;
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: boarding retries only after approach/seat progress stops, capped at three, with delayed native reissue and completed-team preservation')
|
||||||
|
|
||||||
|
safe='\n'.join(body(q,s) for s in ('protected float InfantryDistance(', 'protected void ProtectInfantry('))
|
||||||
|
safe=safe.replace('protected ','').replace('foreach (','for (')
|
||||||
|
safe=safe.replace('AICarMovementComponent movement = AICarMovementComponent.Cast(vehicle.Entity.FindComponent(AICarMovementComponent));', 'AICarMovementComponent* movement = &vehicle->Entity->movement;')
|
||||||
|
for typ in ('IEntity','ERR_SupplyTeam','ERR_ConvoyVehicle'):safe=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',safe)
|
||||||
|
for obj in ('m_Task','team','member','vehicle','movement'):safe=safe.replace(obj+'.',obj+'->')
|
||||||
|
for cls in ('ERR_Actors','CompartmentAccessComponent','Math','vector'):safe=safe.replace(cls+'.',cls+'::')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
struct vector{float x=0;static float DistanceXZ(vector a,vector b){return std::abs(a.x-b.x);}};
|
||||||
|
namespace Math{float Min(float a,float b){return a<b?a:b;}}
|
||||||
|
struct AICarMovementComponent{float speed=99;void SetCruiseSpeed(float s){speed=s;}};
|
||||||
|
struct IEntity{vector position;bool fit=true,aboard=false;AICarMovementComponent movement;vector GetOrigin(){return position;}};
|
||||||
|
namespace ERR_Actors{bool CanFight(IEntity* m){return m&&m->fit;}}
|
||||||
|
namespace CompartmentAccessComponent{IEntity* GetVehicleIn(IEntity* m){return m->aboard?m:nullptr;}}
|
||||||
|
struct ERR_SupplyTeam{bool Released=false;std::vector<IEntity*>Members;};
|
||||||
|
struct ERR_ConvoyVehicle{bool Released=false;IEntity* Entity;};
|
||||||
|
struct Task{std::vector<ERR_SupplyTeam*>Teams;std::vector<ERR_ConvoyVehicle*>Vehicles;};
|
||||||
|
struct Tactics{Task* m_Task;
|
||||||
|
'''+safe+r'''};
|
||||||
|
int main(){
|
||||||
|
IEntity truck{{0}},car{{100}},foot{{30}},passenger{{0}};passenger.aboard=true;ERR_SupplyTeam team{false,{&foot,&passenger}};
|
||||||
|
ERR_ConvoyVehicle a{false,&truck},b{false,&car};Task task{{&team},{&a,&b}};Tactics t{&task};
|
||||||
|
assert(t.InfantryDistance(&truck)==30);t.ProtectInfantry();assert(truck.movement.speed==8&&car.movement.speed==35);
|
||||||
|
foot.aboard=true;t.ProtectInfantry();assert(truck.movement.speed==35&&car.movement.speed==35);
|
||||||
|
foot.aboard=false;foot.fit=false;assert(t.InfantryDistance(&truck)>100000);foot.fit=true;team.Released=true;assert(t.InfantryDistance(&truck)>100000);
|
||||||
|
team.Released=false;a.Released=true;truck.movement.speed=99;t.ProtectInfantry();assert(truck.movement.speed==99); // never retake a released vehicle
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: slow only owned vehicles near fit dismounted infantry; ignore passengers, casualties and released teams/vehicles')
|
||||||
|
|
||||||
|
restore=body(p,'static bool Restore(').replace('ERR_GameMode mode','void* mode').replace('ERR_ConvoyVehicle vehicle','ERR_ConvoyVehicle* vehicle')
|
||||||
|
restore=restore.replace('SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(vehicle.Entity.FindComponent(SCR_AIVehicleUsageComponent));','SCR_AIVehicleUsageComponent* usage = vehicle->Entity->usage;')
|
||||||
|
restore=restore.replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
|
||||||
|
for obj in ('vehicle','Group','utility','usage'):restore=restore.replace(obj+'.',obj+'->')
|
||||||
|
restore=restore.replace('ERR_ConvoyMountedCrew.','ERR_ConvoyMountedCrew::')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
struct SCR_AIVehicleUsageComponent{bool pilot=true;bool CanBePiloted(){return pilot;}};
|
||||||
|
struct Entity{SCR_AIVehicleUsageComponent* usage=nullptr;};
|
||||||
|
struct SCR_AIGroupUtilityComponent{void* m_VehicleMgr=this;bool known=false;int added=0;bool IsUsableVehicle(Entity*){return known;}void AddUsableVehicle(SCR_AIVehicleUsageComponent*){known=true;added++;}};
|
||||||
|
struct Group{SCR_AIGroupUtilityComponent* utility=nullptr;SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return utility;}};
|
||||||
|
struct ERR_ConvoyVehicle{bool Released=false,valid=true;struct Group group;struct Group* Group=&group;struct Entity entity;struct Entity* Entity=&entity;void* Driver=this;bool CrewValid(void*){return valid;}};
|
||||||
|
namespace ERR_ConvoyMountedCrew{bool healthyOwned=true;bool Retain(void*,Entity*){return healthyOwned;}}
|
||||||
|
'''+restore+r'''
|
||||||
|
int main(){
|
||||||
|
int mode;ERR_ConvoyVehicle car;SCR_AIGroupUtilityComponent utility;SCR_AIVehicleUsageComponent usage;car.group.utility=&utility;car.entity.usage=&usage;
|
||||||
|
assert(!Restore(nullptr,&car)&&!Restore(&mode,nullptr));car.Released=true;assert(!Restore(&mode,&car));car.Released=false;
|
||||||
|
car.valid=false;assert(!Restore(&mode,&car));car.valid=true;ERR_ConvoyMountedCrew::healthyOwned=false;assert(!Restore(&mode,&car));ERR_ConvoyMountedCrew::healthyOwned=true;
|
||||||
|
car.group.utility=nullptr;assert(!Restore(&mode,&car));car.group.utility=&utility;utility.m_VehicleMgr=nullptr;assert(!Restore(&mode,&car));utility.m_VehicleMgr=&utility;
|
||||||
|
car.entity.usage=nullptr;assert(!Restore(&mode,&car));car.entity.usage=&usage;usage.pilot=false;assert(!Restore(&mode,&car));usage.pilot=true;
|
||||||
|
assert(Restore(&mode,&car)&&utility.known&&utility.added==1);assert(!Restore(&mode,&car)&&utility.added==1);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: restore missing native driving registration only for healthy owned seated crews; preserve existing association and refuse invalid/released vehicles')
|
||||||
@@ -78,5 +78,6 @@ int main(){
|
|||||||
''')
|
''')
|
||||||
build=body(p,'static bool Build(')
|
build=body(p,'static bool Build(')
|
||||||
assert 'candidateIndex < 33' in build
|
assert 'candidateIndex < 33' in build
|
||||||
|
assert build.index('ERR_SupplyConvoyRules.Separated(') < build.index('Prefer(distance,')
|
||||||
assert build.index('Prefer(distance,') < build.index('ERR_ConvoyRules.GroundClear(')
|
assert build.index('Prefer(distance,') < build.index('ERR_ConvoyRules.GroundClear(')
|
||||||
print('PASS: actual road interpolation/projection/orientation, six35m starts,200m forward tail, preserved bends, short/far/degenerate/nonfinite/overdense/hairpin refusal and deterministic candidate ties')
|
print('PASS: actual road interpolation/projection/orientation, six35m starts,200m forward tail, preserved bends, short/far/degenerate/nonfinite/overdense/hairpin refusal and deterministic candidate ties')
|
||||||
|
|||||||
@@ -7,12 +7,13 @@ def native(signature):
|
|||||||
return s.replace('perceptionComp.', 'perceptionComp->').replace('m_CombatComponent.', 'm_CombatComponent->')
|
return s.replace('perceptionComp.', 'perceptionComp->').replace('m_CombatComponent.', 'm_CombatComponent->')
|
||||||
run(r'''
|
run(r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
enum class EAISkill{REGULAR,CYLON};
|
enum class EAISkill{REGULAR,CYLON};
|
||||||
struct SCR_AIThreatSystem{};
|
struct SCR_AIThreatSystem{};
|
||||||
struct PerceptionComponent{float factor=0;void SetPerceptionFactor(float f){factor=f;}};
|
struct PerceptionComponent{float factor=0;void SetPerceptionFactor(float f){factor=f;}};
|
||||||
struct Native{
|
struct Native{
|
||||||
int calls=0;EAISkill sampled=EAISkill::CYLON;
|
float error=1;int calls=0;EAISkill sampled=EAISkill::CYLON;
|
||||||
float GetRandomFactor(EAISkill s,float mu){calls++;sampled=s;return mu+1;}
|
float GetRandomFactor(EAISkill s,float mu){calls++;sampled=s;return mu+error;}
|
||||||
void UpdatePerceptionFactor(PerceptionComponent* p,SCR_AIThreatSystem*){calls++;p->factor=.4f;}
|
void UpdatePerceptionFactor(PerceptionComponent* p,SCR_AIThreatSystem*){calls++;p->factor=.4f;}
|
||||||
};
|
};
|
||||||
struct Combat:Native{
|
struct Combat:Native{
|
||||||
@@ -27,15 +28,17 @@ struct Aim:Native{
|
|||||||
};
|
};
|
||||||
int main(){
|
int main(){
|
||||||
Aim aim;Combat combat;PerceptionComponent perception;SCR_AIThreatSystem threat;
|
Aim aim;Combat combat;PerceptionComponent perception;SCR_AIThreatSystem threat;
|
||||||
aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::CYLON);
|
assert(aim.GetRandomFactor(EAISkill::CYLON,0)==1);assert(aim.sampled==EAISkill::CYLON);
|
||||||
aim.m_CombatComponent=&combat;aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::CYLON);
|
aim.m_CombatComponent=&combat;assert(aim.GetRandomFactor(EAISkill::CYLON,0)==1);assert(aim.sampled==EAISkill::CYLON);
|
||||||
combat.bound=true;aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::REGULAR);
|
combat.bound=true;assert(std::abs(aim.GetRandomFactor(EAISkill::CYLON,0)-.9f)<.00001f);assert(aim.sampled==EAISkill::REGULAR);
|
||||||
|
assert(std::abs(aim.GetRandomFactor(EAISkill::CYLON,5)-5.9f)<.00001f);
|
||||||
|
aim.error=-2;assert(std::abs(aim.GetRandomFactor(EAISkill::CYLON,0)+1.8f)<.00001f);aim.error=1;
|
||||||
combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==2.5f&&combat.calls==0);
|
combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==2.5f&&combat.calls==0);
|
||||||
combat.m_fEquipmentPerceptionFactor=3;combat.m_fPerceptionFactor=.5f;
|
combat.m_fEquipmentPerceptionFactor=3;combat.m_fPerceptionFactor=.5f;
|
||||||
combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==3.75f);
|
combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==3.75f);
|
||||||
combat.UpdatePerceptionFactor(nullptr,&threat);assert(combat.calls==0);
|
combat.UpdatePerceptionFactor(nullptr,&threat);assert(combat.calls==0);
|
||||||
combat.bound=false;combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==.4f&&combat.calls==1);
|
combat.bound=false;combat.UpdatePerceptionFactor(&perception,&threat);assert(perception.factor==.4f&&combat.calls==1);
|
||||||
aim.GetRandomFactor(EAISkill::CYLON,0);assert(aim.sampled==EAISkill::CYLON);
|
assert(aim.GetRandomFactor(EAISkill::CYLON,0)==1);assert(aim.sampled==EAISkill::CYLON);
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: actual scoped REGULAR aim error and alert perception; preserves equipment multiplier and restores vanilla behavior when unbound')
|
print('PASS: actual scoped 10% tighter REGULAR aim error and alert perception; preserves equipment multiplier and restores vanilla behavior when unbound')
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Execute actual medicine allowance/insertion coordinator with native inventory adapters."""
|
||||||
|
import re
|
||||||
|
from policy_harness import ROOT, run
|
||||||
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyMedicine.c'
|
||||||
|
s=p.read_text();s=s[s.index('class ERR_EnemyMedicine'):]
|
||||||
|
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ERR_EnemyMedicine','struct ERR_EnemyMedicine')
|
||||||
|
s=s.replace('array<IEntity>', 'Array<IEntity*>').replace('foreach (','for (').replace('ResourceName','string')
|
||||||
|
s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory').replace('BaseInventoryStorageComponent.Cast(item.FindComponent(BaseInventoryStorageComponent))','item->bag').replace('ERR_HQ.Cast(mode.Command())','mode->Command()')
|
||||||
|
for cls in ('IEntity','ERR_GameMode','SCR_AIGroup','InventoryStorageManagerComponent','BaseInventoryStorageComponent','ERR_HQ'):
|
||||||
|
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
|
||||||
|
s=s.replace('ERR_EnemyEquipmentCallback m_Operation','ERR_EnemyEquipmentCallback* m_Operation')
|
||||||
|
for obj in ('actor','existing','item','mode','inventory','m_Operation','hq'):
|
||||||
|
s=s.replace(obj+'.',obj+'->')
|
||||||
|
s=s.replace('GetPrefabData().','GetPrefabData()->').replace('Hunters().','Hunters()->')
|
||||||
|
for ns in ('ERR_EnemyEquipmentRules','ERR_EnemyMedicalCatalog','ERR_HQ','Math','EStoragePurpose','string'):
|
||||||
|
s=s.replace(ns+'.',ns+'::')
|
||||||
|
s=s.replace('string::Format(', 'Format(').replace('null','nullptr')
|
||||||
|
s=re.sub(r'\b(bool|int|float|\w+\*) (\w+);',r'\1 \2 {};',s).replace('\n}\n','\n};\n')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
struct string:std::string {using std::string::string;string(const std::string& s):std::string(s){}bool Contains(string x){return find(x)!=npos;}void ToLower(){std::transform(begin(),end(),begin(),::tolower);}};
|
||||||
|
template<class...T>string Format(string,T...){return "event";}
|
||||||
|
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
|
||||||
|
struct Prefab{string path;string GetPrefabName(){return path;}};
|
||||||
|
struct BaseInventoryStorageComponent{};
|
||||||
|
struct InventoryStorageManagerComponent;
|
||||||
|
struct IEntity{Prefab prefab;bool owned=true;InventoryStorageManagerComponent* inventory=nullptr;BaseInventoryStorageComponent* bag=nullptr;Prefab* GetPrefabData(){return &prefab;}int GetOrigin(){return 0;}};
|
||||||
|
struct SCR_AIGroup{};
|
||||||
|
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
|
||||||
|
namespace EStoragePurpose{enum{PURPOSE_DEPOSIT};}
|
||||||
|
struct InventoryStorageManagerComponent{Array<IEntity*> items;int attempts=0,accepted=0;bool fullBag=false,full=false,async=false;ERR_EnemyEquipmentCallback* pending=nullptr;
|
||||||
|
void GetItems(Array<IEntity*>& out){out=items;}
|
||||||
|
bool TrySpawnPrefabToStorage(string prefab,BaseInventoryStorageComponent* bag,int,int,ERR_EnemyEquipmentCallback* callback){attempts++;if(full||(bag&&fullBag))return false;accepted++;auto* item=new IEntity;item->prefab.path=prefab;items.push_back(item);pending=callback;if(!async){callback->Finished=true;callback->Succeeded=true;}return true;}
|
||||||
|
};
|
||||||
|
struct Hunters{bool Contains(SCR_AIGroup*){return false;}};
|
||||||
|
struct ERR_HQ{Hunters hunters;Hunters* Hunters(){return &hunters;}static void Event(string,string){}};
|
||||||
|
struct ERR_GameMode{ERR_HQ hq;ERR_HQ* Command(){return &hq;}};
|
||||||
|
namespace ERR_EnemyEquipmentRules {bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* a){return a&&a->owned;}}
|
||||||
|
namespace ERR_EnemyMedicalCatalog {const string EPI="epi",MORPHINE="morphine";bool Medic(string p){return p.Contains("medic");}bool Specialist(string p){return p.Contains("_sf");}}
|
||||||
|
namespace Math{int RandomInt(int,int){return 0;}}
|
||||||
|
'''.replace('Hunters hunters;Hunters* Hunters()', 'struct Hunters hunters;struct Hunters* Hunters()')+s+r'''
|
||||||
|
int main(){ERR_GameMode mode;SCR_AIGroup group;IEntity actor;InventoryStorageManagerComponent inventory;actor.inventory=&inventory;actor.prefab.path="_sf_medic";
|
||||||
|
IEntity backpack;backpack.prefab.path="backpack";BaseInventoryStorageComponent bag;backpack.bag=&bag;inventory.items.push_back(&backpack);
|
||||||
|
IEntity authored;authored.prefab.path="morphine";inventory.items.push_back(&authored);
|
||||||
|
ERR_EnemyMedicine grant;inventory.fullBag=true;for(int i=0;i<20;i++)grant.Check(&mode,&group,&actor);
|
||||||
|
assert(grant.m_Done&&grant.m_Issued==11&&inventory.accepted==11&&inventory.attempts==22); // six epi plus five missing morphine; refused bag falls back once
|
||||||
|
int attempts=inventory.attempts;inventory.items.clear();for(int i=0;i<5;i++)grant.Check(&mode,&group,&actor);assert(inventory.attempts==attempts); // no refill after loot
|
||||||
|
InventoryStorageManagerComponent blocked;actor.inventory=&blocked;blocked.async=true;ERR_EnemyMedicine pending;for(int i=0;i<35;i++)pending.Check(&mode,&group,&actor);assert(pending.m_Done&&blocked.accepted==1); // pending operation quarantined, never duplicated
|
||||||
|
InventoryStorageManagerComponent unavailable;actor.inventory=&unavailable;unavailable.full=true;ERR_EnemyMedicine noSpace;noSpace.Check(&mode,&group,&actor);assert(noSpace.m_Done&&unavailable.accepted==0);
|
||||||
|
InventoryStorageManagerComponent specialStock;actor.inventory=&specialStock;actor.prefab.path="soldier_sf";
|
||||||
|
ERR_EnemyMedicine special;for(int i=0;i<8;i++)special.Check(&mode,&group,&actor);
|
||||||
|
assert(special.m_Done&&specialStock.accepted==4);int epi=0,morphine=0;
|
||||||
|
for(auto* item:specialStock.items){epi+=item->prefab.path=="epi";morphine+=item->prefab.path=="morphine";}
|
||||||
|
assert(epi==2&&morphine==2); // Every special-forces soldier has two finite injectors.
|
||||||
|
actor.owned=false;ERR_EnemyMedicine captured;captured.Check(&mode,&group,&actor);assert(!captured.m_Started);captured.Stop();actor.owned=true;captured.Check(&mode,&group,&actor);assert(!captured.m_Started);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: actual finite medicine grants, authored stock deduction, full-backpack refusal/pouch fallback, no loot replenishment, unresolved-operation quarantine and ownership/stop guards')
|
||||||
@@ -14,8 +14,9 @@ code=code.replace('ref array<','Array<').replace('array<','Array<').replace('ref
|
|||||||
code=code.replace('static const ResourceName','inline static const ResourceName').replace('void ERR_EnemyRadio(','ERR_EnemyRadio(')
|
code=code.replace('static const ResourceName','inline static const ResourceName').replace('void ERR_EnemyRadio(','ERR_EnemyRadio(')
|
||||||
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
|
code=re.sub(r'\bERR_GameMode (?=\w)','ERR_GameMode* ',code)
|
||||||
code=code.replace('foreach (','for (').replace('null','nullptr')
|
code=code.replace('foreach (','for (').replace('null','nullptr')
|
||||||
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Replication','SCR_Global','ERR_GameMode','EStoragePurpose'):
|
for name in ('ERR_EnemyEquipmentRules','ERR_HQ','Replication','SCR_Global','ERR_GameMode','EStoragePurpose','Math'):
|
||||||
code=code.replace(name+'.',name+'::')
|
code=code.replace(name+'.',name+'::')
|
||||||
|
code=code.replace('ERR_HQ hq','ERR_HQ* hq').replace('hq.Hunters().','hq->Hunters()->').replace('hq.Hunters()','hq->Hunters()')
|
||||||
for name in ('actor','inventory','item','radio','agent','m_Group','m_Operation','mode'):
|
for name in ('actor','inventory','item','radio','agent','m_Group','m_Operation','mode'):
|
||||||
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
code=re.sub(r'\b'+name+r'\.',name+'->',code)
|
||||||
code=re.sub(r'\b(bool|int|IEntity|SCR_AIGroup|ERR_EnemyEquipmentCallback\*) (\w+);',r'\1 \2{};',code)
|
code=re.sub(r'\b(bool|int|IEntity|SCR_AIGroup|ERR_EnemyEquipmentCallback\*) (\w+);',r'\1 \2{};',code)
|
||||||
@@ -61,7 +62,8 @@ struct Group{Array<Agent*>agents;int players=0,faction=1;bool slave=false,expand
|
|||||||
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;static ERR_GameMode*Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}};
|
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;static ERR_GameMode*Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}};
|
||||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||||
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
|
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
|
||||||
struct ERR_HQ{inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
|
struct Math{static int Max(int a,int b){return std::max(a,b);}};
|
||||||
|
struct ERR_HQ{static ERR_HQ* Get(){return nullptr;} ERR_HQ* Hunters(){return this;} int VehicleRadioCapability(IEntity){return 0;} inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
|
||||||
struct ERR_EnemyEquipmentRules{static bool Owned(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return Owned(m,g,a);}};
|
struct ERR_EnemyEquipmentRules{static bool Owned(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return Owned(m,g,a);}};
|
||||||
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
|
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
|
||||||
void GetItems(Array<IEntity>&out){out=items;}
|
void GetItems(Array<IEntity>&out){out=items;}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"""Execute fixes for the 26 September log failures against boundary doubles."""
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
r=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
|
||||||
|
cleanup=body(r/'ERR_HunterCrewRecovery.c','static void Complete(')
|
||||||
|
cleanup=cleanup.replace('SCR_AIVehicleBehavior behavior','Behavior* behavior').replace('IEntity actor','Actor* actor')
|
||||||
|
cleanup=cleanup.replace('SCR_ChimeraAIAgent agent = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(actor));','Agent* agent = actor->agent;')
|
||||||
|
cleanup=cleanup.replace('array<ref AIActionBase> actions = {};','Actions actions;').replace('agent.','agent->').replace('m_UtilityComponent.','m_UtilityComponent->').replace('behavior.Complete()','behavior->Complete()')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
struct Behavior {int completions=0;void Complete(){completions++;}};
|
||||||
|
struct Actions:std::vector<Behavior*>{bool Contains(Behavior* b){return std::find(begin(),end(),b)!=end();}};
|
||||||
|
struct Utility {void* m_AIInfo=this;Actions queued;void GetActions(Actions& out){out=queued;}};
|
||||||
|
struct Agent {Utility* m_UtilityComponent=nullptr;};struct Actor{Agent* agent=nullptr;};
|
||||||
|
'''+cleanup+'''
|
||||||
|
int main(){Behavior b;Actor actor;Complete(&b,nullptr);Complete(&b,&actor);assert(!b.completions);
|
||||||
|
Agent agent;actor.agent=&agent;Complete(&b,&actor);assert(!b.completions);
|
||||||
|
Utility utility;agent.m_UtilityComponent=&utility;Complete(&b,&actor);assert(!b.completions);
|
||||||
|
utility.queued.push_back(&b);utility.m_AIInfo=nullptr;Complete(&b,&actor);assert(!b.completions);
|
||||||
|
utility.m_AIInfo=&utility;Complete(&b,&actor);assert(b.completions==1);
|
||||||
|
utility.queued.clear();Complete(&b,&actor);assert(b.completions==1);}
|
||||||
|
''')
|
||||||
|
# Execute the actual HQ eligibility branch. Its success permits the existing
|
||||||
|
# radio request path, never directly a shot; per-shot checks remain separate.
|
||||||
|
tick=body(r/'ERR_MortarSystem.c','void Tick(')
|
||||||
|
a=tick.index(' vector predicted;');b=tick.index(' selected = battery;',a)
|
||||||
|
preflight=tick[a:b].replace('vector predicted;','Vec predicted;').replace('ERR_MortarPrediction.Predict','Predict').replace('battery.Trace','battery->Trace').replace('predicted.ToString()','std::string("reported")')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
struct Vec{};struct Battery{int holds=0;void Trace(std::string,std::string,std::string){holds++;}};
|
||||||
|
struct Test{bool prediction=true,safe=true;int predictions=0,safetyChecks=0,orders=0;std::string SafetyReason="friendly";
|
||||||
|
bool Predict(void*,float,float,Vec&){predictions++;return prediction;}
|
||||||
|
bool Safe(Battery*,Vec,float){safetyChecks++;return safe;}
|
||||||
|
void Tick(Battery* battery){void* report=nullptr;float now=100;for(int i=0;i<1;i++){
|
||||||
|
'''+preflight+'''orders++;}}};
|
||||||
|
int main(){Test t;Battery battery;t.prediction=false;t.Tick(&battery);assert(!t.orders&&!t.safetyChecks);
|
||||||
|
t.prediction=true;t.safe=false;t.Tick(&battery);assert(!t.orders&&battery.holds==1);
|
||||||
|
t.safe=true;t.Tick(&battery);assert(t.orders==1&&t.safetyChecks==2);}
|
||||||
|
''')
|
||||||
|
# Prevent the exact mounted Defend regression from returning: mounted escorts
|
||||||
|
# keep Move-to-cover/native vehicle combat, while infantry retain flank/search.
|
||||||
|
tactics=r/'ERR_SupplyConvoyTactics.c'
|
||||||
|
assert 'DefendWaypoint()' not in body(tactics,'protected void Support(')
|
||||||
|
assert 'orders.Add(Move(support))' in body(tactics,'protected void Support(')
|
||||||
|
assert 'SearchWaypoint()' in body(tactics,'protected void Flank(')
|
||||||
|
print('PASS: stale/deleted AI utility cannot receive Complete; mortar HQ preflight withholds unsafe orders; mounted convoy cover never creates infantry Defend')
|
||||||
|
|
||||||
|
# The native RHS fixture reproduces this boundary: the convenience API boards
|
||||||
|
# before applying a group faction. Keep production affiliation ahead of entry.
|
||||||
|
flight=r/'ERR_ReconFlightComponent.c'
|
||||||
|
prepare=body(flight,'protected bool PrepareCrew(')
|
||||||
|
assert 'SpawnCharacterInCompartment(' not in prepare
|
||||||
|
assert prepare.index('ERR_Actors.SetFaction(initialCrew,') < prepare.index('access.GetInVehicle(')
|
||||||
|
assert prepare.index('m_Crew.AddAgent(') < prepare.index('access.GetInVehicle(')
|
||||||
|
assert 'SpawnCharacterInCompartment(' not in body(flight,'void Initialize(')
|
||||||
|
finish=body(flight,'protected bool FinishCrew(')
|
||||||
|
assert finish.index('now - m_Started > 45000') < finish.index('m_Assault.Preparing()')
|
||||||
|
print('PASS: flight crew affiliation and group precede native boarding; preparation remains bounded even while passengers wait')
|
||||||
@@ -69,7 +69,7 @@ for name in ('unit', 'utility'):
|
|||||||
for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'):
|
for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'):
|
||||||
movement = movement.replace(name+'.', name+'->')
|
movement = movement.replace(name+'.', name+'->')
|
||||||
movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
|
movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
|
||||||
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event')
|
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event').replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active')
|
||||||
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
|
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
|
||||||
run(r'''
|
run(r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@@ -82,6 +82,7 @@ struct Utility {Perception* m_Perception;};
|
|||||||
struct Physics {Vec GetVelocity(){return {};}};
|
struct Physics {Vec GetVelocity(){return {};}};
|
||||||
struct Vehicle {Vec pos;Physics physics;Vec GetOrigin(){return pos;}Physics* GetPhysics(){return &physics;}};
|
struct Vehicle {Vec pos;Physics physics;Vec GetOrigin(){return pos;}Physics* GetPhysics(){return &physics;}};
|
||||||
struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}void AddWaypoint(void*){}};
|
struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}void AddWaypoint(void*){}};
|
||||||
|
namespace ERR_HunterCombat {bool fresh=false;bool Active(Group*){return fresh;}};
|
||||||
struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}};
|
struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}};
|
||||||
struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}};
|
struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}};
|
||||||
Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;}
|
Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;}
|
||||||
@@ -96,8 +97,8 @@ int main(){
|
|||||||
ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3);
|
ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3);
|
||||||
ObserveMovement(&hunter,240001);assert(hunter.Repaths==3);
|
ObserveMovement(&hunter,240001);assert(hunter.Repaths==3);
|
||||||
vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0);
|
vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0);
|
||||||
perception.m_aTargets.empty=false;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
|
perception.m_aTargets.empty=false;ERR_HunterCombat::fresh=true;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
|
||||||
perception.m_aTargets.empty=true;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
|
ERR_HunterCombat::fresh=false;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
|
||||||
ObserveMovement(&hunter,460000);assert(hunter.Repaths==1);
|
ObserveMovement(&hunter,460000);assert(hunter.Repaths==1);
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
|
|||||||
@@ -2,23 +2,27 @@
|
|||||||
import re
|
import re
|
||||||
from policy_harness import ROOT,body,run
|
from policy_harness import ROOT,body,run
|
||||||
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c'
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c'
|
||||||
s=body(p,'ERR_PrepareResult Tick(')
|
s=body(p,'protected bool SkipOptional(')+'\n'+body(p,'ERR_PrepareResult Tick(')
|
||||||
s=s.replace('ERR_SiteDefinition site','Site* site').replace('site.','site->').replace('m_Terrain.Site','m_Terrain.SiteAt')
|
s=s.replace('ERR_SiteDefinition site','Site* site').replace('site.','site->').replace('m_Terrain.Site','m_Terrain.SiteAt')
|
||||||
s=s.replace('System.GetTickCount','Clock::Now').replace('Replication.IsServer','Server').replace('Math.RandomFloat','RandomFloat').replace('ERR_PrepareResult.','Result::')
|
s=s.replace('System.GetTickCount','Clock::Now').replace('Replication.IsServer','Server').replace('Math.RandomFloat','RandomFloat').replace('ERR_PrepareResult.','Result::')
|
||||||
s=s.replace('float floor;','float floor=0;')
|
s=s.replace('float floor;','float floor=0;').replace('protected ', '').replace('ref ERR_SiteDefinition retained', 'Site* retained').replace('retained.', 'retained->').replace('m_Definition.', 'm_Definition->').replace('ERR_LocationKind.', 'Kind::')
|
||||||
run(r'''
|
run(r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
using string=std::string;
|
using string=std::string;
|
||||||
struct vector{float a[3];float& operator[](int i){return a[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}
|
struct vector{float a[3];float& operator[](int i){return a[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}
|
||||||
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
|
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
|
||||||
namespace Clock {int now=0,step=1;int Now(){int t=now;now+=step;return t;}}
|
namespace Clock {int now=0,step=1;int Now(){int t=now;now+=step;return t;}}
|
||||||
bool Server(){return true;}float RandomFloat(float a,float b){return (a+b)/2;}template<class...T>void PrintFormat(T...){}
|
bool Server(){return true;}float RandomFloat(float a,float b){return (a+b)/2;}template<class...T>void PrintFormat(T...){}
|
||||||
struct Site{vector Position=Vector(10,0,20);float Heading=0;bool PlacementResolved=false;string Id="site";};
|
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
||||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}};
|
struct Site{int Kind=Kind::PRISON;bool required=false;string Label="Test site";vector Position=Vector(10,0,20);float Heading=0;bool PlacementResolved=false;string Id="site";};
|
||||||
struct Terrain{bool clear=true;int queries=0;bool SiteAt(Site*,vector,float,float& height){queries++;height=42;return clear;}};
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}int Find(T v){auto i=std::find(this->begin(),this->end(),v);return i==this->end()?-1:i-this->begin();}void Remove(int i){this->erase(this->begin()+i);}void InsertAt(T v,int i){this->insert(this->begin()+i,v);}};
|
||||||
|
struct Definition{Array<Site*> Sites;Site* required=nullptr;bool Valid(){return !required||Sites.Find(required)>=0;}};
|
||||||
|
struct Terrain{bool clear=true;Site* refused=nullptr;int queries=0;bool SiteAt(Site* site,vector,float,float& height){queries++;height=42;return clear&&site!=refused;}};
|
||||||
struct Scheduler{
|
struct Scheduler{
|
||||||
|
Definition definition;Definition* m_Definition=&definition;
|
||||||
bool m_Started=true;ERR_PrepareResult m_Result=Result::PENDING;int m_Index=0,m_Attempts=0,m_Began=0;
|
bool m_Started=true;ERR_PrepareResult m_Result=Result::PENDING;int m_Index=0,m_Attempts=0,m_Began=0;
|
||||||
vector m_Low=Vector(0,0,0),m_High=Vector(10000,0,10000);Array<Site*>m_Pending;Terrain m_Terrain;string m_Error;
|
vector m_Low=Vector(0,0,0),m_High=Vector(10000,0,10000);Array<Site*>m_Pending;Terrain m_Terrain;string m_Error;
|
||||||
bool Separated(Site*,vector,bool){return true;}
|
bool Separated(Site*,vector,bool){return true;}
|
||||||
@@ -31,6 +35,28 @@ int main(){
|
|||||||
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Terrain.clear=false;assert(s.Tick()==Result::PENDING);assert(s.m_Terrain.queries<10&&!a.PlacementResolved);Clock::now=30001;assert(s.Tick()==Result::FAILED&&!s.m_Error.empty()&&!a.PlacementResolved);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::FAILED&&s.m_Terrain.queries==calls);}
|
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Terrain.clear=false;assert(s.Tick()==Result::PENDING);assert(s.m_Terrain.queries<10&&!a.PlacementResolved);Clock::now=30001;assert(s.Tick()==Result::FAILED&&!s.m_Error.empty()&&!a.PlacementResolved);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::FAILED&&s.m_Terrain.queries==calls);}
|
||||||
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Attempts=12000;assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20&&a.PlacementResolved);}
|
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Attempts=12000;assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20&&a.PlacementResolved);}
|
||||||
{Clock::now=30001;Scheduler s;Site a;s.m_Pending={&a};assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20);}
|
{Clock::now=30001;Scheduler s;Site a;s.m_Pending={&a};assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20);}
|
||||||
|
// Each optional type can fail after exhausting candidates without blocking the next site.
|
||||||
|
for(int kind : {Kind::OUTPOST,Kind::HOSPITAL,Kind::DEPOT}){
|
||||||
|
Clock::now=0;Scheduler s;Site a,b;a.Kind=kind;a.Id="unplaceable";b.Id="next";
|
||||||
|
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;s.m_Attempts=12000;
|
||||||
|
while(s.Tick()==Result::PENDING){}
|
||||||
|
assert(s.m_Result==Result::READY&&s.m_Error.empty()&&!a.PlacementResolved&&b.PlacementResolved);
|
||||||
|
assert(s.definition.Sites.size()==1&&s.definition.Sites[0]==&b);
|
||||||
|
int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);
|
||||||
|
}
|
||||||
|
// A deadline skip gets a fresh candidate/time budget for the following optional site.
|
||||||
|
{Clock::now=30001;Scheduler s;Site a,b;a.Kind=Kind::OUTPOST;b.Kind=Kind::HOSPITAL;
|
||||||
|
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;
|
||||||
|
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&b.Position[0]!=10&&b.PlacementResolved);}
|
||||||
|
// Required objectives (including mortars) still fail; no unsafe terrain is accepted.
|
||||||
|
for(int kind : {Kind::PRISON,Kind::RADIO,Kind::EXTRACTION,Kind::MORTAR}){
|
||||||
|
Clock::now=0;Scheduler s;Site a;a.Kind=kind;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
||||||
|
assert(s.Tick()==Result::FAILED&&s.definition.Sites[0]==&a&&!a.PlacementResolved);
|
||||||
|
}
|
||||||
|
// Optional by type can still be indispensable for minimum accessible radio intel.
|
||||||
|
{Clock::now=0;Scheduler s;Site before,a,after;a.Kind=Kind::HOSPITAL;
|
||||||
|
s.definition.Sites={&before,&a,&after};s.definition.required=&a;s.m_Pending={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
||||||
|
assert(s.Tick()==Result::FAILED&&s.definition.Sites.size()==3&&s.definition.Sites[1]==&a&&!a.PlacementResolved);}
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, and terminates without repeated searches')
|
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, skips only dispensable sites, preserves required objectives/intel, and resets the next search budget')
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ struct RplId {static int Invalid(){return -1;}};
|
|||||||
struct Game {int spawns=0;SCR_ManualCamera* SpawnEntityPrefab(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
struct Game {int spawns=0;SCR_ManualCamera* SpawnEntityPrefab(Resource*,int,EntitySpawnParams){spawns++;return &camera;}int GetWorld(){return 1;}CameraManager* GetCameraManager(){return &manager;}} game;
|
||||||
Game* GetGame(){return &game;}
|
Game* GetGame(){return &game;}
|
||||||
struct Controller {
|
struct Controller {
|
||||||
|
void ERR_UpdateRecoveryCamera(ERR_GameMode*){}
|
||||||
int nativeChanges=0,starts=0;void ERR_StartGuide(){starts++;}
|
int nativeChanges=0,starts=0;void ERR_StartGuide(){starts++;}
|
||||||
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;
|
int m_ERRGuidePhase=-1;string m_ERRGuideMessage;
|
||||||
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
|
Actor* controlled=&actor;int m_ERRObserverReason=ERR_ObserverReason::NONE;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ H=BASE/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
|||||||
A=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
|
A=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQAmmoSampler.c'
|
||||||
|
|
||||||
def cpp(s):
|
def cpp(s):
|
||||||
s=s.replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
|
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('string.Format','Format')
|
||||||
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
|
for t in ('ERR_HQStatusReport','ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQRoute'):
|
||||||
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
||||||
for v in ('stock','report','unit','route'):
|
for v in ('stock','report','unit','route'):
|
||||||
@@ -45,10 +45,12 @@ struct ERR_HQRoute{int MessageId=0;string Payload;};
|
|||||||
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
|
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
|
||||||
struct Journal{int seq=0,writes=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
|
struct Journal{int seq=0,writes=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
|
||||||
struct Task{float ArrivedAt=0;};
|
struct Task{float ArrivedAt=0;};
|
||||||
struct ERR_HQUnit{struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
|
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
|
||||||
|
struct ERR_HQUnit{float NextContact=0;struct Task* Task=nullptr;void* Radio=this;ERR_HQRoute* Uplink=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;
|
||||||
float NextStatus=0;ERR_HQAmmoReporting AmmoReporting;Sampler StatusSampler;bool visible=true,contact=false,sweep=false;
|
float NextStatus=0;ERR_HQAmmoReporting AmmoReporting;Sampler StatusSampler;bool visible=true,contact=false,sweep=false;
|
||||||
ERR_HQUnit(){AmmoReporting.Initialize(6,0);}};
|
ERR_HQUnit(){AmmoReporting.Initialize(6,0);}};
|
||||||
struct HQ{Network m_Network;Journal m_Journal;int delivered=0;
|
struct HQ{void ContactReport(ERR_HQUnit*,float){}
|
||||||
|
Network m_Network;Journal m_Journal;int delivered=0;
|
||||||
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
||||||
void Observe(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&u->visible&&m_Network.available){u->contact=true;u->Uplink=new ERR_HQRoute;}}
|
void Observe(ERR_HQUnit* u,float){if(!u->Uplink&&u->Radio&&u->visible&&m_Network.available){u->contact=true;u->Uplink=new ERR_HQRoute;}}
|
||||||
void ObserveBody(ERR_HQUnit*,float){}
|
void ObserveBody(ERR_HQUnit*,float){}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""Execute the actual local evidence cache and priority/radio intake methods."""
|
||||||
|
import re
|
||||||
|
from policy_harness import ROOT,body,run
|
||||||
|
base=ROOT/'addons/core/Scripts/Game/ERR/10_AI'
|
||||||
|
s=(base/'ERR_HQLocalAwareness.c').read_text().split('modded class')[0]
|
||||||
|
h=(base/'ERR_HQLocalHandoff.c').read_text().split('class ERR_HQObservationHistory')[1]
|
||||||
|
h='class ERR_HQObservationHistory'+h
|
||||||
|
q='\n'.join(body(base/'ERR_HQ.c',sig) for sig in ['protected void ContactReport(', 'protected void QueueReports('])
|
||||||
|
def trans(s):
|
||||||
|
s=s.replace('protected ','').replace('ref ','').replace('class ','struct ').replace('\n}\n','\n};\n')
|
||||||
|
s=s.replace('array<ERR_HQReport>', 'Array<ERR_HQReport*>').replace('array<IEntity>','Array<IEntity>').replace('array<float>','Array<float>')
|
||||||
|
for t in ['ERR_HQReport','ERR_HQObservationHistory','ERR_HQUnit','ERR_HQRoute','SCR_AIGroup']:
|
||||||
|
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
|
||||||
|
s=s.replace('foreach (','for (').replace('null','nullptr')
|
||||||
|
for c in ['ERR_ForceRules','ERR_HQMemoryRules','ERR_HQLocalAwareness','Math']:
|
||||||
|
s=s.replace(c+'.',c+'::')
|
||||||
|
for c in ['known','report','source','history','unit','route']:
|
||||||
|
s=re.sub(r'\b'+c+r'\.',c+'->',s)
|
||||||
|
s=s.replace('m_Sound.', 'm_Sound->').replace('m_Visual[i].','m_Visual[i]->')
|
||||||
|
s=s.replace('unit->LocalAwareness.','unit->LocalAwareness->').replace('unit->SentObservations.','unit->SentObservations->')
|
||||||
|
s=s.replace('m_Mode.Settings().Runtime','m_Mode.Settings().Runtime')
|
||||||
|
s=s.replace('ERR_HQReport* m_Sound;', 'ERR_HQReport* m_Sound=nullptr;').replace('float m_NextSound;', 'float m_NextSound=0;')
|
||||||
|
return s.replace('unit->Task.', 'unit->Task->')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
using string=std::string;
|
||||||
|
struct Actor{};using IEntity=Actor*;
|
||||||
|
struct SCR_AIGroup:Actor{};
|
||||||
|
struct vector{float v[3]={0,0,0};float& operator[](int i){return v[i];}};
|
||||||
|
vector Vector(float x,float y,float z){return {{x,y,z}};}
|
||||||
|
namespace Math{float Floor(float f){return std::floor(f);}}
|
||||||
|
namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}}
|
||||||
|
namespace ERR_HQMemoryRules{bool PositionValid(vector p){return std::isfinite(p[0])&&std::isfinite(p[1])&&std::isfinite(p[2]);}}
|
||||||
|
template<class T>struct Array:std::vector<T>{void Clear(){this->clear();}int Count(){return this->size();}
|
||||||
|
void Insert(T x){this->push_back(x);}void Remove(int i){this->erase(this->begin()+i);}int Find(T x){auto p=std::find(this->begin(),this->end(),x);return p==this->end()?-1:p-this->begin();}};
|
||||||
|
struct ERR_HQReport{IEntity Target=nullptr;SCR_AIGroup* Source=nullptr;vector Position;float ObservedAt=0;
|
||||||
|
int MessageId=0;string TravelMode="unknown";string Weapon="unknown";
|
||||||
|
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}string Description(float){return TravelMode;}};
|
||||||
|
'''+trans(h)+trans(s)+r'''
|
||||||
|
struct ERR_HQRoute{int MessageId=0;string Payload;};
|
||||||
|
struct Task{float ArrivedAt=0;};
|
||||||
|
struct ERR_HQUnit{SCR_AIGroup* Group=nullptr;IEntity Radio=nullptr;ERR_HQLocalAwareness own;
|
||||||
|
ERR_HQLocalAwareness* LocalAwareness=&own;ERR_HQObservationHistory hist;ERR_HQObservationHistory* SentObservations=&hist;
|
||||||
|
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;struct Task* Task=nullptr;
|
||||||
|
float NextContact=0,NextReport=0,NextStatus=0;bool status=false;};
|
||||||
|
struct Settings{int Runtime(string){return 10;}};
|
||||||
|
struct Mode{struct Settings Settings(){return {};}};
|
||||||
|
struct Journal{int id=0;int Message(){return ++id;}void Write(string,string,int){}};
|
||||||
|
struct HQ{Mode m_Mode;Journal m_Journal;bool network=true,occupied=false;
|
||||||
|
ERR_HQRoute* ObservationRoute(ERR_HQUnit*){return network?new ERR_HQRoute:nullptr;}
|
||||||
|
bool OriginOccupied(IEntity,ERR_HQUnit*){return occupied;}
|
||||||
|
void StatusReport(ERR_HQUnit* u,float now){if(!u->Uplink&&now>=u->NextStatus){u->Uplink=new ERR_HQRoute;u->status=true;u->NextStatus=now+180000;}}
|
||||||
|
void Observe(ERR_HQUnit*,float){} void ObserveBody(ERR_HQUnit*,float){} void Sweep(ERR_HQUnit*,float){}
|
||||||
|
'''+trans(q)+r'''
|
||||||
|
};
|
||||||
|
int main(){
|
||||||
|
SCR_AIGroup group;Actor enemy;ERR_HQLocalAwareness cache;ERR_HQObservationHistory history;
|
||||||
|
ERR_HQReport r;r.Target=&enemy;r.Source=&group;r.Position=Vector(20,0,40);r.ObservedAt=100;r.Weapon="rifle";
|
||||||
|
cache.Remember(&r,100);r.Position[0]=900;r.Weapon="launcher";
|
||||||
|
auto held=cache.Peek(1200,&history);assert(held&&held->Position[0]==20&&held->Weapon=="rifle"&&held->ObservedAt==100);
|
||||||
|
assert(cache.Peek(30100,&history)&&!cache.Peek(30101,&history)); // original time, never refresh on reads
|
||||||
|
r.ObservedAt=32000;cache.Remember(&r,32000);assert(!cache.CaptureDue(&enemy,32999)&&cache.CaptureDue(&enemy,33000));
|
||||||
|
held=cache.Peek(33000,&history);history.Queued(held->Target,held->ObservedAt,33000);cache.Queued(held,33000);
|
||||||
|
cache.Remember(&r,33000);assert(!cache.Peek(33000,&history)); // same observation cannot replay
|
||||||
|
cache.Hear(&group,Vector(12,8,-1),34000,34000);auto sound=cache.Peek(34000,&history);
|
||||||
|
assert(sound&&!sound->Target&&sound->Source==&group&&sound->Position[0]==75&&sound->Position[2]==-75);
|
||||||
|
assert(sound->TravelMode=="heard gunfire"&&sound->Weapon=="unknown");
|
||||||
|
cache.Queued(sound,34000);cache.Hear(&group,Vector(800,0,0),35000,35000);
|
||||||
|
assert(!cache.Peek(63999,&history)&&cache.Peek(64000,&history));
|
||||||
|
cache.Clear();assert(!cache.Peek(64000,&history));
|
||||||
|
Actor targets[40];for(int i=0;i<40;i++){r.Target=&targets[i];r.ObservedAt=70000+i;cache.Remember(&r,70000+i);}
|
||||||
|
assert(cache.m_Visual.Count()==16);cache.Clear();
|
||||||
|
HQ h;ERR_HQUnit u;u.Group=&group;r.Target=&enemy;r.ObservedAt=1000;
|
||||||
|
u.own.Remember(&r,1000);h.QueueReports(&u,2000);assert(u.PendingReport&&!u.status&&u.PendingReport->ObservedAt==1000);
|
||||||
|
auto inFlight=u.Uplink;r.ObservedAt=3000;u.own.Remember(&r,3000);h.QueueReports(&u,4000);assert(u.Uplink==inFlight);
|
||||||
|
u.Uplink=nullptr;u.PendingReport=nullptr;r.ObservedAt=16000;u.own.Remember(&r,16000);
|
||||||
|
h.QueueReports(&u,16000);assert(u.status&&!u.PendingReport); // bounded status delay under continuous contacts
|
||||||
|
u.Uplink=nullptr;u.status=false;h.QueueReports(&u,17000);assert(u.PendingReport&&u.PendingReport->ObservedAt==16000);
|
||||||
|
u.Uplink=nullptr;u.PendingReport=nullptr;r.ObservedAt=23000;u.own.Remember(&r,23000);
|
||||||
|
h.network=false;h.ContactReport(&u,23000);assert(!u.Uplink&&u.own.Peek(24000,&u.hist));
|
||||||
|
h.network=true;h.ContactReport(&u,24000);assert(!u.Uplink); // failed-route retry bounded
|
||||||
|
h.ContactReport(&u,28000);assert(u.PendingReport&&u.PendingReport->ObservedAt==23000);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: captured sighting retention, immutable details, expiry, replay rejection, bounded sound uncertainty/cooldown, radio loss, priority and overdue-status fairness')
|
||||||
@@ -83,7 +83,7 @@ int main(){
|
|||||||
// Per-source watermark rejects duplicate/replayed/future/stale sightings including failed-route attempts.
|
// Per-source watermark rejects duplicate/replayed/future/stale sightings including failed-route attempts.
|
||||||
{ERR_HQObservationHistory history;IEntity target;assert(history.CanQueue(&target,100,100));history.Queued(&target,100,100);
|
{ERR_HQObservationHistory history;IEntity target;assert(history.CanQueue(&target,100,100));history.Queued(&target,100,100);
|
||||||
assert(!history.CanQueue(&target,100,101)&&!history.CanQueue(&target,99,101));assert(history.CanQueue(&target,101,101));
|
assert(!history.CanQueue(&target,100,101)&&!history.CanQueue(&target,99,101));assert(history.CanQueue(&target,101,101));
|
||||||
assert(!history.CanQueue(&target,102,101)&&!history.CanQueue(&target,101,602));
|
assert(!history.CanQueue(&target,102,101)&&history.CanQueue(&target,101,602)&&history.CanQueue(&target,101,30101)&&!history.CanQueue(&target,101,30102));
|
||||||
assert(!history.CanQueue(&target,std::numeric_limits<float>::quiet_NaN(),602));}
|
assert(!history.CanQueue(&target,std::numeric_limits<float>::quiet_NaN(),602));}
|
||||||
{ERR_HQObservationHistory history;IEntity targets[65];for(int i=0;i<64;i++){history.Queued(&targets[i],100,100);}assert(!history.CanQueue(&targets[64],100,100));
|
{ERR_HQObservationHistory history;IEntity targets[65];for(int i=0;i<64;i++){history.Queued(&targets[i],100,100);}assert(!history.CanQueue(&targets[64],100,100));
|
||||||
assert(history.CanQueue(&targets[64],120101,120101));assert(!history.CanQueue(&targets[0],100,120101));}
|
assert(history.CanQueue(&targets[64],120101,120101));assert(!history.CanQueue(&targets[0],100,120101));}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
|||||||
from policy_harness import ROOT,body,run
|
from policy_harness import ROOT,body,run
|
||||||
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
p=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
|
||||||
s='\n'.join(body(p,x) for x in ('protected bool OriginOccupied(', 'protected ERR_HQRoute ObservationRoute(', 'protected void Observe(', 'protected void QueueReports('))
|
s='\n'.join(body(p,x) for x in ('protected bool OriginOccupied(', 'protected ERR_HQRoute ObservationRoute(', 'protected void Observe(', 'protected void QueueReports('))
|
||||||
s=s.replace('protected ','').replace('array<IEntity>','Arr<IEntity>').replace('array<ref ERR_HQVisualContact>','Arr<ERR_HQVisualContact*>').replace('foreach (','for (')
|
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('array<IEntity>','Arr<IEntity>').replace('array<ref ERR_HQVisualContact>','Arr<ERR_HQVisualContact*>').replace('foreach (','for (')
|
||||||
for name in ('ERR_HQUnit','ERR_HQRoute','ERR_HQReport','ERR_HQVisualContact'):
|
for name in ('ERR_HQUnit','ERR_HQRoute','ERR_HQReport','ERR_HQVisualContact'):
|
||||||
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
s=re.sub(r'\b'+name+r' (\w+)',name+r'* \1',s)
|
||||||
for name in ('unit','other','route','own','seen','report'):
|
for name in ('unit','other','route','own','seen','report'):
|
||||||
@@ -33,12 +33,14 @@ struct ERR_HQReport{int MessageId=0,Position=0;IEntity Target=nullptr;Group*Sour
|
|||||||
struct Settings{int Runtime(string){return 5;}};
|
struct Settings{int Runtime(string){return 5;}};
|
||||||
struct Mode{struct Settings values;struct Settings*Settings(){return &values;}};
|
struct Mode{struct Settings values;struct Settings*Settings(){return &values;}};
|
||||||
struct Task{float ArrivedAt=0;};
|
struct Task{float ArrivedAt=0;};
|
||||||
struct ERR_HQUnit{struct Group*Group=nullptr;struct Utility*Utility=nullptr;IEntity Radio=nullptr;float NextReport=0;ERR_HQRoute*Uplink=nullptr;ERR_HQReport*PendingReport=nullptr;Selector ReportSelection;History SentObservations;int StatusSampler=1;struct Task*Task=nullptr;};
|
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
|
||||||
|
struct ERR_HQUnit{float NextContact=0,NextStatus=0;struct Group*Group=nullptr;struct Utility*Utility=nullptr;IEntity Radio=nullptr;float NextReport=0;ERR_HQRoute*Uplink=nullptr;ERR_HQReport*PendingReport=nullptr;Selector ReportSelection;History SentObservations;int StatusSampler=1;struct Task*Task=nullptr;};
|
||||||
struct Network{ERR_HQRoute*own=nullptr,*local=nullptr;IEntity ownCarrier=nullptr;int attempts=0;Arr<IEntity>lastBusy;
|
struct Network{ERR_HQRoute*own=nullptr,*local=nullptr;IEntity ownCarrier=nullptr;int attempts=0;Arr<IEntity>lastBusy;
|
||||||
ERR_HQRoute*Route(IEntity caller){return caller==ownCarrier?own:nullptr;}
|
ERR_HQRoute*Route(IEntity caller){return caller==ownCarrier?own:nullptr;}
|
||||||
ERR_HQRoute*LocalRoute(Group*source,int,Arr<IEntity>&busy){attempts++;lastBusy=busy;if(!local||busy.Contains(local->Nodes[0]->Carrier))return nullptr;local->LocalSource=source;return local;}};
|
ERR_HQRoute*LocalRoute(Group*source,int,Arr<IEntity>&busy){attempts++;lastBusy=busy;if(!local||busy.Contains(local->Nodes[0]->Carrier))return nullptr;local->LocalSource=source;return local;}};
|
||||||
struct Journal{int next=0,reports=0;int Message(){return ++next;}void Write(string kind,string,int){if(kind=="REPORT")reports++;}};
|
struct Journal{int next=0,reports=0;int Message(){return ++next;}void Write(string kind,string,int){if(kind=="REPORT")reports++;}};
|
||||||
struct HQ{Mode mode;Mode*m_Mode=&mode;Network m_Network;Journal m_Journal;Arr<ERR_HQUnit*>m_Units;int statuses=0,bodies=0,sweeps=0;
|
struct HQ{void ContactReport(ERR_HQUnit*,float){}
|
||||||
|
Mode mode;Mode*m_Mode=&mode;Network m_Network;Journal m_Journal;Arr<ERR_HQUnit*>m_Units;int statuses=0,bodies=0,sweeps=0;
|
||||||
void StatusReport(ERR_HQUnit*,float){statuses++;}void ObserveBody(ERR_HQUnit*,float){bodies++;}void Sweep(ERR_HQUnit*,float){sweeps++;}
|
void StatusReport(ERR_HQUnit*,float){statuses++;}void ObserveBody(ERR_HQUnit*,float){bodies++;}void Sweep(ERR_HQUnit*,float){sweeps++;}
|
||||||
''' + s.replace('static void Describe(auto*,auto&,float)','template<class T,class A>static void Describe(T*,A&,float)') + r'''
|
''' + s.replace('static void Describe(auto*,auto&,float)','template<class T,class A>static void Describe(T*,A&,float)') + r'''
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -132,6 +132,26 @@ int main(){
|
|||||||
assert(memory.Get(&a)->SampleCount==8&&memory.Get(&a)->BeliefRevision==12&&memory.Get(&a)->HasVelocity);
|
assert(memory.Get(&a)->SampleCount==8&&memory.Get(&a)->BeliefRevision==12&&memory.Get(&a)->HasVelocity);
|
||||||
auto old=Report(13,&a,23000);assert(!memory.Ingest(&old,25000)&&memory.Get(&a)->BeliefRevision==12);
|
auto old=Report(13,&a,23000);assert(!memory.Ingest(&old,25000)&&memory.Get(&a)->BeliefRevision==12);
|
||||||
auto jump=Report(14,&a,26000,{10000,0,0});assert(memory.Ingest(&jump,26000)&&!memory.Get(&a)->HasVelocity);
|
auto jump=Report(14,&a,26000,{10000,0,0});assert(memory.Ingest(&jump,26000)&&!memory.Get(&a)->HasVelocity);
|
||||||
|
// A near-simultaneous second observer must not erase credible motion
|
||||||
|
// history. Use the nearest sample at least two seconds old, never a live
|
||||||
|
// entity position or an older convenient sample across a mode change.
|
||||||
|
memory.Clear();
|
||||||
|
auto spaced=Report(1,&a,1000,{10,0,20});assert(memory.Ingest(&spaced,1000));
|
||||||
|
auto closeA=Report(2,&a,4000,{10,0,20});assert(memory.Ingest(&closeA,4000));
|
||||||
|
auto closeB=Report(3,&a,4500,{10,0,20});assert(memory.Ingest(&closeB,4500));
|
||||||
|
assert(memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Speed,0));
|
||||||
|
auto moving=Report(4,&a,5000,{18,0,20});assert(memory.Ingest(&moving,5000));
|
||||||
|
assert(memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Speed,2));
|
||||||
|
auto boarded=Report(5,&a,5500,{18,0,20},"vehicle");assert(memory.Ingest(&boarded,5500));
|
||||||
|
assert(!memory.Get(&a)->HasVelocity);
|
||||||
|
auto dismounted=Report(6,&a,6000,{18,0,20});assert(memory.Ingest(&dismounted,6000));
|
||||||
|
assert(!memory.Get(&a)->HasVelocity); // do not bridge the intervening vehicle observation
|
||||||
|
auto farJump=Report(7,&a,8000,{10000,0,0});assert(memory.Ingest(&farJump,8000));
|
||||||
|
assert(!memory.Get(&a)->HasVelocity); // do not skip an implausible eligible pair
|
||||||
|
memory.Clear();
|
||||||
|
auto rapidA=Report(1,&a,1000);assert(memory.Ingest(&rapidA,1000));
|
||||||
|
auto rapidB=Report(2,&a,1500);assert(memory.Ingest(&rapidB,1500));
|
||||||
|
assert(!memory.Get(&a)->HasVelocity); // insufficient time baseline still refuses motion
|
||||||
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
|
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
|
||||||
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
|
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
|
||||||
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
|
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
|
||||||
@@ -139,6 +159,18 @@ int main(){
|
|||||||
over.ObservedAt=10000;assert(memory.Ingest(&over,10000)&&memory.Count()==64);
|
over.ObservedAt=10000;assert(memory.Ingest(&over,10000)&&memory.Count()==64);
|
||||||
auto weak=Report(66,&people[65],10000);weak.Distress=true;assert(!memory.Ingest(&weak,10000));
|
auto weak=Report(66,&people[65],10000);weak.Distress=true;assert(!memory.Ingest(&weak,10000));
|
||||||
assert(!memory.At(-1)&&!memory.At(64)&&!memory.Get(nullptr));
|
assert(!memory.At(-1)&&!memory.At(64)&&!memory.Get(nullptr));
|
||||||
|
// Heard fire is source-keyed, imprecise, short-lived, never a precision target.
|
||||||
|
memory.Clear();auto sound=Report(1,nullptr,1000,{75,0,225},"heard gunfire");sound.Source=&friendly;
|
||||||
|
assert(memory.Ingest(&sound,1000));auto noise=memory.At(0);
|
||||||
|
assert(noise->Kind==ERR_HQMemoryKind::GUNFIRE&&!noise->Target&&!noise->HasVelocity&&!noise->Actionable());
|
||||||
|
assert(near(noise->Radius,150)&&near(noise->Confidence,.4));
|
||||||
|
int soundId=noise->BeliefId;assert(memory.NegativeSearch(soundId,1,1000));
|
||||||
|
assert(near(memory.GetById(soundId)->Confidence,.26));
|
||||||
|
sound.MessageId=2;sound.ObservedAt=3000;sound.Position={375,0,225};assert(memory.Ingest(&sound,3000));
|
||||||
|
assert(memory.Count()==1&&!memory.At(0)->HasVelocity&&memory.At(0)->Target==nullptr);
|
||||||
|
auto visual=Report(3,&a,3000,{400,0,210});assert(memory.Ingest(&visual,3000));
|
||||||
|
assert(memory.Count()==2&&memory.Get(&a)->Actionable()&&near(memory.Get(&a)->Confidence,1));
|
||||||
|
memory.Tick(63000);assert(near(memory.GetById(soundId)->Confidence,.2));
|
||||||
// The global replay table is bounded even when valid observation IDs arrive rapidly.
|
// The global replay table is bounded even when valid observation IDs arrive rapidly.
|
||||||
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
|
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
|
||||||
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
|
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ legacy='Observe(unit, now);\n ObserveBody(unit, now);\n StatusReport(unit, n
|
|||||||
assert legacy in original or 'QueueReports(unit, now);' in original
|
assert legacy in original or 'QueueReports(unit, now);' in original
|
||||||
|
|
||||||
def cpp(s):
|
def cpp(s):
|
||||||
s=s.replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
|
s=s.replace('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
|
||||||
for t in ('ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
|
for t in ('ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
|
||||||
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
|
||||||
s=s.replace('ERR_HQRoute* route;', 'ERR_HQRoute* route=nullptr;')
|
s=s.replace('ERR_HQRoute* route;', 'ERR_HQRoute* route=nullptr;')
|
||||||
@@ -62,7 +62,8 @@ struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes
|
|||||||
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
|
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
|
||||||
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
|
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
|
||||||
struct History{bool CanQueue(void*,float,float){return true;}void Queued(void*,float,float){}};
|
struct History{bool CanQueue(void*,float,float){return true;}void Queued(void*,float,float){}};
|
||||||
struct ERR_HQUnit{History SentObservations;int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
|
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
|
||||||
|
struct ERR_HQUnit{float NextContact=0;History SentObservations;int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
|
||||||
struct Group* Group=&ownGroup;struct Utility* Utility=&ownUtility;void* Radio=this;
|
struct Group* Group=&ownGroup;struct Utility* Utility=&ownUtility;void* Radio=this;
|
||||||
Selection* ReportSelection=&selection;Sampler* StatusSampler=&sampler;
|
Selection* ReportSelection=&selection;Sampler* StatusSampler=&sampler;
|
||||||
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQSearchReport* PendingSweep=nullptr;
|
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQSearchReport* PendingSweep=nullptr;
|
||||||
@@ -71,7 +72,8 @@ struct ERR_HQUnit{History SentObservations;int Id=1;struct Group ownGroup;struct
|
|||||||
ERR_HQTask* Task=nullptr;void* Assignment=nullptr;void* Waypoint=nullptr;
|
ERR_HQTask* Task=nullptr;void* Assignment=nullptr;void* Waypoint=nullptr;
|
||||||
bool Eligible(Mode*){return eligible;}
|
bool Eligible(Mode*){return eligible;}
|
||||||
};
|
};
|
||||||
struct HQ{Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
|
struct HQ{void ContactReport(ERR_HQUnit*,float){}
|
||||||
|
Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
|
||||||
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
|
||||||
ERR_HQRoute* ObservationRoute(ERR_HQUnit* u){return u->Radio?m_Network.Route(u->Radio):nullptr;}
|
ERR_HQRoute* ObservationRoute(ERR_HQUnit* u){return u->Radio?m_Network.Route(u->Radio):nullptr;}
|
||||||
bool Fighting(ERR_HQUnit* u){return u->fighting;}
|
bool Fighting(ERR_HQUnit* u){return u->fighting;}
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ struct BodyAdapter{int confirmations=0;void ConfirmDelivered(void*){confirmation
|
|||||||
struct CoverageAdapter{int forgotten=0;void Forget(vector){forgotten++;}};
|
struct CoverageAdapter{int forgotten=0;void Forget(vector){forgotten++;}};
|
||||||
struct JournalAdapter{void Write(string,string,int){}};
|
struct JournalAdapter{void Write(string,string,int){}};
|
||||||
template<class... Args> string Format(string,Args...){return "delivered";}
|
template<class... Args> string Format(string,Args...){return "delivered";}
|
||||||
struct HQDelivery{
|
struct HQDelivery{float m_NextAssignment=5000;
|
||||||
Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;struct RecoveryAdapter{int calls=0;void ReportBody(ERR_HQReport*){calls++;}} m_Recovery;CoverageAdapter m_Coverage;JournalAdapter m_Journal;
|
Watch m_Watch;MemoryAdapter m_Memory;BodyAdapter m_Bodies;struct RecoveryAdapter{int calls=0;void ReportBody(ERR_HQReport*){calls++;}} m_Recovery;CoverageAdapter m_Coverage;JournalAdapter m_Journal;
|
||||||
Array<ERR_HQReport*>m_Contacts;
|
Array<ERR_HQReport*>m_Contacts;
|
||||||
'''+accept+r'''
|
'''+accept+r'''
|
||||||
@@ -251,7 +251,7 @@ int main(){
|
|||||||
assert(wire->CasualtySlot==0&&wire->Position.x==900&&wire->CasualtySourceId=="deleted-during-uplink");
|
assert(wire->CasualtySlot==0&&wire->Position.x==900&&wire->CasualtySourceId=="deleted-during-uplink");
|
||||||
assert(intake.m_Watch.Count()==0); // preparing/queuing local evidence changes nothing
|
assert(intake.m_Watch.Count()==0); // preparing/queuing local evidence changes nothing
|
||||||
wire->Target=nullptr; // simulate native weak-reference invalidation after captured LOS, before radio completes
|
wire->Target=nullptr; // simulate native weak-reference invalidation after captured LOS, before radio completes
|
||||||
intake.Accept(wire,2000);
|
intake.Accept(wire,2000);assert(intake.m_NextAssignment==5000);
|
||||||
auto delivered=intake.m_Watch.Find("deleted-during-uplink");
|
auto delivered=intake.m_Watch.Find("deleted-during-uplink");
|
||||||
assert(delivered&&delivered->Closed&&delivered->Count==0&&intake.m_Watch.ConfirmedLosses()==1);
|
assert(delivered&&delivered->Closed&&delivered->Count==0&&intake.m_Watch.ConfirmedLosses()==1);
|
||||||
assert(delivered->MessageId==0&&delivered->ObservedAt==-1); // no invented check-in or corpse position
|
assert(delivered->MessageId==0&&delivered->ObservedAt==-1); // no invented check-in or corpse position
|
||||||
@@ -259,7 +259,7 @@ int main(){
|
|||||||
intake.Accept(wire,2001);assert(intake.m_Watch.ConfirmedLosses()==1); // delivered duplicates cannot add losses
|
intake.Accept(wire,2001);assert(intake.m_Watch.ConfirmedLosses()==1); // delivered duplicates cannot add losses
|
||||||
wire->CasualtySourceId="stale";intake.Accept(wire,121001);assert(!intake.m_Watch.Find("stale"));
|
wire->CasualtySourceId="stale";intake.Accept(wire,121001);assert(!intake.m_Watch.Find("stale"));
|
||||||
wire->ObservedAt=2000;wire->TravelMode="walking";wire->Target=&local;
|
wire->ObservedAt=2000;wire->TravelMode="walking";wire->Target=&local;
|
||||||
intake.Accept(wire,2000);assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1);
|
intake.Accept(wire,2000);assert(intake.m_NextAssignment==2000);assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1);
|
||||||
wire->TravelMode="body";wire->Distress=true;intake.Accept(wire,2001);
|
wire->TravelMode="body";wire->Distress=true;intake.Accept(wire,2001);
|
||||||
assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1); // distress cannot claim bodily identity
|
assert(!intake.m_Watch.Find("stale")&&intake.m_Contacts.Count()==1); // distress cannot claim bodily identity
|
||||||
Watch bodyOnly;body.CasualtySourceId="no-survivor";body.CasualtySlot=0;body.ObservedAt=1000;
|
Watch bodyOnly;body.CasualtySourceId="no-survivor";body.CasualtySlot=0;body.ObservedAt=1000;
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from policy_harness import ROOT,body,run
|
||||||
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterCombat.c'
|
||||||
|
s=body(p,'static bool Recent(').replace('EAITargetInfoCategory.','EAITargetInfoCategory::')
|
||||||
|
run('''#include <cassert>
|
||||||
|
namespace EAITargetInfoCategory {enum {UNKNOWN,DESTROYED,DISARMED,LOST,DETECTED,IDENTIFIED};}
|
||||||
|
'''+s+'''
|
||||||
|
int main(){for(int c=0;c<6;c++){bool threat=c==4||c==5;assert(Recent(c,0)==threat);assert(Recent(c,15)==threat);assert(!Recent(c,-1));assert(!Recent(c,15.001f));}}
|
||||||
|
''')
|
||||||
|
print('PASS: only recent native detected/identified threats suspend hunter travel; stale/disarmed/destroyed contacts cannot strand patrols')
|
||||||
|
# Execute the actual state transition and owned-order cleanup with native perception doubled.
|
||||||
|
s=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c','protected void UpdateCombat(')
|
||||||
|
s=s.replace('protected ','').replace('ERR_HunterVehicle unit','Unit* unit').replace('unit.','unit->').replace('ERR_HunterCombat.Active','Active').replace('ERR_HQ.Event','Event').replace('null','nullptr')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
struct Group{bool contact=false;};
|
||||||
|
bool Active(Group* g){return g&&g->contact;}
|
||||||
|
int messages=0;void Event(std::string,std::string){messages++;}
|
||||||
|
struct Unit{bool Registered=true,CombatHeld=false;Group* Group=nullptr;float CombatQuietUntil=0,NextOrder=100;void* Downlink=nullptr;std::string Source="test";int clears=0;void ClearOrder(){clears++;}};
|
||||||
|
'''.replace('Group* Group','struct Group* Group')+s+'''
|
||||||
|
int main(){Group group;Unit u;u.Group=&group;u.Downlink=&group;UpdateCombat(&u,1000);assert(!u.CombatHeld&&u.clears==0&&u.Downlink);
|
||||||
|
group.contact=true;UpdateCombat(&u,2000);assert(u.CombatHeld&&u.clears==1&&!u.Downlink&&u.CombatQuietUntil==17000);
|
||||||
|
UpdateCombat(&u,5000);assert(u.clears==1&&u.CombatQuietUntil==20000&&messages==1);
|
||||||
|
group.contact=false;UpdateCombat(&u,19999);assert(u.CombatHeld);UpdateCombat(&u,20000);assert(!u.CombatHeld&&u.NextOrder==0&&messages==2);
|
||||||
|
Unit pending;pending.Group=&group;pending.Registered=false;group.contact=true;UpdateCombat(&pending,100);assert(!pending.CombatHeld&&pending.clears==0);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: actual hunter combat transition cancels only its travel once, holds through renewed contact and resumes after quiet; unregistered startup preserved')
|
||||||
@@ -35,7 +35,7 @@ struct Group{int GetFaction(){return 1;}};
|
|||||||
struct Entity{Vec GetOrigin(){return {};}};
|
struct Entity{Vec GetOrigin(){return {};}};
|
||||||
struct Waypoint{bool complete=false;bool IsWithinCompletionRadius(Group*){return complete;}};
|
struct Waypoint{bool complete=false;bool IsWithinCompletionRadius(Group*){return complete;}};
|
||||||
struct Route{};
|
struct Route{};
|
||||||
struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
|
struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination,ContactPosition,DeliveredContactPosition;bool DeliveredContact=false;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
|
||||||
struct ERR_HQReport{int Target=1;};
|
struct ERR_HQReport{int Target=1;};
|
||||||
struct ERR_HQBelief{bool actionable=true;float ObservedAt=10,Confidence=1;int MessageId=7;Vec PredictedPosition={1000,0,2000};bool Actionable(){return actionable;}};
|
struct ERR_HQBelief{bool actionable=true;float ObservedAt=10,Confidence=1;int MessageId=7;Vec PredictedPosition={1000,0,2000};bool Actionable(){return actionable;}};
|
||||||
struct Memory{ERR_HQBelief belief;ERR_HQBelief* Get(int){return &belief;}};
|
struct Memory{ERR_HQBelief belief;ERR_HQBelief* Get(int){return &belief;}};
|
||||||
@@ -72,6 +72,15 @@ int main(){
|
|||||||
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,7000);assert(hq.requests==2); // no repeated order for same report
|
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,7000);assert(hq.requests==2); // no repeated order for same report
|
||||||
ERR_HunterVehicle disconnected;hq.connected=false;service.QueueDestination(&disconnected,&hq,10000);
|
ERR_HunterVehicle disconnected;hq.connected=false;service.QueueDestination(&disconnected,&hq,10000);
|
||||||
assert(!disconnected.Downlink&&disconnected.LastContact==-1);
|
assert(!disconnected.Downlink&&disconnected.LastContact==-1);
|
||||||
|
// Repeated/new reports in an already delivered area must not churn routing.
|
||||||
|
Waypoint route;unit.Waypoint=&route;unit.DeliveredContact=true;unit.DeliveredContactPosition={1000,0,2000};
|
||||||
|
unit.Downlink=nullptr;unit.NextOrder=0;unit.NextContactOrder=0;
|
||||||
|
int kept=hq.requests;int projections=ERR_SupplyConvoySystem::calls;
|
||||||
|
hq.memory.belief.ObservedAt=12;service.QueueDestination(&unit,&hq,12000);
|
||||||
|
assert(hq.requests==kept&&ERR_SupplyConvoySystem::calls==projections&&unit.LastContact==12);
|
||||||
|
hq.memory.belief.PredictedPosition.x=1100;hq.memory.belief.ObservedAt=13;
|
||||||
|
service.QueueDestination(&unit,&hq,18000);assert(hq.requests==kept+1); // Material displacement redirects.
|
||||||
|
hq.memory.belief.ObservedAt=11;
|
||||||
int requests=hq.requests;service.QueueDestination(&disconnected,&hq,11000);assert(hq.requests==requests);
|
int requests=hq.requests;service.QueueDestination(&disconnected,&hq,11000);assert(hq.requests==requests);
|
||||||
hq.connected=true;service.QueueDestination(&disconnected,&hq,15000);assert(disconnected.Downlink&&disconnected.LastContact==11);
|
hq.connected=true;service.QueueDestination(&disconnected,&hq,15000);assert(disconnected.Downlink&&disconnected.LastContact==11);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""Execute the pre-spawn profile gate; verify registry-backed faction resources."""
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
import re
|
||||||
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
|
||||||
|
method=body(p,'protected bool SpawnHunter(')
|
||||||
|
# The real preflight block runs before construction/insertion/native spawning.
|
||||||
|
preflight=method[method.index('{')+1:method.index(' ERR_HunterVehicle unit =')]
|
||||||
|
preflight=preflight.replace('ResourceName','std::string').replace('ERR_EnemyMedicalCatalog.','ERR_EnemyMedicalCatalog::').replace('ERR_EnemyLoadoutCatalog.','ERR_EnemyLoadoutCatalog::').replace('ERR_HQ.Event','ERR_HQ::Event').replace('string.Format','Format').replace('m_Mode.EnemyFaction().GetFactionKey()','"TEST_FACTION"')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
namespace ERR_EnemyMedicalCatalog {std::string soldier="soldier",medic="medic";std::string Crew(void*,bool m){return m?medic:soldier;}}
|
||||||
|
namespace ERR_EnemyLoadoutCatalog {bool Available(std::string p){return !p.empty()&&p!="invalid";}}
|
||||||
|
int logged=0;namespace ERR_HQ {void Event(std::string,std::string){logged++;}}
|
||||||
|
template<class...T>std::string Format(std::string text,T...){return text;}
|
||||||
|
struct Gate {void* m_Mode=nullptr;float m_NextSpawn=0;bool Check(float now){
|
||||||
|
'''+preflight+'''return true;}};
|
||||||
|
int main(){Gate gate;assert(gate.Check(1000)&&logged==0);
|
||||||
|
ERR_EnemyMedicalCatalog::medic="";assert(!gate.Check(1000)&&gate.m_NextSpawn==31000&&logged==1);
|
||||||
|
ERR_EnemyMedicalCatalog::medic="medic";ERR_EnemyMedicalCatalog::soldier="invalid";assert(!gate.Check(5000)&&gate.m_NextSpawn==35000);
|
||||||
|
ERR_EnemyMedicalCatalog::soldier="soldier";assert(gate.Check(35000));
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
registry=(ROOT/'addons/core/Configs/ERR/Content/Registry.conf').read_text()
|
||||||
|
for key in ('RHS_ION','RHS_USAF','RHS_AFRF'):
|
||||||
|
option=next(x for x in registry.split('ERR_ContentOption {') if f'NativeFactionKey "{key}"' in x)
|
||||||
|
assert re.search(r'ResponseSoldier "\{[A-F0-9]{16}\}[^"\n]+\.et"',option)
|
||||||
|
assert re.search(r'ResponseMedic "\{[A-F0-9]{16}\}[^"\n]*Medic[^"\n]*\.et"',option)
|
||||||
|
print('PASS: missing/invalid faction crew defers before any native spawn or allowance insertion; all registered RHS factions have explicit soldier and medic profiles')
|
||||||
|
# Execute actual finite vehicle-radio initialization: pending/refused/lost radios
|
||||||
|
# never cause repeat allocations, and admission requires physical capability.
|
||||||
|
prepare=body(p,'protected bool PrepareVehicleRadio(').replace('protected ','').replace('ERR_HunterVehicle unit','Unit* unit')
|
||||||
|
prepare=prepare.replace('InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));','Storage* storage = vehicleStorage;')
|
||||||
|
prepare=prepare.replace('ref ','').replace('unit.','unit->').replace('storage.','storage->').replace('RadioOperation.','RadioOperation->').replace('ERR_HQ.Event','Event').replace('ERR_EnemyRadio.MANPACK','"manpack"').replace('EStoragePurpose.PURPOSE_DEPOSIT','1').replace('null','nullptr')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
|
||||||
|
struct Unit{bool RadioRequested=false;ERR_EnemyEquipmentCallback* RadioOperation=nullptr;void* Driver=nullptr;std::string Source="uaz";};
|
||||||
|
struct Storage{int calls=0;bool accepts=true;bool TrySpawnPrefabToStorage(std::string,void*,int,int,ERR_EnemyEquipmentCallback*){calls++;return accepts;}};
|
||||||
|
void Event(std::string,std::string){}
|
||||||
|
struct Service{Storage* vehicleStorage=nullptr;int physicalCapability=0;int VehicleRadioCapability(void*){return physicalCapability;}
|
||||||
|
'''+prepare+'''
|
||||||
|
};
|
||||||
|
int main(){Service service;Storage storage;service.vehicleStorage=&storage;Unit unit;
|
||||||
|
assert(!service.PrepareVehicleRadio(&unit)&&storage.calls==1);for(int i=0;i<10;i++)assert(!service.PrepareVehicleRadio(&unit));assert(storage.calls==1);
|
||||||
|
unit.RadioOperation->Finished=true;unit.RadioOperation->Succeeded=true;assert(!service.PrepareVehicleRadio(&unit));
|
||||||
|
service.physicalCapability=2;assert(service.PrepareVehicleRadio(&unit)&&storage.calls==1);
|
||||||
|
service.physicalCapability=0;assert(!service.PrepareVehicleRadio(&unit)&&storage.calls==1); // Loot does not replenish the radio.
|
||||||
|
Unit refused;storage.accepts=false;assert(!service.PrepareVehicleRadio(&refused));for(int i=0;i<10;i++)assert(!service.PrepareVehicleRadio(&refused));assert(storage.calls==2);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: vehicle radio issued once; pending, refusal and looting cannot duplicate or fabricate a physical radio link')
|
||||||
|
capability=body(p,'int VehicleRadioCapability(')
|
||||||
|
capability=capability.replace('IEntity actor','Entity* actor').replace('foreach (ERR_HunterVehicle unit : m_Hunters)','for (Unit* unit : m_Hunters)').replace('InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(unit.Entity.FindComponent(InventoryStorageManagerComponent));','Storage* storage = unit->Entity->storage;')
|
||||||
|
capability=capability.replace('array<IEntity> items = {};','std::vector<Entity*> items;').replace('foreach (IEntity item : items)','for (Entity* item : items)').replace('item.FindComponent(BaseRadioComponent)','item->baseRadio').replace('SCR_RadioComponent radio = SCR_RadioComponent.Cast(item.FindComponent(SCR_RadioComponent));','Radio* radio = item->radio;')
|
||||||
|
for obj in ('m_Mode','unit','storage','radio'):capability=capability.replace(obj+'.',obj+'->')
|
||||||
|
capability=capability.replace('ERR_Actors.Alive','Alive').replace('CompartmentAccessComponent.GetVehicleIn','VehicleIn').replace('ERR_ConvoyRules.Captured','Captured')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
struct Radio{bool pack=true;bool ERR_IsHQManpack(){return pack;}};
|
||||||
|
struct Storage;
|
||||||
|
struct Entity{bool alive=true,baseRadio=true,captured=false;Entity* vehicle=nullptr;Storage* storage=nullptr;Radio* radio=nullptr;};
|
||||||
|
struct Storage{std::vector<Entity*>items;void GetItems(std::vector<Entity*>&out){out=items;}};
|
||||||
|
struct Members:std::vector<Entity*>{bool Contains(Entity* p){return std::find(begin(),end(),p)!=end();}};
|
||||||
|
struct Mode{bool active=true;bool Active(){return active;}};
|
||||||
|
struct Unit{struct Entity* Entity=nullptr;void* Group=nullptr;bool Released=false,valid=true;struct Members Members;bool CrewValid(Mode*){return valid;}};
|
||||||
|
bool Alive(Entity* e){return e&&e->alive;}Entity* VehicleIn(Entity* e){return e->vehicle;}bool Captured(Entity* e,void*){return e->captured;}
|
||||||
|
struct Service{Mode* m_Mode=nullptr;bool m_Stopped=false;std::vector<Unit*>m_Hunters;
|
||||||
|
'''+capability+'''
|
||||||
|
};
|
||||||
|
int main(){Service s;Mode mode;s.m_Mode=&mode;Entity actor,car,item;Radio radio;Storage storage;Unit unit;
|
||||||
|
unit.Entity=&car;unit.Members.push_back(&actor);actor.vehicle=&car;car.storage=&storage;item.radio=&radio;storage.items={&item};s.m_Hunters={&unit};
|
||||||
|
assert(s.VehicleRadioCapability(&actor)==2);storage.items.clear();assert(s.VehicleRadioCapability(&actor)==0);storage.items={&item};
|
||||||
|
actor.vehicle=nullptr;assert(s.VehicleRadioCapability(&actor)==0);actor.vehicle=&car;car.captured=true;assert(s.VehicleRadioCapability(&actor)==0);car.captured=false;
|
||||||
|
car.alive=false;assert(s.VehicleRadioCapability(&actor)==0);car.alive=true;unit.Released=true;assert(s.VehicleRadioCapability(&actor)==0);unit.Released=false;
|
||||||
|
mode.active=false;assert(s.VehicleRadioCapability(&actor)==0);mode.active=true;s.m_Stopped=true;assert(s.VehicleRadioCapability(&actor)==0);s.m_Stopped=false;
|
||||||
|
radio.pack=false;assert(s.VehicleRadioCapability(&actor)==0);radio.pack=true;Entity stranger;stranger.vehicle=&car;assert(s.VehicleRadioCapability(&stranger)==0);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: actual vehicle-radio link requires the original seated crew, physical manpack and live uncaptured mission vehicle; looting, dismount, release and teardown remove it')
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
"""Execute the production infantry combat state machine against waypoint doubles."""
|
||||||
|
import re
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantryCombat.c'
|
||||||
|
s=p.read_text()
|
||||||
|
s=s.replace('class ERR_InfantryCombat', 'struct ERR_InfantryCombat')
|
||||||
|
s=s.replace('protected ', '').replace('ref ', '').replace('null','nullptr')
|
||||||
|
s=s.replace('EAIGroupCombatMode.FIRE_AT_WILL','1').replace('.IsEmpty()', '.empty()')
|
||||||
|
for typ in ('AIWaypoint','ERR_HQTask','ERR_HQUnit'):
|
||||||
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
||||||
|
s=s.replace('bool Held;', 'bool Held=false;').replace('float QuietUntil;', 'float QuietUntil=0;')
|
||||||
|
s=s.replace('m_PausedWaypoint;', 'm_PausedWaypoint=nullptr;').replace('m_PausedTask;', 'm_PausedTask=nullptr;')
|
||||||
|
for name in ('unit','task','Task','Group','Utility'):
|
||||||
|
s=s.replace(name+'.',name+'->')
|
||||||
|
s=s.rstrip()+';'
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <algorithm>
|
||||||
|
struct AIWaypoint{};
|
||||||
|
struct ERR_HQTask {bool ReleaseOnly=false,RoadIntercept=false;std::string SecuritySite;float ExpiresAt=100000;};
|
||||||
|
struct Group {std::vector<AIWaypoint*> queue;int adds=0,removes=0;
|
||||||
|
void RemoveWaypoint(AIWaypoint* w){removes++;queue.erase(std::remove(queue.begin(),queue.end(),w),queue.end());}
|
||||||
|
void AddWaypointAt(AIWaypoint* w,int slot){assert(slot==0);adds++;queue.insert(queue.begin(),w);}};
|
||||||
|
struct Utility{int mode=0,changes=0;void SetCombatMode(int m){mode=m;changes++;}};
|
||||||
|
struct ERR_HQUnit{AIWaypoint* Waypoint=nullptr;ERR_HQTask* Task=nullptr;void* Assignment=nullptr;struct Group* Group;struct Utility* Utility;};
|
||||||
|
'''+s+r'''
|
||||||
|
struct Fixture {Group group;Utility utility;AIWaypoint search,home;ERR_HQTask task;ERR_HQUnit unit;ERR_InfantryCombat combat;
|
||||||
|
Fixture(){unit.Group=&group;unit.Utility=&utility;unit.Task=&task;unit.Assignment=&task;unit.Waypoint=&search;group.queue={&search,&home};}
|
||||||
|
};
|
||||||
|
int main(){
|
||||||
|
{Fixture f;assert(f.combat.Update(&f.unit,true,false,1000)==0);assert(f.group.removes==0);
|
||||||
|
assert(f.combat.Update(&f.unit,true,true,2000)==1);assert(f.combat.Held&&f.combat.Paused());
|
||||||
|
assert(f.group.queue.size()==1&&f.group.queue[0]==&f.home&&f.utility.mode==1);
|
||||||
|
assert(f.unit.Task==&f.task&&f.unit.Waypoint==&f.search&&f.unit.Assignment);
|
||||||
|
assert(f.combat.Update(&f.unit,true,true,4000)==0&&f.group.removes==1&&f.utility.changes==1);
|
||||||
|
assert(f.combat.Update(&f.unit,true,false,18999)==0&&f.combat.Held);
|
||||||
|
assert(f.combat.Update(&f.unit,true,false,19000)==2&&!f.combat.Held&&!f.combat.Paused());
|
||||||
|
assert(f.group.adds==1&&f.group.queue.size()==2&&f.group.queue[0]==&f.search);
|
||||||
|
assert(f.combat.Update(&f.unit,true,false,20000)==0&&f.group.adds==1);}
|
||||||
|
// Never resurrect expired, released, replaced, or newly defensive tasks.
|
||||||
|
for(int invalid=0;invalid<6;invalid++){Fixture f;ERR_HQTask replacement;AIWaypoint other;
|
||||||
|
f.combat.Update(&f.unit,true,true,0);
|
||||||
|
if(invalid==0)f.task.ExpiresAt=15000;
|
||||||
|
if(invalid==1)f.unit.Task=nullptr;
|
||||||
|
if(invalid==2)f.unit.Waypoint=&other;
|
||||||
|
if(invalid==3)f.unit.Task=&replacement;
|
||||||
|
if(invalid==4)f.unit.Assignment=nullptr;
|
||||||
|
if(invalid==5)f.task.RoadIntercept=true;
|
||||||
|
assert(f.combat.Update(&f.unit,true,false,15000)==3&&f.group.adds==0);}
|
||||||
|
// Existing defensive posts and recalls keep their queue and fire policy.
|
||||||
|
for(int defence=0;defence<3;defence++){Fixture f;
|
||||||
|
if(defence==0){f.task.SecuritySite="radio";}if(defence==1){f.task.RoadIntercept=true;}if(defence==2){f.task.ReleaseOnly=true;}
|
||||||
|
assert(f.combat.Update(&f.unit,true,true,0)==1&&!f.combat.Paused());
|
||||||
|
assert(f.group.removes==0&&f.utility.changes==0);
|
||||||
|
assert(f.combat.Update(&f.unit,true,false,15000)==3&&f.group.adds==0);}
|
||||||
|
// Reservations/player/vehicle control cannot insert or remove waypoints.
|
||||||
|
{Fixture f;assert(f.combat.Update(&f.unit,false,true,0)==0&&f.group.removes==0);
|
||||||
|
f.combat.Update(&f.unit,true,true,0);
|
||||||
|
assert(f.combat.Update(&f.unit,false,false,20000)==0&&f.group.adds==0);
|
||||||
|
assert(f.combat.Update(&f.unit,true,false,20000)==2&&f.group.adds==1);}
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: actual infantry search suspension, quiet grace, finite resume, expiry/replacement and defensive/ownership boundaries')
|
||||||
|
# Guard the production integration: local pause occurs before radio delivery, and
|
||||||
|
# only task downlinks are cancelled; status/vehicle messages remain independent.
|
||||||
|
hq=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
||||||
|
tick=body(hq,'override void Tick(')
|
||||||
|
assert tick.index('UpdateInfantryCombat(unit, now)') < tick.index('unit.Downlink.Advance')
|
||||||
|
assert 'unit.InfantryCombat.Held || ERR_HunterCombat.Active(unit.Group)' in body(hq,'protected bool Fighting(')
|
||||||
|
assert 'if (unit.PendingTask) LostOrder(unit, now)' in body(hq,'protected void UpdateInfantryCombat(')
|
||||||
|
print('PASS: infantry combat gate precedes radio delivery and preserves non-task transports')
|
||||||
|
|
||||||
|
update=body(hq,'protected void UpdateInfantryCombat(').replace('protected ', '')
|
||||||
|
update=update.replace('ERR_HQUnit unit','ERR_HQUnit* unit').replace('unit.', 'unit->')
|
||||||
|
update=update.replace('InfantryCombat.', 'InfantryCombat->').replace('Group.', 'Group->')
|
||||||
|
update=update.replace('array<AIAgent> reacting', 'std::vector<AIAgent*> reacting')
|
||||||
|
update=update.replace('foreach (AIAgent actor : reacting)', 'for (AIAgent* actor : reacting)').replace('actor.', 'actor->')
|
||||||
|
update=update.replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active').replace('ERR_Actors.CanFight', 'ERR_Actors::CanFight').replace('string.Format', 'Format')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
using string=std::string;
|
||||||
|
template<class... T>string Format(T...){return "entry";}
|
||||||
|
struct AIAgent{bool fit=true;int activations=0;AIAgent* GetControlledEntity(){return this;}void ActivateAI(){activations++;}};
|
||||||
|
struct Group{int activations=0;std::vector<AIAgent*> members;void ActivateAI(){activations++;}void GetAgents(std::vector<AIAgent*>& a){a=members;}};
|
||||||
|
struct ERR_Actors{static bool CanFight(AIAgent* a){return a->fit;}};
|
||||||
|
struct ERR_HunterCombat{static bool Active(Group*){return true;}};
|
||||||
|
struct ERR_HQUnit;
|
||||||
|
struct State{int change=1;bool paused=true;int Update(ERR_HQUnit*,bool eligible,bool,float){return eligible?change:0;}bool Paused(){return paused;}};
|
||||||
|
struct ERR_HQUnit{int Id=7;string ForceSourceId="town";struct Group* Group;State* InfantryCombat;void* PendingTask=nullptr;bool eligible=true;bool Eligible(int){return eligible;}};
|
||||||
|
struct Journal{int logs=0;void Write(string,string){logs++;}};
|
||||||
|
struct HQ{int m_Mode=1,cancelled=0;Journal m_Journal;void LostOrder(ERR_HQUnit* u,float){cancelled++;u->PendingTask=nullptr;}
|
||||||
|
'''+update+r'''
|
||||||
|
};
|
||||||
|
int main(){HQ h;Group g;State state;ERR_HQUnit u;u.Group=&g;u.InfantryCombat=&state;
|
||||||
|
AIAgent fit,casualty;casualty.fit=false;g.members={&fit,&casualty,nullptr};u.PendingTask=&fit;
|
||||||
|
h.UpdateInfantryCombat(&u,1000);assert(g.activations==1&&fit.activations==1&&casualty.activations==0&&h.cancelled==1&&h.m_Journal.logs==1);
|
||||||
|
state.change=0;h.UpdateInfantryCombat(&u,2000);assert(g.activations==1&&h.cancelled==1);
|
||||||
|
state.change=1;u.PendingTask=nullptr;h.UpdateInfantryCombat(&u,3000);assert(h.cancelled==1&&fit.activations==2);
|
||||||
|
u.eligible=false;h.UpdateInfantryCombat(&u,4000);assert(g.activations==2&&h.m_Journal.logs==2);
|
||||||
|
u.eligible=true;state.change=2;h.UpdateInfantryCombat(&u,5000);assert(g.activations==2&&h.m_Journal.logs==3);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: actual HQ contact transition wakes only fit squad members, cancels only task orders and logs changes once')
|
||||||
@@ -39,7 +39,7 @@ struct Mode {
|
|||||||
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
|
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
|
||||||
int main(){
|
int main(){
|
||||||
{Fixture f;f.m.roster.survivor=true;f.m.service.defeat=true;f.tick(31000);
|
{Fixture f;f.m.roster.survivor=true;f.m.service.defeat=true;f.tick(31000);
|
||||||
assert(!f.m.Active() && f.m.m_EndReason=="All prisoners are unconscious and no treatment or recovery is progressing.");}
|
assert(!f.m.Active() && f.m.m_EndReason=="Everybody died. All prisoners are unconscious and nobody can recover or receive help.");}
|
||||||
// Sole exhausted first admission reaches existing grace, without a life/body.
|
// Sole exhausted first admission reaches existing grace, without a life/body.
|
||||||
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
|
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
|
||||||
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
|
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
|
||||||
|
|||||||
@@ -63,12 +63,12 @@ run(r'''
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
struct vector {float x=0,y=0,z=0;static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}static float DistanceXZ(vector a,vector b){a.y=0;b.y=0;return Distance(a,b);}};
|
struct vector {float x=0,y=0,z=0;std::string ToString(){return "point";}static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}static float DistanceXZ(vector a,vector b){a.y=0;b.y=0;return Distance(a,b);}};
|
||||||
using IEntity=void*;
|
using IEntity=void*;
|
||||||
template<class T>struct Items {std::vector<T> values;void Insert(T t){values.push_back(t);}};
|
template<class T>struct Items {std::vector<T> values;void Insert(T t){values.push_back(t);}};
|
||||||
struct Replication {static inline bool server=true;static bool IsServer(){return server;}};
|
struct Replication {static inline bool server=true;static bool IsServer(){return server;}};
|
||||||
struct ERR_HQReport {IEntity Target=this;int MessageId=1;float ObservedAt=0;bool Distress=false;vector Position{};std::string TravelMode="foot";std::string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
|
struct ERR_HQReport {IEntity Target=this;int MessageId=1;float ObservedAt=0;bool Distress=false;vector Position{};std::string TravelMode="foot";std::string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
|
||||||
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;
|
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;static bool Predict(ERR_HQReport*,float,float,vector&){return true;}
|
||||||
'''+fresh+r'''};
|
'''+fresh+r'''};
|
||||||
struct ERR_MortarRefreshRules {
|
struct ERR_MortarRefreshRules {
|
||||||
'''+newer+r'''};
|
'''+newer+r'''};
|
||||||
@@ -90,6 +90,7 @@ struct ERR_MortarBattery {
|
|||||||
bool Operational(){return operational;}
|
bool Operational(){return operational;}
|
||||||
bool CanRefresh(ERR_HQReport*,float);bool BeginRefresh(ERR_HQReport*,ERR_HQRoute*,float);void RejectRefresh(std::string);
|
bool CanRefresh(ERR_HQReport*,float);bool BeginRefresh(ERR_HQReport*,ERR_HQRoute*,float);void RejectRefresh(std::string);
|
||||||
void Cancel(std::string reason="cancel requested");void ObserveProjectile(IEntity,IEntity,bool);void FlushTraces(){}void DetachActivity(){}
|
void Cancel(std::string reason="cancel requested");void ObserveProjectile(IEntity,IEntity,bool);void FlushTraces(){}void DetachActivity(){}
|
||||||
|
void Trace(std::string,std::string,std::string){}
|
||||||
void Update(float){}bool SupplyPending(){return false;}int CountShells(){return 27;}
|
void Update(float){}bool SupplyPending(){return false;}int CountShells(){return 27;}
|
||||||
bool CanRequest(ERR_HQReport*,float){return false;}bool BeginRequest(ERR_HQReport*,float){assert(false);return false;}
|
bool CanRequest(ERR_HQReport*,float){return false;}bool BeginRequest(ERR_HQReport*,float){assert(false);return false;}
|
||||||
};
|
};
|
||||||
@@ -100,7 +101,7 @@ struct ERR_MortarAimSolution {
|
|||||||
IEntity Gun=nullptr;ERR_HQReport* Report=nullptr;int OrderId=1,Shot=0,Charge=0;vector GunOrigin{},Impact{500,0,0},AimTarget{500,500,0};float FlightTime=30;
|
IEntity Gun=nullptr;ERR_HQReport* Report=nullptr;int OrderId=1,Shot=0,Charge=0;vector GunOrigin{},Impact{500,0,0},AimTarget{500,500,0};float FlightTime=30;
|
||||||
'''+matches+r'''};
|
'''+matches+r'''};
|
||||||
struct ERR_MortarSystem {
|
struct ERR_MortarSystem {
|
||||||
HQ hq;HQ* m_HQ=&hq;int failures=0;
|
HQ hq;HQ* m_HQ=&hq;int failures=0;std::string SafetyReason;bool Safe(ERR_MortarBattery*,vector,float){return true;}
|
||||||
Items<ERR_MortarBattery*> m_Batteries;bool m_Stopped=false;float m_NextDecision=1e9f,m_NextRefreshDecision=0;
|
Items<ERR_MortarBattery*> m_Batteries;bool m_Stopped=false;float m_NextDecision=1e9f,m_NextRefreshDecision=0;
|
||||||
bool Withhold(ERR_MortarBattery* b,const char* why){b->Rejected=true;b->LastFailure=why;failures++;return false;}
|
bool Withhold(ERR_MortarBattery* b,const char* why){b->Rejected=true;b->LastFailure=why;failures++;return false;}
|
||||||
void AdvanceRefresh(ERR_MortarBattery*,float);
|
void AdvanceRefresh(ERR_MortarBattery*,float);
|
||||||
@@ -196,7 +197,7 @@ int main(){
|
|||||||
interrupted.b.ObserveProjectile(&interrupted.next,nullptr,false); // actual physical-event bookkeeping, not native launch proof
|
interrupted.b.ObserveProjectile(&interrupted.next,nullptr,false); // actual physical-event bookkeeping, not native launch proof
|
||||||
assert(!interrupted.b.Released&&interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.Report==&interrupted.old);
|
assert(!interrupted.b.Released&&interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.Report==&interrupted.old);
|
||||||
interrupted.system.Tick(25003);assert(!interrupted.b.CommandOpen&&!interrupted.b.Activity&&!interrupted.b.Report&&!interrupted.b.RefreshRejected);
|
interrupted.system.Tick(25003);assert(!interrupted.b.CommandOpen&&!interrupted.b.Activity&&!interrupted.b.Report&&!interrupted.b.RefreshRejected);
|
||||||
assert(interrupted.b.originalActivity.completions==1&&interrupted.b.transfer.invalidated==1&&interrupted.b.NextOrder==35003);
|
assert(interrupted.b.originalActivity.completions==1&&interrupted.b.transfer.invalidated==1&&interrupted.b.NextOrder==85003);
|
||||||
assert(interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.m_ReportLedger.claimed.count(2));
|
assert(interrupted.b.OrderShots==1&&interrupted.b.TotalShots==1&&interrupted.b.m_ReportLedger.claimed.count(2));
|
||||||
F noRoute;noRoute.system.hq.contacts.Insert(&noRoute.next);noRoute.b.Released=true;
|
F noRoute;noRoute.system.hq.contacts.Insert(&noRoute.next);noRoute.b.Released=true;
|
||||||
noRoute.system.Tick(25000);assert(noRoute.system.hq.downlinks==1&&noRoute.b.RefreshRejected&&noRoute.b.Released);
|
noRoute.system.Tick(25000);assert(noRoute.system.hq.downlinks==1&&noRoute.b.RefreshRejected&&noRoute.b.Released);
|
||||||
|
|||||||
@@ -128,3 +128,61 @@ supply_state=body(battery,'ERR_MortarSupplyState SupplyState(')
|
|||||||
assert supply_state.index('m_Transfer.Poll(now)') < supply_state.index('if (!SupplyAuthorized(now))')
|
assert supply_state.index('m_Transfer.Poll(now)') < supply_state.index('if (!SupplyAuthorized(now))')
|
||||||
assert shoot.count('super.EOnTaskSimulate(')==1 and 'battery.SupplyGunner()' not in shoot
|
assert shoot.count('super.EOnTaskSimulate(')==1 and 'battery.SupplyGunner()' not in shoot
|
||||||
print('PASS: actual startup/generation/ownership/timeout/callback policies and native-counter wait; integration preserves one-shot allocation and pending-crew cancellation')
|
print('PASS: actual startup/generation/ownership/timeout/callback policies and native-counter wait; integration preserves one-shot allocation and pending-crew cancellation')
|
||||||
|
|
||||||
|
# Execute the production post-shot node, including the superclass completion
|
||||||
|
# callback. A fast repeater must not start a ninth aim while HQ is still waiting
|
||||||
|
# to observe native completion; pending ammunition and duplicate ticks stay safe.
|
||||||
|
from types import SimpleNamespace
|
||||||
|
section=(r/'ERR_MortarFiring.c').read_text().split('modded class SCR_AIStaticArtilleryBehavior_ShootingLogic',1)[1]
|
||||||
|
cycle=body(SimpleNamespace(read_text=lambda:section),'override ENodeResult EOnTaskSimulate(')
|
||||||
|
cycle=cycle.replace('override ', '').replace('AIAgent owner','void* owner')
|
||||||
|
cycle=cycle.replace('ERR_MortarBattery battery;', 'ERR_MortarBattery* battery=nullptr;').replace('SCR_AIStaticArtilleryBehavior behavior;', 'SCR_AIStaticArtilleryBehavior* behavior=nullptr;')
|
||||||
|
cycle=cycle.replace('SCR_AIStaticArtilleryBehavior.Cast(m_Utility.GetExecutedAction())','m_Utility->GetExecutedAction()')
|
||||||
|
cycle=cycle.replace('ERR_MortarSystem system = ERR_MortarSystem.Get();','ERR_MortarSystem* system = &systemInstance;')
|
||||||
|
for name in ['battery','behavior','system']:cycle=cycle.replace(name+'.',name+'->')
|
||||||
|
for name in ['ERR_MortarSupplyRules','ERR_MortarSalvo','ERR_MortarSupplyState','ENodeResult']:cycle=cycle.replace(name+'.',name+'::')
|
||||||
|
cycle=cycle.replace('GetGame().GetWorld().GetWorldTime()', '1000.f').replace('super.EOnTaskSimulate(owner, dt)', 'Base::EOnTaskSimulate(owner, dt)')
|
||||||
|
gate=body(readiness,'static int CycleGate(')
|
||||||
|
run('''#include <cassert>
|
||||||
|
enum class ENodeResult {FAIL,RUNNING,SUCCESS};
|
||||||
|
enum ERR_MortarSupplyState {FAILED=-1,PENDING,READY};
|
||||||
|
struct ERR_MortarSalvo {static constexpr int ROUNDS=8;};
|
||||||
|
struct ERR_MortarSupplyRules {'''+gate+'''};
|
||||||
|
struct ERR_MortarBattery {
|
||||||
|
bool CommandOpen=true;int OrderShots=0,checks=0;ERR_MortarSupplyState supply=ERR_MortarSupplyState::READY;
|
||||||
|
ERR_MortarSupplyState SupplyState(float){checks++;return supply;}
|
||||||
|
};
|
||||||
|
struct SCR_AIStaticArtilleryBehavior {
|
||||||
|
ERR_MortarBattery* battery=nullptr;int count=0,completions=0;
|
||||||
|
int GetShotCount(){return count;}ERR_MortarBattery* ERR_GetMortarBattery(){return battery;}
|
||||||
|
};
|
||||||
|
struct Utility {SCR_AIStaticArtilleryBehavior* behavior;auto GetExecutedAction(){return behavior;}};
|
||||||
|
struct ERR_MortarSystem {int refusals=0;void Withhold(ERR_MortarBattery*,int){refusals++;}int SupplyFailure(ERR_MortarBattery*,float){return 0;}} systemInstance;
|
||||||
|
struct Base {
|
||||||
|
Utility* m_Utility=nullptr;int calls=0;bool aiming=false;static constexpr int PORT_AIMING_NEEDED=1;
|
||||||
|
ENodeResult EOnTaskSimulate(void*,float){calls++;auto b=m_Utility->behavior;if(++b->count==8)b->completions++;return ENodeResult::SUCCESS;}
|
||||||
|
void SetVariableOut(int,bool value){aiming=value;}
|
||||||
|
};
|
||||||
|
struct Node:Base {'''+cycle+'''};
|
||||||
|
int main(){
|
||||||
|
ERR_MortarBattery battery;SCR_AIStaticArtilleryBehavior behavior;behavior.battery=&battery;Utility utility{&behavior};Node n;n.m_Utility=&utility;
|
||||||
|
assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING&&n.calls==0);
|
||||||
|
battery.OrderShots=1;battery.supply=ERR_MortarSupplyState::PENDING;
|
||||||
|
assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING&&n.calls==0);
|
||||||
|
battery.supply=ERR_MortarSupplyState::READY;
|
||||||
|
for(int shot=1;shot<=8;shot++){
|
||||||
|
battery.OrderShots=shot;n.aiming=false;
|
||||||
|
if(shot==8)battery.supply=ERR_MortarSupplyState::FAILED; // final shot needs no ninth shell
|
||||||
|
auto result=n.EOnTaskSimulate(nullptr,0);
|
||||||
|
assert(n.calls==shot&&behavior.count==shot);
|
||||||
|
assert(result==(shot<8?ENodeResult::SUCCESS:ENodeResult::RUNNING));
|
||||||
|
assert(n.aiming==(shot<8));
|
||||||
|
}
|
||||||
|
assert(behavior.completions==1&&systemInstance.refusals==0);
|
||||||
|
for(int tick=0;tick<100;tick++)assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::RUNNING);
|
||||||
|
assert(n.calls==8&&behavior.completions==1); // no duplicate count or ninth aim
|
||||||
|
battery.CommandOpen=false;assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::FAIL);
|
||||||
|
behavior.battery=nullptr;assert(n.EOnTaskSimulate(nullptr,0)==ENodeResult::SUCCESS); // unrelated native artillery unchanged
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: rapid mortar cycle counts eight physical rounds once and waits for native completion without a ninth aim or shell')
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ s=s.replace('ERR_HQ hq','ERR_HQ* hq').replace('hq.','hq->')
|
|||||||
run(r'''
|
run(r'''
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
using string = std::string;
|
||||||
|
struct CLI {string outcome="hq-memory"; void GetCLIParam(string,string& value){value=outcome;}} System;
|
||||||
struct ERR_HQ{bool ready=true;bool ERR_ProbeSquadsReady(){return ready;}};
|
struct ERR_HQ{bool ready=true;bool ERR_ProbeSquadsReady(){return ready;}};
|
||||||
struct Probe{bool enabled=true,m_Mode=true,m_Failed=false,m_AutoChecked=false;int m_Step=0,automatic=0,checks=0;float m_Deadline=0;bool Enabled(){return enabled;}void Check(bool,std::string){checks++;m_Failed=true;}int AutomaticConvoy(ERR_HQ*){if(automatic==1)m_AutoChecked=true;return automatic;}
|
struct Probe{bool enabled=true,m_Mode=true,m_Failed=false,m_AutoChecked=false;int m_Step=0,automatic=0,checks=0;float m_Deadline=0;bool Enabled(){return enabled;}void Check(bool,std::string){checks++;m_Failed=true;}int AutomaticConvoy(ERR_HQ*){if(automatic==1)m_AutoChecked=true;return automatic;}
|
||||||
METHOD
|
METHOD
|
||||||
@@ -22,6 +24,7 @@ int main(){ERR_HQ hq;
|
|||||||
Probe missing;assert(missing.Tick(&hq,0)==0);assert(missing.Tick(&hq,150001)==-1&&missing.checks==1);
|
Probe missing;assert(missing.Tick(&hq,0)==0);assert(missing.Tick(&hq,150001)==-1&&missing.checks==1);
|
||||||
Probe refused;refused.Tick(&hq,0);refused.automatic=-1;assert(refused.Tick(&hq,40000)==-1&&refused.m_Deadline==150000&&!refused.m_AutoChecked);
|
Probe refused;refused.Tick(&hq,0);refused.automatic=-1;assert(refused.Tick(&hq,40000)==-1&&refused.m_Deadline==150000&&!refused.m_AutoChecked);
|
||||||
Probe unavailable;hq.ready=false;assert(unavailable.Tick(&hq,0)==0);assert(unavailable.Tick(&hq,150001)==-1);hq.ready=true;
|
Probe unavailable;hq.ready=false;assert(unavailable.Tick(&hq,0)==0);assert(unavailable.Tick(&hq,150001)==-1);hq.ready=true;
|
||||||
|
System.outcome="hq-awareness"; hq.ready=false; Probe awareness; assert(awareness.Tick(&hq,1000)==1 && !awareness.m_AutoChecked && awareness.m_Deadline==151000); assert(awareness.Tick(&hq,151001)==-1);
|
||||||
Probe invalid;invalid.enabled=false;assert(invalid.Tick(&hq,0)==-1&&invalid.m_Deadline==0);
|
Probe invalid;invalid.enabled=false;assert(invalid.Tick(&hq,0)==-1&&invalid.m_Deadline==0);
|
||||||
}
|
}
|
||||||
'''.replace('METHOD',s))
|
'''.replace('METHOD',s))
|
||||||
|
|||||||
@@ -61,7 +61,8 @@ struct F {
|
|||||||
int main(){
|
int main(){
|
||||||
assert(Rules::QueueUpdate(true,false,0,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,19999));
|
assert(Rules::QueueUpdate(true,false,0,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,19999));
|
||||||
assert(!Rules::QueueUpdate(false,false,0,0,1000,20000));assert(!Rules::QueueUpdate(true,true,0,0,1000,20000));
|
assert(!Rules::QueueUpdate(false,false,0,0,1000,20000));assert(!Rules::QueueUpdate(true,true,0,0,1000,20000));
|
||||||
assert(!Rules::QueueUpdate(true,false,4,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,91000));
|
assert(Rules::QueueUpdate(true,false,4,0,1000,20000));assert(Rules::QueueUpdate(true,false,5,0,1000,20000));
|
||||||
|
assert(!Rules::QueueUpdate(true,false,6,0,1000,20000));assert(!Rules::QueueUpdate(true,false,0,0,1000,91000));
|
||||||
assert(!Rules::QueueUpdate(true,false,0,0,22000,20000));
|
assert(!Rules::QueueUpdate(true,false,0,0,22000,20000));
|
||||||
F f;f.initial();assert(f.p.ReportKnown(&f.first));assert(!f.p.ReportKnown(&f.next));assert(!f.p.AllAccepted());
|
F f;f.initial();assert(f.p.ReportKnown(&f.first));assert(!f.p.ReportKnown(&f.next));assert(!f.p.AllAccepted());
|
||||||
assert(!f.p.AddObservation(1,1));assert(!f.p.AddObservation(2,0));assert(!f.p.AddObservation(0,100));
|
assert(!f.p.AddObservation(1,1));assert(!f.p.AddObservation(2,0));assert(!f.p.AddObservation(0,100));
|
||||||
|
|||||||
@@ -1,17 +1,79 @@
|
|||||||
|
"""Execute player policy methods; native damage/regen boundaries are modeled."""
|
||||||
from policy_harness import ROOT, body, run
|
from policy_harness import ROOT, body, run
|
||||||
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_PlayerMedical.c'
|
||||||
rule=body(p,'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('std::max(0,','std::max(0.0f,')
|
source=p.read_text()
|
||||||
run('''#include <algorithm>
|
methods='\n'.join(body(p,s) for s in ['void ERR_EnableCasualtyProtection(', 'override protected bool IsIndefinitelyUnconscious(', 'override float GetResilienceRegenScale('])
|
||||||
#include <cassert>
|
methods=methods.replace('override ','').replace('protected ','').replace('Replication.IsServer()','server').replace('super.','Native::')
|
||||||
'''+rule+'''
|
run('''#include <cassert>
|
||||||
int main(){
|
bool server=true;
|
||||||
assert(AllowedDamage(-40,10,100,0)==-40); // medical treatment must pass unchanged
|
struct Native {bool unconscious=false,changed=false;float regen=1;int writes=0;bool bleeding=false;
|
||||||
assert(AllowedDamage(0,10,100,0)==0);
|
void SetPermitUnconsciousness(bool value,bool c){unconscious=value;changed=c;writes++;}
|
||||||
assert(AllowedDamage(5,100,100,0)==5); // ordinary damage still wounds
|
void SetRegenScale(float value,bool){regen=value;writes++;}
|
||||||
assert(AllowedDamage(100000,100,100,0)==99); // lethal blow stops above destruction
|
bool IsIndefinitelyUnconscious(bool){return true;}bool IsBleeding(){return bleeding;}float GetResilienceRegenScale(){return .8f;}
|
||||||
assert(AllowedDamage(100000,30,100,0.2f)>8.99f&&AllowedDamage(100000,30,100,0.2f)<9.01f);
|
};
|
||||||
assert(AllowedDamage(50,1,100,0)==0); // repeated damage cannot finish casualty
|
struct Policy:Native{bool m_ERRRevivablePlayer=false;
|
||||||
assert(AllowedDamage(50,0,100,0)==0); // never turn positive damage into free healing
|
'''+methods+'''
|
||||||
|
};
|
||||||
|
int main(){Policy p;server=false;p.ERR_EnableCasualtyProtection();assert(!p.m_ERRRevivablePlayer&&p.writes==0);
|
||||||
|
server=true;p.ERR_EnableCasualtyProtection();assert(p.unconscious&&p.changed&&p.regen==.25f);
|
||||||
|
assert(!p.IsIndefinitelyUnconscious());p.bleeding=true;assert(p.GetResilienceRegenScale()==0);
|
||||||
|
p.bleeding=false;assert(p.GetResilienceRegenScale()==.8f);Policy ai;assert(ai.IsIndefinitelyUnconscious());ai.bleeding=true;assert(ai.GetResilienceRegenScale()==.8f);
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: lethal and repeated damage clamp above configured destruction thresholds; ordinary wounds and healing retain their values')
|
# Execute the real damage cap across wound/blood scales; healing is untouched.
|
||||||
|
cap=body(p, 'static float AllowedDamage(').replace('Math.Clamp','std::clamp').replace('Math.Min','std::min').replace('Math.Max','std::max').replace('0.01','0.01f').replace(', 1)',', 1.0f)').replace('(0,','(0.0f,')
|
||||||
|
run('''#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
'''+cap+'''
|
||||||
|
int main(){
|
||||||
|
for(float maximum:{100.0f,5000.0f}) for(float threshold:{0.0f,.05f,.2f}) {
|
||||||
|
float floor=maximum*(threshold+.01f);
|
||||||
|
assert(AllowedDamage(-10,maximum,maximum,threshold)==-10);
|
||||||
|
assert(AllowedDamage(100000, floor,maximum,threshold)==0);
|
||||||
|
assert(AllowedDamage(100000, floor/2,maximum,threshold)==0);
|
||||||
|
float health=maximum;
|
||||||
|
for(int i=0;i<1000;i++) health-=AllowedDamage(13,health,maximum,threshold);
|
||||||
|
assert(std::abs(health-floor)<.001f);
|
||||||
|
assert(AllowedDamage(1,maximum,maximum,threshold)==1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
# Passive resilience has its own native rate; treatment DPS bypasses this getter.
|
||||||
|
rate=body(p,'override float CalculatePassiveRegenDPS(').replace('override ','').replace('super.','Native::').replace('SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());','Manager* manager = target;').replace('manager.','manager->')
|
||||||
|
run('''#include <cassert>
|
||||||
|
struct Manager { bool player; bool ERR_IsRevivablePlayer(){return player;} };
|
||||||
|
struct Native {float CalculatePassiveRegenDPS(bool){return 4;}};
|
||||||
|
struct Policy:Native {Manager* target=nullptr;
|
||||||
|
'''+rate+'''
|
||||||
|
};
|
||||||
|
int main(){Policy p;assert(p.CalculatePassiveRegenDPS()==4);Manager ai{false};p.target=&ai;assert(p.CalculatePassiveRegenDPS()==4);ai.player=true;assert(p.CalculatePassiveRegenDPS()==1);assert(p.CalculatePassiveRegenDPS(false)==4);}
|
||||||
|
''')
|
||||||
|
assert 'SCR_CharacterResilienceHitZone.Cast(this)) return damage' in source
|
||||||
|
assert 'damage <= 0' in source and '!manager.ERR_IsRevivablePlayer()' in source
|
||||||
|
assert 'override void Kill(' in source
|
||||||
|
assert 'FullHeal(' not in source and 'SetHealth(' not in source
|
||||||
|
print('PASS: authority-scoped slow recovery, bleeding pauses resilience, medicine unaffected, player-only lethal damage cap without healing')
|
||||||
|
# Run the production recovery prediction over real effect states modeled at the
|
||||||
|
# native boundary, including a pending regeneration delay and exhausted effects.
|
||||||
|
future=body(p,'bool ERR_CanRecoverNaturally(')
|
||||||
|
future=future.replace('array<ref SCR_PersistentDamageEffect> effects','auto effects').replace('GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect, true)','GetAllPersistentEffectsOfType()')
|
||||||
|
future=future.replace('foreach (SCR_PersistentDamageEffect effect : effects)','for (Effect* effect : effects)').replace('DotDamageEffect dot = DotDamageEffect.Cast(effect);','Effect* dot = effect;').replace('HitZone zone','Zone* zone').replace('dot.','dot->').replace('zone.','zone->').replace('EDamageType.REGENERATION','1')
|
||||||
|
run('''#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
struct Zone {float value=0;float GetHealthScaled(){return value;}};
|
||||||
|
struct Effect {Zone* zone;float dps;int type=1;int GetDamageType(){return type;}float GetDPS(){return dps;}Zone* GetAffectedHitZone(){return zone;}};
|
||||||
|
struct Policy {bool m_ERRRevivablePlayer=true,bleeding=false;float scale=1;Zone blood,resilience;std::vector<Effect*> effects;
|
||||||
|
bool IsBleeding(){return bleeding;}auto GetAllPersistentEffectsOfType(){return effects;}
|
||||||
|
Zone* GetBloodHitZone(){return &blood;}Zone* GetResilienceHitZone(){return &resilience;}float GetResilienceRegenScale(){return scale;}
|
||||||
|
'''+future+'''
|
||||||
|
};
|
||||||
|
int main(){Policy p;assert(!p.ERR_CanRecoverNaturally());Effect e{&p.blood,-.1f};p.effects={&e};
|
||||||
|
assert(p.ERR_CanRecoverNaturally());p.bleeding=true;assert(!p.ERR_CanRecoverNaturally());p.bleeding=false;
|
||||||
|
p.blood.value=1;assert(!p.ERR_CanRecoverNaturally());p.blood.value=.2;
|
||||||
|
e.dps=0;assert(!p.ERR_CanRecoverNaturally());e.dps=-1;e.type=2;assert(!p.ERR_CanRecoverNaturally());e.type=1;
|
||||||
|
e.zone=&p.resilience;p.scale=0;assert(!p.ERR_CanRecoverNaturally());p.scale=.1;assert(p.ERR_CanRecoverNaturally());
|
||||||
|
p.m_ERRRevivablePlayer=false;assert(!p.ERR_CanRecoverNaturally());
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: viable passive recovery survives native delay; bleeding, disabled/full effects and low-blood resilience do not block defeat')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user