Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
+19 -3
View File
@@ -6,7 +6,9 @@ An independently authored replacement for the abandoned Escapists base mission,
The current base supplies prison spawning, two independent scavenging/clue routes, finite medical and outpost stock, an optional ground vehicle, physical radio documents, named discovery markers, native objectives, a separate radio-defence service, capacity-checked helicopter extraction, identity-based partial results, early helicopter-loss defeat, saved mission time/speed, defeat/restart, identity-based lives and local spectator following. Authored site definitions and an injected vanilla faction/asset catalog own the initial physical population. Core provides administration and startup diagnostics; the AI, Intel, Chat and Map modules extend its interfaces inside one addon. HQ coordinates enemy responses through physical radios and available forces.
The [local development playtest handoff](docs/playtest-build.md) contains the earlier `5e74269` checkpoint: five native addon packages, matching source and separate manual launch scripts. It does not contain the newer HQ memory or RHS/WCS/BMP integration. Two scripted Core escape/restart rounds pass616 assertions; packaged Core and full-companion startup both pass. Full human completion and multiplayer remain to be tested.
The current working tree passes **326 registered regression programs** after the [5 October validation repairs](docs/evidence/review-audit-2026-10-05/review.json). These offline checks do not establish native gameplay or multiplayer correctness.
The [4 October development playtest handoff](.build/playtest-2026-10-04-ui-bmp/README.md) includes the inventory callback and BMP ammunition-accounting corrections. Its 439 authored files match 378 PAK payloads and three runtime files. At that archived checkpoint, code/tests/tools bound to 309 passing regression programs; exact-source normal escape and ordinary packaged startup complete with retained engine errors. It remains an **unqualified development candidate**. Existing missions and prior packages are preserved. See [current status](docs/current-status.md) for exact scope and limits.
The [AI migration checkpoint](docs/ai-migration-progress.md) adds delivered-report memory, bounded trajectory prediction, physical infantry pursuit, status checks, body observations and finite equipment. Native memory and actual-sighting response fixtures pass their assertions, with four retained upstream localization errors. See [current development status](docs/current-status.md) for source-bound checkpoints. The earlier five-addon checkpoint shows the [real prisoner view and starting objective](docs/evidence/game-2026-09-19-125906-integrated-prison/ordinary-prison-view.png). Native tests cover scripted escape/restart, finite mortar support, physical radio delivery, shared-map ownership, and the reusable aircraft controller's vertical flight, airborne cancellation and an isolated 20 m fixed route. Archived five-addon packages pass ordinary Chat startup without loose successor source and compile/prepare the empty mission on an isolated Linux server. The latter retains four installed-asset errors reproduced by a vanilla control.
@@ -18,7 +20,12 @@ Later ordinary input checks demonstrate walking, clean inventory interaction and
Open **`addons/core/addon.gproj`**, titled **Co10 Escape Reintroduced**. This is the
single project to develop, package and eventually publish as one Workshop mod.
Use **`Missions/ERR_Everon.conf`** (`{C1E20667DCE324ED}`) for the full scenario.
Select **Co10 Escape Reintroduced — Automatic terrain**,
**`Missions/ERR_Everon.conf`** (`{C1E20667DCE324ED}`), as the universal starting
scenario. It opens Escape on Everon by default; a saved choice in the admin
**Terrains** menu loads another installed terrain after restart. The existing
resource ID is unchanged. The public browser may keep the generic Automatic
terrain title after switching maps; it no longer labels this entry as Everon.
The Core directory and addon GUID `2528466DE220D91C` are retained for continuity.
Scripts are organized under `addons/core/Scripts/Game/ERR/`:
@@ -42,6 +49,15 @@ See the [single-mod migration and current commands](docs/single-mod.md).
Earlier five-addon build reports below are historical evidence, not validation of
the merged project. Nothing has been published to Workshop.
[Automatic terrain takeover](docs/terrain-takeover.md) starts Escape when an admin
loads Escape alongside a terrain and selects that terrain's existing scenario.
No connector, custom startup flag or airport coordinates are required. The map
must provide usable navigation and safe placement space. Published authenticated
multiplayer validation remains outstanding; see the linked test scope.
Terrain authors can optionally use the [shared connector prefab](docs/terrain-adapters.md)
for an authored Escape scenario; [Nogova setup](docs/nogova-publishing.md) describes
both routes.
See the [player route playtest guide](docs/player-route.md), [player lifecycle](docs/player-lifecycle.md), [manual gameplay gaps](docs/base-gameplay-gaps.md), [release qualification](docs/release-qualification.md), [performance comparison tooling](docs/performance-comparison.md), [configuration and commands](docs/configuration.md), [site authoring](docs/site-authoring.md), [architecture](docs/architecture.md) and [migration priorities](docs/base-replacement.md).
## Validation
@@ -60,4 +76,4 @@ The [development source bundle tool](docs/development-source-bundle.md) previews
Random field hospitals and camps, their permanent/mobile guard split, finite supplies and authoring options are documented in [Field sites](docs/field-sites.md).
Town population and its 700-metre spawning rule: [town garrisons](docs/town-garrisons.md).
Town population, configurable factory guards and safe tower/roof posts: [town and object garrisons](docs/town-garrisons.md).
@@ -0,0 +1,6 @@
MatPBRBasic {
Color 0.48 0.018 0.012 1
MetalnessScale 0
RoughnessScale 0.75
BCRMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{F6575DAA717C6693}Assets/Items/ERR_Atlas/AtlasCover.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
@@ -0,0 +1,6 @@
MatPBRBasic {
Color 0.73 0.70 0.62 1
MetalnessScale 0
RoughnessScale 0.75
BCRMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{C029F1CCABDA2C3E}Assets/Items/ERR_Atlas/AtlasPaper.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
@@ -0,0 +1,7 @@
MatPBRBasic {
Color 0.882 0.847 0.792 1
AlphaTest 0.5
AllowUserAlphaBias 1
BCRMap "$rendertarget"
OpacityMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds" "{A64A8C63E0FBAE87}system/textures/defWhite.edds" anim( 0 )
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{CE2D59FD9280F47D}Assets/Items/ERR_Atlas/AtlasPrint.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
@@ -0,0 +1,64 @@
AnimSrcGraphFile {
Sheets {
AnimSrcGraphSheet Atlas {
Nodes {
AnimSrcNodeStateMachine ERRAtlasControl {
states {
AnimSrcNodeState Idle { Child "MasterControl" IsExit 1 }
AnimSrcNodeState Turn { Child "ERRAtlasHandIK" TimeStorage "Real Time" }
AnimSrcNodeState TurnBack { Child "ERRAtlasBackHandIK" TimeStorage "Real Time" }
}
transitions {
AnimSrcNodeTransition { FromState "Idle" ToState "Turn" Duration "0.25" Condition "IsCommand(CMD_HealSelf) && GetCommandI(CMD_HealSelf) == 1" }
AnimSrcNodeTransition { FromState "Idle" ToState "TurnBack" Duration "0.25" Condition "IsCommand(CMD_HealSelf) && GetCommandI(CMD_HealSelf) == 2" }
AnimSrcNodeTransition { FromState "TurnBack" ToState "Idle" Duration "0.25" Condition "GetStateTime() > 0.95 || GetCommandI(CMD_HealSelf) == -1" }
AnimSrcNodeTransition { FromState "Turn" ToState "Idle" Duration "0.25" Condition "GetStateTime() > 0.95 || GetCommandI(CMD_HealSelf) == -1" }
}
}
AnimSrcNodeIK2Target ERRAtlasHandTarget {
Child "MasterControl"
Bones {
AnimSrcNodeIK2BoneTarget { IkTarget "ERRAtlasRightHand" Bone "LeftHandProp" }
}
}
AnimSrcNodeProcTransform ERRAtlasHandOffset {
Child "ERRAtlasHandTarget"
Expression "1"
IkTargets {
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis X Amount "-0.068 + 0.21*cos(3.141593*curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1)))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Y Amount "0.02 + 0.055*sin(3.141593*curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1)))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Z Amount "-0.065" Space Local IkTarget "ERRAtlasRightHand" }
}
}
AnimSrcNodeIK2 ERRAtlasHandIK {
Tags { "TagLItemAction" }
Child "ERRAtlasHandOffset"
Weight "1"
Chains {
AnimSrcIkBinding { IkTarget "ERRAtlasRightHand" IkChain "ERRAtlasRightArm" }
}
Solver AnimSrcNodeIK2TwoBoneSolver { SnapRotation 0 AllowStretching 0 }
Version 1
}
AnimSrcNodeProcTransform ERRAtlasBackHandOffset {
Child "ERRAtlasHandTarget"
Expression "1"
IkTargets {
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis X Amount "-0.068 + 0.21*cos(3.141593*(1-curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1))))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Y Amount "0.02 + 0.055*sin(3.141593*(1-curve_s(clamp((GetUpperRTime()-0.25)/0.65,0,1))))" Space Local IkTarget "ERRAtlasRightHand" }
AnimSrcNodeProcTrIkTargetItem { Operation Translate Axis Z Amount "-0.065" Space Local IkTarget "ERRAtlasRightHand" }
}
}
AnimSrcNodeIK2 ERRAtlasBackHandIK {
Tags { "TagLItemAction" }
Child "ERRAtlasBackHandOffset"
Weight "1"
Chains {
AnimSrcIkBinding { IkTarget "ERRAtlasRightHand" IkChain "ERRAtlasRightArm" }
}
Solver AnimSrcNodeIK2TwoBoneSolver { SnapRotation 0 AllowStretching 0 }
Version 1
}
}
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{B428122ACA3850BC}Assets/Items/ERR_Atlas/AtlasTurn.agf"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} ResourceClass XBOX_ONE : PC {} ResourceClass XBOX_SERIES : PC {} ResourceClass PS4 : PC {} ResourceClass PS5 : PC {} }
}
@@ -0,0 +1,70 @@
AnimSrcGraph {
AnimSetTemplate "{91953B0B3D746630}Assets/Items/Equipment/workspaces/paper_map.ast"
ControlTemplate AnimSrcGCT "{55567D08724BC61D}" {
Variables {
AnimSrcGCTVarFloat AimY {
MinValue -90
MaxValue 90
}
AnimSrcGCTVarInt Stance {
DefaultValue 0
MaxValue 2
}
AnimSrcGCTVarBool Low {
DefaultValue 0
}
AnimSrcGCTVarFloat AimX {
MinValue -90
MaxValue 90
}
AnimSrcGCTVarBool Temp {
}
AnimSrcGCTVarInt WeaponType {
MaxValue 20
}
AnimSrcGCTVarBool FreeLook {
}
}
Commands { AnimSrcGCTCmd CMD_HealSelf { } }
IkChains {
AnimSrcGCTIkChain ERRAtlasRightArm {
Joints { "rightarm" "rightarmtwist" "rightforearm" "rightforearmtwist" "righthand" }
MiddleJoint "rightforearm"
ChainAxis "+y"
}
AnimSrcGCTIkChain Head {
Joints {
"neck2"
"neck3"
"head"
}
MiddleJoint "neck3"
ChainAxis "+y"
}
AnimSrcGCTIkChain LeftArm {
Joints {
"leftarm"
"leftarmtwist"
"leftforearm"
"leftforearmtwist"
"lefthand"
}
MiddleJoint "leftforearm"
ChainAxis "+y"
}
}
GlobalTags {
"ITEM"
"GADGET"
"MAP"
"PaperMap01"
}
}
Debug AnimSrcGD "{55567D08724BC783}" {
}
GraphFilesResourceNames {
"{B428122ACA3850BC}Assets/Items/ERR_Atlas/AtlasTurn.agf"
"{6F87679E2512527D}Assets/Items/Equipment/workspaces/map.agf"
}
DefaultRunNode "ERRAtlasControl"
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{4959464B088CCCA4}Assets/Items/ERR_Atlas/AtlasTurn.agr"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} ResourceClass XBOX_ONE : PC {} ResourceClass XBOX_SERIES : PC {} ResourceClass PS4 : PC {} ResourceClass PS5 : PC {} }
}
@@ -0,0 +1,6 @@
MatPBRBasic {
Color 0.8 0.8 0.78 1
MetalnessScale 0.65
RoughnessScale 0.75
BCRMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds"
}
@@ -0,0 +1,11 @@
MetaFileClass {
Name "{993AED2484A6829B}Assets/Items/ERR_Atlas/AtlasWire.emat"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
ResourceClass XBOX_ONE : PC {}
ResourceClass XBOX_SERIES : PC {}
ResourceClass PS4 : PC {}
ResourceClass PS5 : PC {}
}
}
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
@@ -0,0 +1,85 @@
MetaFileClass {
Name "{51ABB9E2E838FC9E}Assets/Items/ERR_Atlas/ERR_Atlas.xob"
Configurations {
FBXResourceClass PC {
MaterialAssigns {
MaterialAssignClass "{6A9727841D4273B0}" {
SourceMaterial "AtlasPrint"
AssignedMaterial "{CE2D59FD9280F47D}Assets/Items/ERR_Atlas/AtlasPrint.emat"
}
MaterialAssignClass "{6A9727841D427046}" {
SourceMaterial "AtlasPaper"
AssignedMaterial "{C029F1CCABDA2C3E}Assets/Items/ERR_Atlas/AtlasPaper.emat"
}
MaterialAssignClass "{6A9727841D427075}" {
SourceMaterial "AtlasCover"
AssignedMaterial "{F6575DAA717C6693}Assets/Items/ERR_Atlas/AtlasCover.emat"
}
MaterialAssignClass "{6A9727841D427065}" {
SourceMaterial "AtlasWire"
AssignedMaterial "{993AED2484A6829B}Assets/Items/ERR_Atlas/AtlasWire.emat"
}
}
MeshParams {
MeshParam BackCover {
}
MeshParam FrontCover {
}
MeshParam PageBlock {
}
MeshParam PageEdge_00 {
}
MeshParam PageEdge_01 {
}
MeshParam PageEdge_02 {
}
MeshParam PageEdge_03 {
}
MeshParam PageEdge_04 {
}
MeshParam PageEdge_05 {
}
MeshParam PageEdge_06 {
}
MeshParam PageEdge_07 {
}
MeshParam PageEdge_08 {
}
MeshParam PageEdge_09 {
}
MeshParam PageEdge_10 {
}
MeshParam PageEdge_11 {
}
MeshParam PrintedSheet {
}
MeshParam SpiralWire {
}
}
GeometryParams {
GeometryParam UBX_Atlas {
LayerPreset "ItemFireView"
SurfaceProperties {
"{AC1A1CDEE58FEE27}Common/Materials/Game/paper.gamemat"
}
Mass 0.35
}
}
Common TXOCommonClass "{6A9727841D4270C8}" : "{0877E7C4BB2B2C9A}Configs/System/ResourceTypes/PC/MeshObjectCommon.conf" {
}
ApplicationVendor "Blender Foundation"
ApplicationName "Blender (stable FBX IO)"
ApplicationVersion "4.5.3 LTS"
}
FBXResourceClass HEADLESS : PC {
}
FBXResourceClass XBOX_ONE : PC {
}
FBXResourceClass XBOX_SERIES : PC {
}
FBXResourceClass PS4 : PC {
}
FBXResourceClass PS5 : PC {
}
}
}
Binary file not shown.
@@ -0,0 +1,374 @@
$object "" {
#version 1.0
#tag MergeMeshes "1"
#tag Coords "XZY"
#tag Scale "1.000000"
#tag SrcFile_AppVendor "Blender Foundation"
#tag SrcFile_AppName "Blender (stable FBX IO)"
#tag SrcFile_AppVersion "4.5.3 LTS"
$materials {
$material "AtlasPaper" {
#UVset 0
}
$material "AtlasPrint" {
#UVset 0
}
}
$lod 0 {
#lodFactor 0.5
$mesh "LeafBack" {
#texCoords 1
$faces {
0 4 0 1 2 3
0 4 4 5 6 7
0 4 8 9 10 11
0 4 12 13 14 15
0 4 16 17 18 19
0 4 20 21 22 23
0 4 24 25 26 27
0 4 28 29 30 31
0 4 32 33 34 35
0 4 36 37 38 39
0 4 40 41 42 43
0 4 44 45 46 47
0 4 48 49 50 51
0 4 52 53 54 55
0 4 56 57 58 59
0 4 60 61 62 63
}
$faceVerts {
0 0 0
1 0 1
3 0 2
2 0 3
2 1 3
3 1 2
5 1 4
4 1 5
4 2 5
5 2 4
7 2 6
6 2 7
6 3 7
7 3 6
9 3 8
8 3 9
8 4 9
9 4 8
11 4 10
10 4 11
10 5 11
11 5 10
13 5 12
12 5 13
12 6 13
13 6 12
15 6 14
14 6 15
14 7 15
15 7 14
17 7 16
16 7 17
16 8 17
17 8 16
19 8 18
18 8 19
18 9 19
19 9 18
21 9 20
20 9 21
20 10 21
21 10 20
23 10 22
22 10 23
22 11 23
23 11 22
25 11 24
24 11 25
24 12 25
25 12 24
27 12 26
26 12 27
26 13 27
27 13 26
29 13 28
28 13 29
28 14 29
29 14 28
31 14 30
30 14 31
30 15 31
31 15 30
33 15 32
32 15 33
}
$verts {
0 -0 0.1485 0
0 0 -0.1485 0
0.011937 0.00078 0.1485 0
0.011937 0.00078 -0.1485 0
0.023875 0.001531 0.1485 0
0.023875 0.001531 -0.1485 0
0.035813 0.002222 0.1485 0
0.035813 0.002222 -0.1485 0
0.04775 0.002828 0.1485 0
0.04775 0.002828 -0.1485 0
0.059687 0.003326 0.1485 0
0.059687 0.003326 -0.1485 0
0.071625 0.003696 0.1485 0
0.071625 0.003696 -0.1485 0
0.083563 0.003923 0.1485 0
0.083563 0.003923 -0.1485 0
0.0955 0.004 0.1485 0
0.0955 0.004 -0.1485 0
0.107437 0.003923 0.1485 0
0.107437 0.003923 -0.1485 0
0.119375 0.003696 0.1485 0
0.119375 0.003696 -0.1485 0
0.131313 0.003326 0.1485 0
0.131313 0.003326 -0.1485 0
0.14325 0.002828 0.1485 0
0.14325 0.002828 -0.1485 0
0.155188 0.002222 0.1485 0
0.155188 0.002222 -0.1485 0
0.167125 0.001531 0.1485 0
0.167125 0.001531 -0.1485 0
0.179063 0.00078 0.1485 0
0.179063 0.00078 -0.1485 0
0.191 -0 0.1485 0
0.191 0 -0.1485 0
}
$texCoords {
0.10753 0
0.10753 1
0.16331 1
0.16331 0
0.21909 1
0.21909 0
0.27487 1
0.27487 0
0.33065 1
0.33065 0
0.38642 1
0.38642 0
0.4422 1
0.4422 0
0.49798 1
0.49798 0
0.55376 1
0.55376 0
0.60954 1
0.60954 0
0.66532 1
0.66532 0
0.7211 1
0.7211 0
0.77688 1
0.77688 0
0.83266 1
0.83266 0
0.88844 1
0.88844 0
0.94422 1
0.94422 0
1 1
1 0
}
$normals {
0.0652 -0.9979 -0
0.0627 -0.998 -0
0.0578 -0.9983 -0
0.0507 -0.9987 -0
0.0416 -0.9991 -0
0.0309 -0.9995 -0
0.0191 -0.9998 -0
0.0064 -1 -0
-0.0064 -1 -0
-0.0191 -0.9998 -0
-0.0309 -0.9995 -0
-0.0416 -0.9991 -0
-0.0507 -0.9987 -0
-0.0578 -0.9983 -0
-0.0627 -0.998 -0
-0.0652 -0.9979 -0
}
}
$mesh "TurningLeaf" {
#texCoords 1
$faces {
1 4 0 1 2 3
1 4 4 5 6 7
1 4 8 9 10 11
1 4 12 13 14 15
1 4 16 17 18 19
1 4 20 21 22 23
1 4 24 25 26 27
1 4 28 29 30 31
1 4 32 33 34 35
1 4 36 37 38 39
1 4 40 41 42 43
1 4 44 45 46 47
1 4 48 49 50 51
1 4 52 53 54 55
1 4 56 57 58 59
1 4 60 61 62 63
}
$faceVerts {
2 0 0
3 0 1
1 0 2
0 0 3
4 1 4
5 1 5
3 1 1
2 1 0
6 2 6
7 2 7
5 2 5
4 2 4
8 3 8
9 3 9
7 3 7
6 3 6
10 4 10
11 4 11
9 4 9
8 4 8
12 5 12
13 5 13
11 5 11
10 5 10
14 6 14
15 6 15
13 6 13
12 6 12
16 7 16
17 7 17
15 7 15
14 7 14
18 8 18
19 8 19
17 8 17
16 8 16
20 9 20
21 9 21
19 9 19
18 9 18
22 10 22
23 10 23
21 10 21
20 10 20
24 11 24
25 11 25
23 11 23
22 11 22
26 12 26
27 12 27
25 12 25
24 12 24
28 13 28
29 13 29
27 13 27
26 13 26
30 14 30
31 14 31
29 14 29
28 14 28
32 15 32
33 15 33
31 15 31
30 15 30
}
$verts {
0 -0 0.1485 0
0 0 -0.1485 0
0.011937 0.00078 0.1485 0
0.011937 0.00078 -0.1485 0
0.023875 0.001531 0.1485 0
0.023875 0.001531 -0.1485 0
0.035813 0.002222 0.1485 0
0.035813 0.002222 -0.1485 0
0.04775 0.002828 0.1485 0
0.04775 0.002828 -0.1485 0
0.059687 0.003326 0.1485 0
0.059687 0.003326 -0.1485 0
0.071625 0.003696 0.1485 0
0.071625 0.003696 -0.1485 0
0.083563 0.003923 0.1485 0
0.083563 0.003923 -0.1485 0
0.0955 0.004 0.1485 0
0.0955 0.004 -0.1485 0
0.107437 0.003923 0.1485 0
0.107437 0.003923 -0.1485 0
0.119375 0.003696 0.1485 0
0.119375 0.003696 -0.1485 0
0.131313 0.003326 0.1485 0
0.131313 0.003326 -0.1485 0
0.14325 0.002828 0.1485 0
0.14325 0.002828 -0.1485 0
0.155188 0.002222 0.1485 0
0.155188 0.002222 -0.1485 0
0.167125 0.001531 0.1485 0
0.167125 0.001531 -0.1485 0
0.179063 0.00078 0.1485 0
0.179063 0.00078 -0.1485 0
0.191 -0 0.1485 0
0.191 0 -0.1485 0
}
$texCoords {
0.16331 0
0.16331 1
0.10753 1
0.10753 0
0.21909 0
0.21909 1
0.27487 0
0.27487 1
0.33065 0
0.33065 1
0.38642 0
0.38642 1
0.4422 0
0.4422 1
0.49798 0
0.49798 1
0.55376 0
0.55376 1
0.60954 0
0.60954 1
0.66532 0
0.66532 1
0.7211 0
0.7211 1
0.77688 0
0.77688 1
0.83266 0
0.83266 1
0.88844 0
0.88844 1
0.94422 0
0.94422 1
1 0
1 1
}
$normals {
-0.0652 0.9979 0
-0.0627 0.998 0
-0.0578 0.9983 0
-0.0507 0.9987 0
-0.0416 0.9991 0
-0.0309 0.9995 0
-0.0191 0.9998 0
-0.0064 1 0
0.0064 1 0
0.0191 0.9998 0
0.0309 0.9995 0
0.0416 0.9991 0
0.0507 0.9987 0
0.0578 0.9983 0
0.0627 0.998 0
0.0652 0.9979 0
}
}
}
}
Binary file not shown.
@@ -0,0 +1,38 @@
MetaFileClass {
Name "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob"
Configurations {
FBXResourceClass PC {
MaterialAssigns {
MaterialAssignClass "{6A9727841D4273B0}" {
SourceMaterial "AtlasPrint"
AssignedMaterial "{CE2D59FD9280F47D}Assets/Items/ERR_Atlas/AtlasPrint.emat"
}
MaterialAssignClass "{6A9727841D427046}" {
SourceMaterial "AtlasPaper"
AssignedMaterial "{C029F1CCABDA2C3E}Assets/Items/ERR_Atlas/AtlasPaper.emat"
}
}
MeshParams {
MeshParam LeafBack {
}
MeshParam TurningLeaf {
}
}
Common TXOCommonClass "{6A9727841D4270C8}" : "{0877E7C4BB2B2C9A}Configs/System/ResourceTypes/PC/MeshObjectCommon.conf" {
}
ApplicationVendor "Blender Foundation"
ApplicationName "Blender (stable FBX IO)"
ApplicationVersion "4.5.3 LTS"
}
FBXResourceClass HEADLESS : PC {
}
FBXResourceClass XBOX_ONE : PC {
}
FBXResourceClass XBOX_SERIES : PC {
}
FBXResourceClass PS4 : PC {
}
FBXResourceClass PS5 : PC {
}
}
}
+15
View File
@@ -30,12 +30,22 @@ ERR_ScenarioDefinition {
GuardOffset 18 0 0
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
ContainerOffset 0 0 0
Transport ERR_SiteTransport {
Optional 1
Prefab "{FA65287C19DE304B}Prefabs/ERR/Vehicles/SupplyUAZ.et"
Offset 0 0 -12
Heading 180
ClearanceRadius 4
MaximumTerrainRange 1
}
Stock {
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 8 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 16 }
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 1 }
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
ERR_SiteStock { Prefab "{C09C7C97EFA34909}Prefabs/ERR/Items/EmergencyRepairKit.et" Count 1 }
ERR_SiteStock { Prefab "{12D5AD21E383B768}Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.et" Count 2 }
}
RevealOnEscape 1
}
@@ -121,6 +131,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
}
Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -244,6 +255,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
}
Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -273,6 +285,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
}
Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -302,6 +315,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
}
Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -331,6 +345,7 @@ ERR_ScenarioDefinition {
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
}
Stock {
ERR_SiteStock { Prefab "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et" Count 2 }
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
@@ -1,5 +1,93 @@
ActionManager : "{795184CF9AD764DB}Configs/System/chimeraInputCommon.conf" {
Contexts {
ActionContext ERR_FieldMapContext {
Priority 60
Flags 0xa 0
Actions {
Action ERR_AtlasDigit0 {
InputSource InputSourceValue "{A7B19F416E541020}" {
Input "keyboard:KC_NUMPAD0"
}
}
Action ERR_AtlasDigit1 {
InputSource InputSourceValue "{A7B19F416E541021}" {
Input "keyboard:KC_NUMPAD1"
}
}
Action ERR_AtlasDigit2 {
InputSource InputSourceValue "{A7B19F416E541022}" {
Input "keyboard:KC_NUMPAD2"
}
}
Action ERR_AtlasDigit3 {
InputSource InputSourceValue "{A7B19F416E541023}" {
Input "keyboard:KC_NUMPAD3"
}
}
Action ERR_AtlasDigit4 {
InputSource InputSourceValue "{A7B19F416E541024}" {
Input "keyboard:KC_NUMPAD4"
}
}
Action ERR_AtlasDigit5 {
InputSource InputSourceValue "{A7B19F416E541025}" {
Input "keyboard:KC_NUMPAD5"
}
}
Action ERR_AtlasDigit6 {
InputSource InputSourceValue "{A7B19F416E541026}" {
Input "keyboard:KC_NUMPAD6"
}
}
Action ERR_AtlasDigit7 {
InputSource InputSourceValue "{A7B19F416E541027}" {
Input "keyboard:KC_NUMPAD7"
}
}
Action ERR_AtlasDigit8 {
InputSource InputSourceValue "{A7B19F416E541028}" {
Input "keyboard:KC_NUMPAD8"
}
}
Action ERR_AtlasDigit9 {
InputSource InputSourceValue "{A7B19F416E541029}" {
Input "keyboard:KC_NUMPAD9"
}
}
Action ERR_AtlasEnter {
InputSource InputSourceSum "{A7B19F416E54102B}" {
Sources {
InputSourceValue "{A7B19F416E54102A}" {
Input "keyboard:KC_NUMPADENTER"
}
InputSourceValue "{A7B19F416E54102C}" {
Input "keyboard:KC_RETURN"
}
}
}
}
Action ERR_FieldMapLeft {
InputSource InputSourceValue "{A7B19F416E541010}" {
Input "keyboard:KC_LEFT"
}
}
Action ERR_FieldMapRight {
InputSource InputSourceValue "{A7B19F416E541011}" {
Input "keyboard:KC_RIGHT"
}
}
Action ERR_FieldMapUp {
InputSource InputSourceValue "{A7B19F416E541012}" {
Input "keyboard:KC_UP"
}
}
Action ERR_FieldMapDown {
InputSource InputSourceValue "{A7B19F416E541013}" {
Input "keyboard:KC_DOWN"
}
}
}
}
ActionContext ERR_ObserverContext : "{C538807EEE48BED8}Configs/System/ActionContext/ManualCameraContext.conf" {
Actions {
Action ERR_ObserverPrevious {
@@ -3,5 +3,11 @@ MatPBRBasic {
AlphaTest 0.5
AllowUserAlphaBias 1
BCRMap "$rendertarget"
GlobalMapsUVTransform MatUVTransform {
TilingU 1.5
TilingV 2
Rotation 270
OffsetU 1
}
OpacityMap "{A64A8C63E0FBAE87}system/textures/defWhite.edds" "{A64A8C63E0FBAE87}system/textures/defWhite.edds" anim( 0 )
}
+3 -2
View File
@@ -1,9 +1,10 @@
SCR_MissionHeader {
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
m_sName "Co10 Escape Reintroduced — Everon"
m_sName "Co10 Escape Reintroduced — Automatic terrain"
m_sAuthor "Jakub Jim Žáček"
m_sDescription "Independent escape prototype. Development scenario; runtime validation in progress."
m_sDescription "Escape from captivity, find equipment and intelligence, and arrange helicopter extraction while enemy forces hunt you."
m_sDetails "Universal Escape starting scenario. Starts on Everon unless an administrator has saved another terrain in the Escape mission menu. Add terrain mods to the server, select their scenario in the Terrains page, save and restart. No terrain connector is required. The server browser may keep this Automatic terrain title while the selected map is running."
m_sGameMode "Escape"
m_iPlayerCount 12
m_eSaveTypes 0
-11
View File
@@ -1,11 +0,0 @@
SCR_MissionHeader {
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
m_sName "Co10 Escape Reintroduced — Everon"
m_sAuthor "Jakub Jim Žáček"
m_sDescription "Legacy diagnostic header for the integrated Co10 Escape Reintroduced mod."
m_sGameMode "Escape"
m_iPlayerCount 12
m_eSaveTypes 0
m_bShowInScenarioMenu 0
}
@@ -1,9 +0,0 @@
MetaFileClass {
Name "{AD62322D5213C440}Missions/ERR_Everon_Base.conf"
Configurations {
ResourceClass PC {
}
ResourceClass HEADLESS : PC {
}
}
}
@@ -0,0 +1,23 @@
GenericEntity : "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" {
ID "59DAB68B83510861"
components {
InventoryItemComponent "{5222EB4D0C73006B}" {
Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" {
ItemDisplayName SCR_InventoryUIInfo "{5222EB4D07D865FA}" {
Name "Combat epinephrine (strong)"
Description "Stronger field injector carried by special troops and medics. Stops bleeding, restores consciousness, boosts dangerously low blood and partially heals injuries. Cannot revive the dead."
m_aItemHints {}
}
}
}
SCR_ConsumableItemComponent "{59E68C74C5CEE74F}" {
m_ConsumableEffect ERR_ConsumableCombatEpinephrine "{59E68C74C1F3910E}" {
m_fItemRegenerationSpeed 0
m_fItemRegenerationDuration 0
m_aDamageEffectsToLoad {
ERR_CombatEpinephrineEffect "{6122D60861B541E7}" {}
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{792634D9359AED21}Prefabs/ERR/CombatEpinephrine.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
+7
View File
@@ -0,0 +1,7 @@
GenericEntity {
ID "46B3FAD3A89A4EA3"
components {
ERR_LocationComponent "{606DF66235E34CA9}" { m_Kind 0 }
RplComponent "{B6AB8FE43FE143B4}" { Streamable Disabled }
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
+9
View File
@@ -0,0 +1,9 @@
GenericEntity : "{74DCE1AEC6832E18}Prefabs/Props/Wrecks/UH1H_wreck/UH1H_wreck_static.et" {
ID "BDBAE020F10A41DF"
components {
Hierarchy "{BD920DE465D04435}" {}
RplComponent "{7E4852E4567E49D6}" { Streamable Disabled }
}
coords 0 0 0
angles 0 0 0
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"
Configurations {
ResourceClass PC {}
ResourceClass HEADLESS : PC {}
}
}
@@ -2,6 +2,9 @@ SCR_FactionManager : "{A242612865F2A48E}Prefabs/MP/Managers/Factions/FactionMana
ID "9D8B772E349A5ED5"
m_bCanChangeFactionsPlayable 1
Factions {
SCR_Faction : "{3FA20B01D950D31F}Configs/Factions/CIV.conf" {
m_bIsPlayable 0
}
SCR_Faction : "{8053DB656DD18B14}Configs/Factions/FIA.conf" {
m_aFriendlyFactionsIds { }
}
+1
View File
@@ -11,5 +11,6 @@ ERR_GameMode : "{0F307326459A1395}Prefabs/MP/Modes/GameMode_Base.et" {
m_SpawnLogic ERR_SpawnLogic "{79AD722B2EFD595F}" {}
}
}
m_fAutoReloadTime 10
m_bUseSpawnPreload 1
}
+8
View File
@@ -91,6 +91,14 @@ GenericEntity {
coords 0 0 -6
angles 0 0 0
}
StaticModelEntity : "{CAE1588B6265A2AB}Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Short_noStake.et" {
ID "F23719AD8B46C015"
components {
Hierarchy "{B86B0198EF40D762}" {}
}
coords 0 2.95 -6
angles 0 90 0
}
DecalEntity : "{4AD6EB80A8B1FA3A}Prefabs/Decals/Dirt/Decal_Dirt_Patch_03.et" {
ID "FCFBA935CC13E016"
components {
+10
View File
@@ -6,4 +6,14 @@ GenericEntity {
RigidBody "{4F20A90EDBD625A2}" { ModelGeometry 1 Static 1 }
RplComponent "{4F20A90EDBD625A3}" {}
}
{
StaticModelEntity : "{46904AF5D473FB75}Prefabs/Structures/Walls/BarbedWire/BarbedTape_01/BarbedTape_01_Long_noStake.et" {
ID "A32066F0B56D0EA9"
components {
Hierarchy "{F93ED27DEB1E2490}" {}
}
coords 1.5 2.95 0
angles 0 90 0
}
}
}
@@ -0,0 +1,27 @@
Vehicle : "{90B3A9E658D47B30}Prefabs/ERR/RescueEscortWest.et" {
ID "90B3A9E658D47B71"
components {
MeshObject "{51DAA09FEFBFC0E7}" {
Materials {
MaterialAssignClass "{6168D22F500C3196}" {
SourceMaterial "UH1H_Text_Decals_Ext_30EC8A12786836F9"
AssignedMaterial "{E244CE4CE64A92D8}Assets/Vehicles/Helicopters/UH1H/Data/UH1H_Text_Decals_Ext_CIV.emat"
}
MaterialAssignClass "{6168D22F500C31B3}" {
SourceMaterial "UH_1H_Body01_6224CA051369DE45"
AssignedMaterial "{FFDE0DE516E4816F}Assets/Vehicles/Helicopters/UH1H/Data/UH_1H_Body01_CIV.emat"
}
}
}
SCR_VehicleFactionAffiliationComponent "{5882CBD9AC741CEC}" {
"faction affiliation" "RHS_ION"
}
SlotManagerComponent "{55BCE45E438E4CFF}" {
Slots {
RegisteringComponentSlotInfo USArmyDecal {
Enabled 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B70}Prefabs/ERR/RescueEscortION.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,14 @@
Vehicle : "{DDDD9B51F1234DF3}Prefabs/Vehicles/Helicopters/UH1H/UH1H_armed.et" {
ID "90B3A9E658D47B31"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B32}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B30}Prefabs/ERR/RescueEscortWest.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,14 @@
Vehicle : "{7BD282AF716ED639}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MT_armed.et" {
ID "90B3A9E658D47B11"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B12}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B10}Prefabs/ERR/RescueHelicopter.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,14 @@
Vehicle : "{5BF04078A1EC66D8}Prefabs/Vehicles/Helicopters/Mi8MT/Mi8MTT-A_Black_no_pylons.et" {
ID "90B3A9E658D47B51"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B52}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B50}Prefabs/ERR/RescueHelicopterAFRF.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,16 @@
Vehicle : "{90B3A9E658D47B10}Prefabs/ERR/RescueHelicopter.et" {
ID "90B3A9E658D47B41"
components {
MeshObject "{51DAA09FEFBFC0E7}" {
Materials {
MaterialAssignClass "{6168E1F20FDD15C6}" {
SourceMaterial "Flag_decals_01_0418C5226701B4E1"
AssignedMaterial "{35D747E785C4D90F}Assets/Vehicles/Helicopters/Mi8/Data/MI8_Flag_decals_CIV_01.emat"
}
}
}
SCR_VehicleFactionAffiliationComponent "{5882CBD9AC741CEC}" {
"faction affiliation" "FIA"
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B40}Prefabs/ERR/RescueHelicopterFIA.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,27 @@
Vehicle : "{90B3A9E658D47B20}Prefabs/ERR/RescueHelicopterWest.et" {
ID "90B3A9E658D47B61"
components {
MeshObject "{51DAA09FEFBFC0E7}" {
Materials {
MaterialAssignClass "{6168D22F500C3196}" {
SourceMaterial "UH1H_Text_Decals_Ext_30EC8A12786836F9"
AssignedMaterial "{E244CE4CE64A92D8}Assets/Vehicles/Helicopters/UH1H/Data/UH1H_Text_Decals_Ext_CIV.emat"
}
MaterialAssignClass "{6168D22F500C31B3}" {
SourceMaterial "UH_1H_Body01_6224CA051369DE45"
AssignedMaterial "{FFDE0DE516E4816F}Assets/Vehicles/Helicopters/UH1H/Data/UH_1H_Body01_CIV.emat"
}
}
}
SCR_VehicleFactionAffiliationComponent "{5882CBD9AC741CEC}" {
"faction affiliation" "RHS_ION"
}
SlotManagerComponent "{55BCE45E438E4CFF}" {
Slots {
RegisteringComponentSlotInfo USArmyDecal {
Enabled 0
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B60}Prefabs/ERR/RescueHelicopterION.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
@@ -0,0 +1,24 @@
Vehicle : "{70BAEEFC2D3FEE64}Prefabs/Vehicles/Helicopters/UH1H/UH1H.et" {
ID "90B3A9E658D47B21"
components {
ERR_RescueFlightComponent "{90B3A9E658D47B22}" {
}
VehicleHelicopterSimulation "{51FAEE57F7DA270E}" {
Simulation Helicopter "{51FAED9EF37A2D29}" {
Engine HelicopterEngine "{51FAED9EF37A2D2B}" {
StartUpTime 0
}
}
}
SlotManagerComponent "{55BCE45E438E4CFF}" {
Slots {
RegisteringComponentSlotInfo Seat_Gunners {
Prefab "{67030528FA4FA725}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_gunners.et"
}
RegisteringComponentSlotInfo Seat_Cargo {
Prefab "{205599C7074F1E02}Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo.et"
}
}
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{90B3A9E658D47B20}Prefabs/ERR/RescueHelicopterWest.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS {}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
SCR_ManualCamera {
ERR_ObserverCamera {
ID "634904BCEDA1A285"
m_bInputEnabled 1
m_sActionContext "ERR_ObserverContext"
@@ -0,0 +1,26 @@
ERR_GameMode : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
ID "D12F4DF2625CA3AC"
components {
Hierarchy "{3DC8FB32DE3E1EBD}" {}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_TerrainIndependent 1
}
}
{
PerceptionManager Perception : "{028DAEAD63E056BE}Prefabs/World/Game/PerceptionManager.et" {
ID "1CD3AA95056E2D65"
}
SCR_FactionManager Factions : "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et" {
ID "9FE86AC6892BF013"
}
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
ID "588AD55F7655278B"
}
RadioManagerEntity RadioManager : "{B8E09FAB91C4ECCD}Prefabs/Systems/Radio/RadioManager.et" {
ID "5F87F7A144BDDE95"
}
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
ID "85ED17B0836DB4A2"
}
}
}
@@ -0,0 +1,7 @@
MetaFileClass {
Name "{8FB1559AE3F29722}Prefabs/ERR/TerrainConnector.et"
Configurations {
EntityTemplateResourceClass PC {}
EntityTemplateResourceClass HEADLESS : PC {}
}
}
@@ -0,0 +1,24 @@
ERR_TerrainTakeoverMode : "{978E045E53EF36C8}Prefabs/ERR/GameMode.et" {
ID "F39C0AB761E582D4"
components {
Hierarchy "{3DC8FB32DE3E1EBD}" {}
ERR_ScenarioComponent "{B78E910E88DE30D2}" {
m_TerrainIndependent 1
}
}
{
PerceptionManager Perception : "{028DAEAD63E056BE}Prefabs/World/Game/PerceptionManager.et" {
ID "1CD3AA95056E2D65"
}
SCR_FactionManager Factions : "{5414041A15DC4B54}Prefabs/ERR/FactionManager.et" {
ID "9FE86AC6892BF013"
}
SCR_LoadoutManager Loadouts : "{97EF4C1A41F63DDB}Prefabs/MP/Managers/Loadouts/LoadoutManager_USxUSSR.et" {
ID "588AD55F7655278B"
}
ScriptedChatEntity Chat : "{F69BC912AC8236F9}Prefabs/MP/ScriptedChatEntity.et" {
ID "85ED17B0836DB4A2"
components { RplComponent "{CA456708D69D73BA}" { Streamable Disabled } }
}
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{9D182739ABEF405C}Prefabs/ERR/TerrainTakeover.et"
Configurations { ResourceClass PC {} ResourceClass HEADLESS : PC {} }
}
@@ -0,0 +1,5 @@
GenericEntity {
components {
MeshObject { Object "{76D738EEB0CA431F}Assets/Items/ERR_Atlas/ERR_AtlasLeaf.xob" InheritVisibility 1 }
}
}
@@ -0,0 +1,4 @@
MetaFileClass {
Name "{385C5228D90F456B}Prefabs/Items/Equipment/ERRM_AtlasLeaf.et"
Configurations { EntityPrefabResourceClass PC {} EntityPrefabResourceClass HEADLESS : PC {} EntityPrefabResourceClass XBOX_ONE : PC {} EntityPrefabResourceClass XBOX_SERIES : PC {} EntityPrefabResourceClass PS4 : PC {} EntityPrefabResourceClass PS5 : PC {} }
}
@@ -4,8 +4,8 @@ GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
InventoryItemComponent "{5222EB4D0C73006B}" {
Attributes SCR_ItemAttributeCollection "{5222EB4D0A2B466B}" {
ItemDisplayName UIInfo "{7236A6DB18BD574C}" {
Name "Shared Field Map"
Description "A field map whose visible surface can be read by nearby players."
Name "OCRAM OLOP Autoatlas"
Description "Terrain atlas with an overview and numbered detail pages. Arrow keys turn sheets; numpad page number + Enter jumps directly; 0 + Enter returns to the index. Double-tap M opens the interactive map. No player marker."
}
ItemPhysAttributes ItemPhysicalAttributes PhysicalAttributes {
Weight 0.1
@@ -19,8 +19,9 @@ GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
ItemAnimationAttributes ItemAnimationAttributes "{4AC999738D39BA9A}" {
AnimationAttachments {
AnimationAttachmentInfo "{1D66E5B861D2F00A}" {
AnimGraph "{83675D0B0AB1B2C3}Assets/Items/Equipment/workspaces/paper_map.agr"
AnimGraph "{4959464B088CCCA4}Assets/Items/ERR_Atlas/AtlasTurn.agr"
AnimInstance "{56794534F6B8C3D7}Assets/Items/Equipment/workspaces/player_map.asi"
StartNode "ERRAtlasControl"
BindingName "Gadget"
}
}
@@ -46,13 +47,7 @@ GameEntity : "{C95E11C60810F432}Prefabs/Items/Core/Item_Base.et" {
}
}
MeshObject "{E66421D681D64AB7}" {
Object "{E443663E5241622A}Assets/Items/Equipment/Maps/Map_Paper_01/Map_Paper_01_folded.xob"
Materials {
MaterialAssignClass "{667CE655A40B0E50}" {
SourceMaterial "01__Default"
AssignedMaterial "{03D6DE777C8E411C}Materials/ERRM_MapRenderTarget.emat"
}
}
Object "{51ABB9E2E838FC9E}Assets/Items/ERR_Atlas/ERR_Atlas.xob"
InheritVisibility 1
}
RigidBody "{B29D8B091D657415}" {
@@ -8,12 +8,13 @@ class ERR_AdminMenuFields
{
static array<string> Keys()
{
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent"};
return {"aiDoctrine", "aiShadow", "automaticEquipmentChancePercent", "motorbikeCount", "planeAltitudeMetres", "planeReplacementSeconds", "planeSectorSeconds", "fieldCampCount", "fieldHospitalCount", "startWeaponCount", "startMagazineCount", "objectGuardCount", "objectGuardEvery", "objectGuardHQMode", "hunterSpawnDistanceMetres", "roadblockCount", "civilianTrafficCount", "startMode", "radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "viewDistanceMetres", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent", "heliTimerMinutes"};
}
static bool Available(string key, bool hq, bool intel)
{
if (key == "aiDoctrine" || key == "aiShadow") return hq;
if (key == "aiIntelChancePercent") return intel;
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
if (key == "automaticEquipmentChancePercent" || key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "heliTimerMinutes" || key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
return Keys().Contains(key);
}
// A short journal has no older off-screen lines, even if its slider is zero.
@@ -29,27 +30,56 @@ class ERR_AdminMenuFields
}
static string Display(string key, string value)
{
if (key == "aiDoctrine") { if (value == "1") return "Coordinated"; return "Current"; }
if (key == "aiShadow") { if (value == "1") return "On"; return "Off"; }
if (value == "-1" || value == "-2") return ERR_Settings.SelectionText(value.ToInt());
if (key == "startMode") { if (value == "1") return "Crash"; return "Prison"; }
if (key == "startTimeMinutes") return ERR_TimeService.Clock(value.ToInt());
if (key == "startDateCode") return ERR_EnvironmentService.Date(value.ToInt());
if (key == "weatherProfile") return ERR_EnvironmentService.ProfileName(value.ToInt());
if (key == "viewDistanceMetres" && value == "0") return "default";
return value;
}
static string StartSummary(int saved, int current)
{
return "Next start: " + Display("startMode", saved.ToString()) + " | Current round: " + Display("startMode", current.ToString());
}
static string Command(string key, string value)
{
string prefix;
if (key == "radioDefenceSeconds") prefix = "mission radio";
if (key == "aiDoctrine") prefix = "mission doctrine";
else if (key == "aiShadow") prefix = "mission aishadow";
else if (key == "automaticEquipmentChancePercent") prefix = "hq equipment";
else if (key == "motorbikeCount") prefix = "mission motorbikes";
else if (key == "planeReconEnabled") prefix = "mission plane";
else if (key == "planeAltitudeMetres") prefix = "mission planealtitude";
else if (key == "planeReplacementSeconds") prefix = "mission planereplacement";
else if (key == "planeSectorSeconds") prefix = "mission planesector";
else if (key == "startWeaponCount") prefix = "mission startweapons";
else if (key == "startMagazineCount") prefix = "mission startammo";
else if (key == "objectGuardEvery") prefix = "mission objectevery";
else if (key == "objectGuardHQMode") prefix = "mission objecthq";
else if (key == "objectGuardCount") prefix = "mission objectguards";
else if (key == "hunterSpawnDistanceMetres") prefix = "mission hunterdistance";
else if (key == "fieldCampCount") prefix = "mission camps";
else if (key == "fieldHospitalCount") prefix = "mission hospitals";
else if (key == "roadblockCount") prefix = "mission roadblocks";
else if (key == "civilianTrafficCount") prefix = "mission traffic";
else if (key == "startMode") prefix = "mission start";
else if (key == "radioDefenceSeconds") prefix = "mission radio";
else if (key == "aiIntelChancePercent") prefix = "intel carrying";
else if (key == "intelRevealChancePercent") prefix = "intel reveal";
else if (key == "startTimeMinutes") prefix = "mission time";
else if (key == "timeMultiplier") prefix = "mission speed";
else if (key == "startDateCode") prefix = "mission date";
else if (key == "weatherProfile") prefix = "mission weather";
else if (key == "viewDistanceMetres") prefix = "mission viewdistance";
else if (key == "reportIntervalSeconds") prefix = "hq interval";
else if (key == "supportSquadLimit") prefix = "hq squads";
else if (key == "responseRadiusMetres") prefix = "hq radius";
else if (key == "antiAirChancePercent") prefix = "hq aa";
else if (key == "antiPersonnelMineChancePercent") prefix = "hq mines";
else if (key == "heliTimerMinutes") prefix = "hq heli timer";
else if (key == "convoyCount") prefix = "hq convoy count";
value = value.Trim();
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
@@ -100,12 +130,21 @@ class ERR_AdminMenu : ChimeraMenuBase
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
if (!m_Controller) { Close(); return; }
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"};
array<string> names = {"PlaneOn", "PlaneOff", "Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS"};
names.Insert("RandomDay_startTimeMinutes");
names.Insert("Default_viewDistanceMetres");
names.Insert("OpenForces");
names.Insert("OpenGuards"); names.Insert("GuardsBack");
names.Insert("OpenSites"); names.Insert("SitesBack");
names.Insert("OpenPlane"); names.Insert("PlaneBack");
names.Insert("OpenAI"); names.Insert("AIBack");
names.Insert("EquipmentOverview"); names.Insert("OpenAllowedItems"); names.Insert("OpenItemPresets"); names.Insert("OpenWeights"); names.Insert("OpenRecommendations"); names.Insert("RecommendationsBack"); names.Insert("RecommendedPlane"); names.Insert("RecommendedWeapons");
for (int density = 1; density <= 4; density++) names.Insert("GuardEvery_" + density.ToString());
for (int policy = 0; policy <= 2; policy++) names.Insert("GuardHQ_" + policy.ToString());
foreach (string key : ERR_AdminMenuFields.Keys())
{
names.Insert("Apply_" + key);
names.Insert("Random_" + key);
if (ERR_Settings.Randomizable(key)) names.Insert("Random_" + key);
}
foreach (string name : names)
{
@@ -195,8 +234,8 @@ class ERR_AdminMenu : ChimeraMenuBase
if (widget) widget.SetText(value);
}
// Stable page order is also used by the shared navigation bar and native probe.
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus"}; }
static array<string> Pages() { return {"SettingsPage", "HQPage", "JournalPage", "ModsPage", "FactionsPage", "WorldPage", "IntelPage", "StatusPage", "TerrainsPage", "ItemsPage", "TTSPage", "ForcesPage", "GuardsPage", "SitesPage", "PlanePage", "RecommendationsPage", "WeightsPage", "AllowedItemsPage", "ItemPresetsPage", "AIPage"}; }
static array<string> Navigation() { return {"OpenSettings", "OpenHQ", "OpenJournal", "OpenMods", "OpenFactions", "OpenWorld", "OpenIntel", "OpenStatus", "OpenTerrains", "OpenItems", "OpenTTS", "OpenForces", "OpenGuards", "OpenSites", "OpenPlane", "OpenRecommendations", "OpenWeights", "OpenAllowedItems", "OpenItemPresets", "OpenAI"}; }
protected void ShowPage(int page)
{
array<string> pages = Pages();
@@ -206,31 +245,42 @@ class ERR_AdminMenu : ChimeraMenuBase
for (int i = 0; i < pages.Count(); i++)
{
GetRootWidget().FindAnyWidget(pages[i]).SetVisible(i == page);
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(i == page);
bool selected = i == page || ((page == 12 || page == 13) && i == 11) || ((page == 16 || page == 17 || page == 18) && i == 9) || ((page == 14 || page == 19) && i == 1);
GetRootWidget().FindAnyWidget("Selected_" + navigation[i]).SetVisible(selected);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText(navigation[i], GetRootWidget()), selected);
}
GetRootWidget().FindAnyWidget("EquipmentNavigation").SetVisible(page == 9 || page == 16 || page == 17 || page == 18);
GetRootWidget().FindAnyWidget("Selected_EquipmentOverview").SetVisible(page == 9);
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("EquipmentOverview", GetRootWidget()), page == 9);
if (page == 2) m_LogPoll = 1;
}
protected int m_KeepWeaponState = -1;
protected void UpdateActions()
{
ERR_SelectionStyle.Pair(GetRootWidget(), "KeepWeaponOn", "KeepWeaponOff", m_KeepWeaponState);
foreach (SCR_ButtonBaseComponent button : m_Buttons)
{
string name = button.GetRootWidget().GetName();
if (name.IndexOf("Apply_") == 0 || name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0)
if (name.IndexOf("Apply_") == 0 || name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0 || name.IndexOf("Default_") == 0)
{
int prefix = name.IndexOf("_") + 1;
string key = name.Substring(prefix, name.Length() - prefix);
button.SetEnabled(m_Ready && m_AvailableFields.Contains(key) && ERR_AdminMenuFields.Available(key, m_HQ, m_Intel));
}
else if (name == "PlaneOn" || name == "PlaneOff") button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("planeReconEnabled"));
else if (name.IndexOf("GuardEvery_") == 0) button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("objectGuardEvery"));
else if (name.IndexOf("GuardHQ_") == 0) button.SetEnabled(m_Ready && m_HQ && m_AvailableFields.Contains("objectGuardHQMode"));
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
else if (name == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
else if (name == "KeepWeaponOn" || name == "KeepWeaponOff") button.SetEnabled(m_Ready);
else if (name == "KeepWeaponOn") button.SetEnabled(m_Ready && m_KeepWeaponState == 0);
else if (name == "KeepWeaponOff") button.SetEnabled(m_Ready && m_KeepWeaponState == 1);
}
}
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
{
if (!authorized) { Close(); return; }
if (!values) return;
m_HQ = false; m_Intel = false; m_AvailableFields.Clear();
m_HQ = false; m_Intel = false; m_AvailableFields.Clear(); m_KeepWeaponState = -1;
foreach (string entry : values)
{
if (entry == "supports=hq") { m_HQ = true; continue; }
@@ -238,23 +288,58 @@ class ERR_AdminMenu : ChimeraMenuBase
int divider = entry.IndexOf("=");
if (divider <= 0) continue;
string key = entry.Substring(0, divider);
if (key == "recommendedBikeRuntime") { SetText("RecommendedBikeRuntime", entry.Substring(divider + 1, entry.Length() - divider - 1)); continue; }
if (key.IndexOf("recommended_") == 0)
{
SetText(key, entry.Substring(divider + 1, entry.Length() - divider - 1)); continue;
}
if (key.IndexOf("recommendedReady_") == 0)
{
string addonKey = key.Substring(17, key.Length() - 17);
Widget state = GetRootWidget().FindAnyWidget("recommended_" + addonKey);
if (state)
{
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") state.SetColor(Color.FromRGBA(105, 190, 140, 255));
else state.SetColor(Color.FromRGBA(220, 115, 115, 255));
}
continue;
}
if (key.IndexOf("resolved_") == 0)
{
string resolvedKey = key.Substring(9, key.Length() - 9);
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
continue;
}
if (key == "planeReconEnabled")
{
int planeState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
ERR_SelectionStyle.Pair(GetRootWidget(), "PlaneOn", "PlaneOff", planeState);
m_AvailableFields.Insert(key); continue;
}
if (key == "keepWeaponOnUnconscious")
{
m_KeepWeaponState = entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt();
string retention = "Disabled: held guns drop on unconsciousness.";
if (entry.Substring(divider + 1, entry.Length() - divider - 1) == "1") retention = "Enabled: retain your gun and equip it after revival.";
SetText("Saved_keepWeaponOnUnconscious", retention);
continue;
}
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
string saved = entry.Substring(divider + 1, entry.Length() - divider - 1);
if (ERR_Settings.Randomizable(key)) ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("Random_" + key, GetRootWidget()), saved == "-1");
if (key == "startTimeMinutes") ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("RandomDay_startTimeMinutes", GetRootWidget()), saved == "-2");
if (key == "viewDistanceMetres") ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("Default_viewDistanceMetres", GetRootWidget()), saved == "0");
if (key == "objectGuardEvery")
for (int density = 1; density <= 4; density++)
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("GuardEvery_" + density.ToString(), GetRootWidget()), saved.ToInt() == density);
if (key == "objectGuardHQMode")
for (int policy = 0; policy <= 2; policy++)
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("GuardHQ_" + policy.ToString(), GetRootWidget()), saved.ToInt() == policy);
string value = ERR_AdminMenuFields.Display(key, saved);
m_AvailableFields.Insert(key);
SetText("Saved_" + key, "Saved: " + value);
if (key == "startMode") SetText("Saved_" + key, ERR_AdminMenuFields.StartSummary(entry.Substring(divider + 1, entry.Length() - divider - 1).ToInt(), m_Mode.StartMode()));
else if (key == "aiDoctrine" || key == "aiShadow") SetText("Saved_" + key, "Next round: " + value);
else SetText("Saved_" + key, "Saved: " + value);
// Existing fields retain all draft text, including a temporarily empty edit.
if (!m_InitializedFields.Contains(key))
{
@@ -272,7 +357,7 @@ class ERR_AdminMenu : ChimeraMenuBase
}
if (summary.IsEmpty()) summary = "No mission status is available yet.";
SetText("Status", summary);
string connection = "Connected to server. Editing a value does not save it: press Apply. Unsaved edits are lost on close.";
string connection = "Connected to server. Use Apply for edited values. Selection buttons save immediately. Unsaved edits are lost on close.";
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
SetText("Connection", connection);
m_Ready = true; m_Age = 0; UpdateActions();
@@ -324,6 +409,13 @@ class ERR_AdminMenu : ChimeraMenuBase
if (!button || !IsFocused()) return;
string name = button.GetRootWidget().GetName();
if (name == "Close") { Close(); return; }
if (name == "RecommendationsBack") { ShowPage(3); return; }
if (name == "RecommendedPlane") { ShowPage(14); return; }
if (name == "EquipmentOverview") { ShowPage(9); return; }
if (name == "RecommendedWeapons") { ShowPage(9); return; }
if (name == "PlaneBack") { ShowPage(1); return; }
if (name == "AIBack") { ShowPage(1); return; }
if (name == "GuardsBack" || name == "SitesBack") { ShowPage(11); return; }
int page = Navigation().Find(name);
if (page >= 0) { ShowPage(page); return; }
if (name == "JournalDetails")
@@ -354,6 +446,24 @@ class ERR_AdminMenu : ChimeraMenuBase
return;
}
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
if (name.IndexOf("GuardEvery_") == 0 || name.IndexOf("GuardHQ_") == 0)
{
string guardKey = "objectGuardEvery";
if (name.IndexOf("GuardHQ_") == 0) guardKey = "objectGuardHQMode";
if (!m_HQ || !m_AvailableFields.Contains(guardKey)) return;
int guardPrefix = name.IndexOf("_") + 1;
SCR_EditBoxComponent guardEdit = SCR_EditBoxComponent.GetEditBoxComponent(guardKey, GetRootWidget());
if (guardEdit) guardEdit.SetValue(name.Substring(guardPrefix, name.Length() - guardPrefix));
SetText("Feedback", "Choice selected. Press Apply to save for next round. Green marks the saved choice.");
return;
}
if (name == "PlaneOn" || name == "PlaneOff")
{
if (!m_HQ || !m_AvailableFields.Contains("planeReconEnabled")) return;
string enabled = "0"; if (name == "PlaneOn") enabled = "1";
m_ClickDelay = 0.3; SetText("Feedback", "Saving reconnaissance plane selection for next round...");
m_Controller.ERR_AdminSubmit("mission plane set " + enabled, true); return;
}
if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
{
string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
@@ -370,7 +480,7 @@ class ERR_AdminMenu : ChimeraMenuBase
if (pause) pause.SetText("Pause live");
return;
}
if (name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0)
if (name.IndexOf("Random_") == 0 || name.IndexOf("RandomDay_") == 0 || name == "Default_viewDistanceMetres")
{
int prefix = name.IndexOf("_") + 1;
string key = name.Substring(prefix, name.Length() - prefix);
@@ -379,6 +489,7 @@ class ERR_AdminMenu : ChimeraMenuBase
if (edit)
{
string choice = "Random";
if (name == "Default_viewDistanceMetres") choice = "default";
if (name.IndexOf("RandomDay_") == 0) choice = "Random day";
edit.SetValue(choice);
SetText("Feedback", choice + " selected. Press Apply to save.");
@@ -4,6 +4,24 @@ class ERR_CommandArguments
{
static string CoreSetting(string section, string option)
{
if (section == "mission" && option == "doctrine") return "aiDoctrine";
if (section == "mission" && option == "aishadow") return "aiShadow";
if (section == "mission" && option == "motorbikes") return "motorbikeCount";
if (section == "mission" && option == "planealtitude") return "planeAltitudeMetres";
if (section == "mission" && option == "planereplacement") return "planeReplacementSeconds";
if (section == "mission" && option == "planesector") return "planeSectorSeconds";
if (section == "mission" && option == "plane") return "planeReconEnabled";
if (section == "mission" && option == "camps") return "fieldCampCount";
if (section == "mission" && option == "hospitals") return "fieldHospitalCount";
if (section == "mission" && option == "startweapons") return "startWeaponCount";
if (section == "mission" && option == "startammo") return "startMagazineCount";
if (section == "mission" && option == "objectevery") return "objectGuardEvery";
if (section == "mission" && option == "objecthq") return "objectGuardHQMode";
if (section == "mission" && option == "objectguards") return "objectGuardCount";
if (section == "mission" && option == "hunterdistance") return "hunterSpawnDistanceMetres";
if (section == "mission" && option == "roadblocks") return "roadblockCount";
if (section == "mission" && option == "traffic") return "civilianTrafficCount";
if (section == "mission" && option == "start") return "startMode";
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
@@ -11,11 +29,18 @@ class ERR_CommandArguments
}
static bool Selection(string key, string text, out int value)
{
if (key == "aiDoctrine" && text == "current") { value = 0; return true; }
if (key == "aiDoctrine" && text == "coordinated") { value = 1; return true; }
if (key == "aiShadow" && text == "off") { value = 0; return true; }
if (key == "aiShadow" && text == "on") { value = 1; return true; }
if (key == "startMode" && text == "prison") { value = 0; return true; }
if (key == "startMode" && text == "crash") { value = 1; return true; }
if (text == "random" && ERR_Settings.Randomizable(key)) { value = -1; return true; }
if ((text == "random-day" || text == "random day") && key == "startTimeMinutes") { value = -2; return true; }
if (key == "startTimeMinutes") return ERR_TimeService.ParseClock(text, value);
if (key == "startDateCode") return ERR_EnvironmentService.ParseDate(text, value);
if (key == "weatherProfile") return ERR_EnvironmentService.ParseProfile(text, value);
if (key == "viewDistanceMetres" && text == "default") { value = 0; return true; }
return Integer(text, value);
}
static bool Integer(string text, out int value)
@@ -81,6 +106,37 @@ class ERR_AdminService
}
return handled;
}
protected bool ViewDistanceCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || parts[1] != "viewdistance") return false;
if (parts.Count() == 3 && parts[2] == "show")
{
replies.Insert("Mission view distance: " + ERR_AdminMenuFields.Display("viewDistanceMetres", mode.Settings().Get("viewDistanceMetres").ToString()) + ". Server limit: " + GetGame().GetViewDistanceServerLimit().ToString() + " m. Platform limits also apply.");
return true;
}
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection("viewDistanceMetres", parts[3], value) && mode.Settings().Set("viewDistanceMetres", value))
{
mode.PublishViewDistance();
replies.Insert("Saved mission view distance: " + ERR_AdminMenuFields.Display("viewDistanceMetres", value.ToString()) + ". Applies now when players close menus, and to later joins; server/platform limits apply.");
}
else replies.Insert("Use /esc mission viewdistance show|set 500..10000|default. Invalid value or save failure leaves the previous setting unchanged.");
return true;
}
protected bool StartCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || parts[1] != "start") return false;
if (parts.Count() == 3 && parts[2] == "show")
{
replies.Insert(ERR_AdminMenuFields.StartSummary(mode.Settings().Get("startMode"), mode.StartMode()));
return true;
}
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection("startMode", parts[3], value) && mode.Settings().Set("startMode", value))
replies.Insert(ERR_AdminMenuFields.StartSummary(value, mode.StartMode()) + ". Saved for next round; current start and players are unchanged.");
else replies.Insert("Use /esc mission start show|set prison|crash (or 0|1). No random start. Invalid value or save failure leaves the previous selection unchanged.");
return true;
}
protected bool TimeCommand(ERR_GameMode mode, array<string> parts, array<string> replies)
{
if (parts.Count() < 2 || parts[0] != "mission" || (parts[1] != "time" && parts[1] != "speed")) return false;
@@ -107,6 +163,23 @@ class ERR_AdminService
}
protected string SettingHelp(string key)
{
if (key == "aiDoctrine") return "Current (0) or Coordinated (1); default Current. Saved for next round.";
if (key == "aiShadow") return "On (1) records coordinated proposals without issuing their orders; Off (0) uses the selected doctrine. Saved for next round.";
if (key == "startWeaponCount") return "0..12 pistols in the shared prison/crash supply backpack; default 5. Applies next round.";
if (key == "startMagazineCount") return "0..48 spare pistol magazines in the shared prison/crash supply backpack; default 8. Applies next round.";
if (key == "motorbikeCount") return "0-6; default 2; 0 disabled. Optional WCS Motorbikes, two response specialists per bike. Applies next round; losses are not replaced.";
if (key == "planeAltitudeMetres") return "150-500; default 250. Applies next round.";
if (key == "planeReplacementSeconds") return "30-1800; default 60. Applies next round.";
if (key == "planeSectorSeconds") return "30-600; default 120. Applies next round.";
if (key == "planeReconEnabled") return "0 disabled, 1 enabled if Cessna 172 and Propeller Flight Core are loaded. Applies next round. A clear airport departure area is required.";
if (key == "fieldCampCount") return "1..16 field camps; default 12. Applies next round. Counts are requested: unsafe optional sites may be omitted.";
if (key == "fieldHospitalCount") return "1..8 field hospitals; default 6. Applies next round. The required radio-clue hospital is retained.";
if (key == "objectGuardEvery") return "Guard every 1st, 2nd, 3rd or 4th eligible site; default every third. Nearby buildings share one site. Next round.";
if (key == "objectGuardHQMode") return "0: all hold their posts; 1: half hold, half respond to HQ; 2: all may respond to HQ. All report contacts. Next round.";
if (key == "objectGuardCount") return "0..16 soldiers per factory/industrial, military or infrastructure site; default 8, zero off. Next round. HQ participation is configured separately; all report. Towers and chimneys use suitable platforms. Lost guards are not replaced.";
if (key == "hunterSpawnDistanceMetres") return "Opening UAZ and troop-truck patrol placement. Applies next round.";
if (key == "roadblockCount") return "Set 0..12 roadblocks (default 4; 0 disables). Applies next round. Each has four guards; unsuitable sites are skipped.";
if (key == "civilianTrafficCount") return "Set 0..6 civilian cars (default 3). Zero disables new traffic. Unclaimed cars retire only far from all players and out of sight; player-entered cars are preserved.";
if (key == "keepWeaponOnUnconscious") return "Set 1 to retain the held firearm during unconsciousness and equip it after recovery; 0 keeps normal dropping. Applies to the next knockout.";
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent")
return "Set 0..100 percent or Random. Applies to newly equipped enemy soldiers; existing loadouts are not rerolled or replenished.";
@@ -114,14 +187,25 @@ class ERR_AdminService
return "Use /esc intel reveal set N (0-100) or random. This is the chance that a first document identifies its location exactly; otherwise it gives a rough location. A second document for that location always identifies it. Changes affect newly recovered documents immediately; known locations and spent documents remain unchanged.";
return "";
}
protected string RoadblockTiming(string key)
{
if (key == "aiDoctrine" || key == "aiShadow") return " for next round";
if (key == "motorbikeCount" || key == "planeAltitudeMetres" || key == "planeReplacementSeconds" || key == "planeSectorSeconds" || key == "planeReconEnabled" || key == "fieldCampCount" || key == "fieldHospitalCount" || key == "startWeaponCount" || key == "startMagazineCount" || key == "objectGuardCount" || key == "objectGuardEvery" || key == "objectGuardHQMode" || key == "hunterSpawnDistanceMetres" || key == "roadblockCount") return " for next round";
return "";
}
protected void SettingCommand(ERR_Settings settings, string key, array<string> parts, array<string> replies)
{
if (parts.Count() == 3 && parts[2] == "show") { replies.Insert(key + "=" + ERR_Settings.SelectionText(settings.Get(key))); return; }
if (parts.Count() == 3 && parts[2] == "show")
{
string displayed = ERR_Settings.SelectionText(settings.Get(key));
if (key == "aiDoctrine" || key == "aiShadow") displayed = ERR_AdminMenuFields.Display(key, settings.Get(key).ToString()) + " (next round)";
replies.Insert(key + "=" + displayed); return;
}
string help = SettingHelp(key);
if (parts.Count() == 3 && parts[2] == "help" && !help.IsEmpty()) { replies.Insert(help); return; }
int value;
if (parts.Count() == 4 && parts[2] == "set" && ERR_CommandArguments.Selection(key, parts[3], value) && settings.Set(key, value))
replies.Insert(key + "=" + ERR_Settings.SelectionText(value) + " saved");
replies.Insert(key + "=" + ERR_Settings.SelectionText(value) + " saved" + RoadblockTiming(key));
else
{
string failure = "Invalid value or settings could not be saved; previous setting retained.";
@@ -136,6 +220,10 @@ class ERR_AdminService
if (topic == "time" || topic == "mission")
{
replies.Insert("/esc mission radio show|set N|random | mission abandon");
replies.Insert("/esc mission doctrine show|set current|coordinated | mission aishadow show|set off|on; next round only.");
replies.Insert("/esc mission start show|set prison|crash: save the next round start; current round unchanged.");
replies.Insert("/esc mission startweapons show|set 0..12 | mission startammo show|set 0..48; shared backpack, next round.");
replies.Insert("/esc mission viewdistance show|set 500..10000|default; applies immediately after closing menus.");
replies.Insert("/esc time show|set HH:MM|random|random-day | mission speed show|set N|random; saved changes apply next round.");
replies.Insert("/esc mission date show|set YYYY-MM-DD|authored|random | mission weather show|set authored|clear|overcast|rain|random");
return;
@@ -153,7 +241,8 @@ class ERR_AdminService
}
replies.Insert("/esc menu | status | settings | help mission|time|intel|hq|cheat|god|killais");
replies.Insert("/esc mission abandon | mission radio show|set N|random | time show|set HH:MM|random|random-day");
replies.Insert("/esc mission date|weather|speed show|set VALUE | intel reveal show|set N|help");
replies.Insert("/esc mission date|weather|speed show|set VALUE | mission viewdistance show|set 500..10000|default | intel reveal show|set N|help");
replies.Insert("/esc mission start show|set prison|crash; next round only.");
replies.Insert("/esc spectate next|previous; optional systems add their own command topics.");
}
protected bool Unavailable(array<string> parts, array<string> replies)
@@ -193,6 +282,8 @@ class ERR_AdminService
foreach (string key : ERR_Settings.Keys())
{
string line = key + "=" + mode.Settings().Get(key).ToString();
if (key == "startMode") line = ERR_AdminMenuFields.StartSummary(mode.Settings().Get(key), mode.StartMode());
if (key == "aiDoctrine" || key == "aiShadow") line = key + "=" + ERR_AdminMenuFields.Display(key, mode.Settings().Get(key).ToString()) + " (next round)";
if (key == "automaticConvoyEnabled") line += " (saved for next round; current startup unchanged)";
replies.Insert(line);
}
@@ -219,7 +310,9 @@ class ERR_AdminService
return;
}
if (ERR_EnvironmentCommands.Execute(mode.Settings(), mode.Environment(), parts, replies)) return;
if (ViewDistanceCommand(mode, parts, replies)) return;
if (TimeCommand(mode, parts, replies)) return;
if (StartCommand(mode, parts, replies)) return;
if (parts.Count() == 3 || parts.Count() == 4)
{
string key = Setting(parts[0], parts[1]);
@@ -9,6 +9,8 @@ class ERR_AdminSession
protected bool m_Stopped;
protected bool m_Live;
protected int m_Cursor;
protected string m_ItemAddon;
protected int m_ItemPage;
protected float m_LastRequest = -1000;
protected float m_LastSnapshot = -1000;
protected float m_LastLog = -1000;
@@ -66,11 +68,43 @@ class ERR_AdminSession
return;
}
array<string> replies = {};
if (!JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
if (!ItemCatalogCommand(arguments, replies) && !JournalCommand(arguments, replies) && !m_Tools.Execute(m_Mode, m_Controller, arguments, replies))
m_Mode.Admin().Execute(m_Mode, true, arguments, replies);
foreach (string reply : replies) m_Controller.ERR_AdminDeliverReply(m_Token, reply, menu);
if (menu) Snapshot(true);
}
// Per-session paging: another administrator cannot change this menu's selection.
protected bool ItemCatalogCommand(string arguments, array<string> replies)
{
array<string> parts = {}; arguments.Split(" ", parts, true);
if (parts.IsEmpty() || parts[0] != "items") return false;
int page;
ERR_ServerItemCatalog catalog = GetGame().ERR_ServerItems(); catalog.Start(m_Mode);
if (parts.Count() == 4 && parts[1] == "preset")
{
if (GetGame().ERR_ItemSelections().Preset(parts[2], parts[3])) replies.Insert("Preset " + parts[2] + " completed: " + parts[3] + ". Future random equipment uses the saved selection.");
else replies.Insert("Preset not saved/applied. Use an existing name to load/delete, or save a name of 1-24 letters, digits, _ or -. Maximum 16 presets. Previous selection retained.");
return true;
}
if (parts.Count() == 5 && parts[1] == "set" && catalog.Find(parts[2]) && (parts[3] == "*" || catalog.Contains(parts[2], parts[3])) && (parts[4] == "0" || parts[4] == "1"))
{
if (GetGame().ERR_ItemSelections().Set(parts[2], parts[3], parts[4] == "1")) replies.Insert("Item selection saved. Applies to future random equipment; existing inventories remain unchanged.");
else replies.Insert("Could not save item selection. Previous selection retained.");
return true;
}
if (parts.Count() == 5 && parts[1] == "weight" && catalog.Find(parts[2]) && (parts[3] == "*" || catalog.Contains(parts[2], parts[3])))
{
int weight;
if (!ERR_CommandArguments.Integer(parts[4], weight) || weight < 0 || weight > 100) replies.Insert("Relative weight must be 0-100; 1 is normal, 0 excludes from extra equipment.");
else if (GetGame().ERR_ItemSelections().SetWeight(parts[2], parts[3], weight)) replies.Insert("Probability weight saved. Applies to future extra-equipment rolls; disabled items remain disabled.");
else replies.Insert("Could not save weight. Previous settings retained.");
return true;
}
if (parts.Count() != 4 || parts[1] != "list" || !catalog.Find(parts[2]) || !ERR_CommandArguments.Integer(parts[3], page) || page < 0 || page > 2730)
{ replies.Insert("Choose a loaded server mod in Items, then press Show items."); return true; }
m_ItemAddon = parts[2]; m_ItemPage = page;
return true;
}
protected bool JournalCommand(string arguments, array<string> replies)
{
if (arguments != "hq log" && arguments != "hq log show" && arguments != "hq log next" && arguments != "hq log set on" && arguments != "hq log set off") return false;
@@ -99,6 +133,8 @@ class ERR_AdminSession
if (m_Mode.Settings().Get(key) == -1) values.Insert("resolved_" + key + "=" + m_Mode.Settings().Runtime(key).ToString());
}
m_Mode.Admin().Capabilities(values);
ERR_RecommendedAddons.Snapshot(m_Mode, values);
GetGame().ERR_ServerItems().Snapshot(m_Mode, values, m_ItemAddon, m_ItemPage);
m_Mode.Admin().Execute(m_Mode, true, "status", status);
m_Controller.ERR_AdminDeliverSnapshot(m_Token, true, values, status);
}
@@ -3,19 +3,53 @@
class ERR_AirfieldPlacement
{
protected bool m_NearAirport;
// Optional terrain/adapter anchors; positions are centres, not trusted pads.
// Numbered names allow several airports without matching unrelated map labels.
static void AuthoredAirports(array<vector> positions)
{
if (!GetGame().GetWorldEntity()) return;
vector low, high;
GetGame().GetWorldEntity().GetWorldBounds(low, high);
for (int index = -1; index < 16; index++)
{
string name = "ERR_ReconAirport";
if (index >= 0) name = "ERR_Airfield_" + index.ToString();
IEntity marker = GetGame().GetWorld().FindEntityByName(name);
if (!marker) continue;
vector position = ERR_Actors.Ground(marker.GetOrigin());
if (position[0] < low[0] || position[0] > high[0] || position[2] < low[2] || position[2] > high[2]) continue;
if (ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) continue;
positions.Insert(position);
}
}
static bool AirportPrefab(string path)
{
path.ToLower();
return path.Contains("prefabs/structures/") && (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/controltower"));
}
protected bool Airport(IEntity entity)
{
if (!entity || !entity.GetPrefabData()) return true;
string path = entity.GetPrefabData().GetPrefabName(); path.ToLower();
if (path.Contains("/runway") || path.Contains("/lightrunway_") || path.Contains("/aircontroltower") || path.Contains("/airportcontroltower") || path.Contains("/locations/eden/airport.et")) m_NearAirport = true;
if (AirportPrefab(path) || path.Contains("/locations/eden/airport.et")) m_NearAirport = true;
return !m_NearAirport;
}
static bool Clear(vector position)
{
if (!Replication.IsServer() || position[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
ERR_AirfieldPlacement survey = new ERR_AirfieldPlacement();
GetGame().GetWorld().QueryEntitiesBySphere(position, 600, survey.Airport, null, EQueryEntitiesFlags.ALL);
array<vector> authored = {};
AuthoredAirports(authored);
foreach (vector centre : authored)
if (vector.DistanceXZ(centre, position) <= 600) survey.m_NearAirport = true;
if (!survey.m_NearAirport) GetGame().GetWorld().QueryEntitiesBySphere(position, 600, survey.Airport, null, EQueryEntitiesFlags.ALL);
if (!survey.m_NearAirport) return false;
return ClearGround(position);
}
// Same aircraft footprint/vertical sweep, without requiring airport scenery.
static bool ClearGround(vector position)
{
if (!Replication.IsServer() || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), position)) return false;
for (int edge = 0; edge < 4; edge++)
{
float side = edge * Math.PI / 2;
@@ -1,5 +1,5 @@
// Conservative fallback for a sleeping, engine-off Mi8 whose native simulation
// has not yet populated contact flags. Reads physical support; never wakes it.
// Conservative fallback for audited sleeping, engine-off rescue aircraft whose
// native simulation has not populated contact flags. Reads support; never wakes it.
class ERR_CasualtyGroundSupport
{
static bool SupportHit(float fraction, float normalY, bool selfHit, bool dynamicHit)
@@ -59,14 +59,37 @@ class ERR_CasualtyGroundSupport
}
return supported;
}
static int LandingGearKind(string prefab)
{
if (prefab.Contains("/Mi8MT/") || prefab.EndsWith("Prefabs/ERR/RescueHelicopter.et") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterFIA.et") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterAFRF.et")) return 1;
if (prefab.Contains("/UH1H/") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterWest.et") || prefab.EndsWith("Prefabs/ERR/RescueHelicopterION.et")) return 2;
return 0;
}
static bool SkidsSupported(Vehicle aircraft, bool diagnostics = false)
{
vector leftFront, rightFront, leftRear, rightRear;
if (!WheelCenter(aircraft, "v_ground_contact_L01", leftFront, diagnostics) || !WheelCenter(aircraft, "v_ground_contact_R01", rightFront, diagnostics) || !WheelCenter(aircraft, "v_ground_contact_L02", leftRear, diagnostics) || !WheelCenter(aircraft, "v_ground_contact_R02", rightRear, diagnostics)) return false;
// Installed UH1H_base.et defines these four skid pivots without wheel radii.
// Both skids and both ends need direct static support. Missing/collapsed
// pivots and a helicopter suspended above the ground fail closed.
if (vector.DistanceXZ(leftFront, rightFront) < 1 || vector.DistanceXZ(leftRear, rightRear) < 1 || vector.DistanceXZ(leftFront, leftRear) < 1 || vector.DistanceXZ(rightFront, rightRear) < 1) return false;
bool a = WheelSupported(aircraft, leftFront, 0, diagnostics);
bool b = WheelSupported(aircraft, rightFront, 0, diagnostics);
bool c = WheelSupported(aircraft, leftRear, 0, diagnostics);
bool d = WheelSupported(aircraft, rightRear, 0, diagnostics);
return a && b && c && d;
}
static bool Parked(Vehicle aircraft, bool diagnostics = false)
{
if (!aircraft || !aircraft.GetPrefabData() || !aircraft.GetPrefabData().GetPrefabName().Contains("/Mi8MT/")) return false;
if (!aircraft || !aircraft.GetPrefabData()) return false;
int kind = LandingGearKind(aircraft.GetPrefabData().GetPrefabName());
if (!kind) return false;
Physics physics = aircraft.GetPhysics();
HelicopterControllerComponent controller = HelicopterControllerComponent.Cast(aircraft.FindComponent(HelicopterControllerComponent));
if (!physics || !controller || !controller.GetSimulation()) return false;
if (diagnostics) PrintFormat("ERR PARKED SUPPORT: physicsAwake=%1 engineOn=%2 nativeContact=%3", physics.IsActive(), controller.GetSimulation().EngineIsOn(), controller.GetSimulation().HasAnyGroundContact());
if (physics.IsActive() || controller.GetSimulation().EngineIsOn()) return false;
if (kind == 2) return SkidsSupported(aircraft, diagnostics);
vector front, left, right;
if (!WheelCenter(aircraft, "v_wheel_F", front, diagnostics) || !WheelCenter(aircraft, "v_wheel_bL", left, diagnostics) || !WheelCenter(aircraft, "v_wheel_bR", right, diagnostics)) return false;
// Fail closed if native pivot resolution collapses to an owner origin or
@@ -4,7 +4,7 @@ class ERR_CasualtyFeedback
static bool Visible(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty || !ERR_CasualtyLoadingRules.Down(casualty)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft();
Vehicle aircraft = mode.CasualtyLoadingAircraftFor(caller, casualty);
Faction faction = mode.PlayerFaction();
return aircraft && faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()));
}
@@ -14,7 +14,7 @@ class ERR_CasualtyFeedback
if (!caller || caller == casualty || !ERR_Actors.CanFight(caller)) return "A living, conscious rescuer is required";
if (CompartmentAccessComponent.GetVehicleIn(caller)) return "Leave the helicopter before loading a casualty";
if (!ERR_CasualtyLoadingRules.GroundSettled(casualty)) return "Place the living, unconscious casualty on stable ground";
Vehicle aircraft = mode.CasualtyLoadingAircraft();
Vehicle aircraft = mode.CasualtyLoadingAircraftFor(caller, casualty);
if (!aircraft || !ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin()))) return "Stay beside the casualty and within twelve metres of the extraction helicopter";
if (!ERR_CasualtyLoadingRules.AircraftReady(aircraft)) return "The helicopter must be upright, grounded and stationary";
if (!ERR_CasualtyLoadingRules.Nearby(mode, caller, casualty)) return "Keep clear access to a fellow prisoner's body";
@@ -1,9 +1,11 @@
// Round-owned server service; only the aircraft reference is replicated for action visibility.
// Round-owned server service; only registered transport references are replicated.
modded class ERR_GameMode
{
protected ref ERR_CasualtyLoadingService m_ERRCasualtyLoading;
[RplProp()]
protected RplId m_ERRCasualtyAircraftId = RplId.Invalid();
[RplProp()]
protected RplId m_ERRCasualtySecondAircraftId = RplId.Invalid();
ERR_CasualtyLoadingService CasualtyLoading() { return m_ERRCasualtyLoading; }
Vehicle CasualtyLoadingAircraft()
{
@@ -12,17 +14,61 @@ modded class ERR_GameMode
if (!component) return null;
return Vehicle.Cast(component.GetEntity());
}
int CasualtyLoadingAircrafts(out array<Vehicle> aircrafts)
{
aircrafts.Clear();
if (Replication.IsServer())
{
if (Extraction()) return Extraction().Vehicles(aircrafts);
return 0;
}
Vehicle first = CasualtyLoadingAircraft();
if (first) aircrafts.Insert(first);
Vehicle second = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(m_ERRCasualtySecondAircraftId));
if (second && second != first) aircrafts.Insert(second);
return aircrafts.Count();
}
bool CasualtyLoadingTransport(Vehicle aircraft)
{
if (!aircraft) return false;
if (Replication.IsServer()) return Extraction() && Extraction().IsTransport(aircraft);
return aircraft == CasualtyLoadingAircraft() || (m_ERRCasualtySecondAircraftId != RplId.Invalid() && ERR_CasualtyLoadingRules.EntityId(aircraft) == m_ERRCasualtySecondAircraftId);
}
// Prefer a usable nearby transport; retain the nearest ineligible aircraft for
// a useful denial reason. An existing casualty reservation wins over distance.
Vehicle CasualtyLoadingAircraftFor(IEntity caller, IEntity casualty)
{
if (!caller || !casualty) return null;
array<Vehicle> aircrafts = {}; CasualtyLoadingAircrafts(aircrafts);
Vehicle nearest, available;
float nearestDistance = float.MAX, availableDistance = float.MAX;
foreach (Vehicle aircraft : aircrafts)
{
if (!ERR_Actors.Alive(aircraft)) continue;
float distance = vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin());
if (distance < nearestDistance) { nearest = aircraft; nearestDistance = distance; }
if (!ERR_CasualtyLoadingRules.Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), distance) || !ERR_CasualtyLoadingRules.AircraftReady(aircraft)) continue;
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(aircraft.FindComponent(BaseCompartmentManagerComponent));
if (!manager) continue;
array<BaseCompartmentSlot> seats = {}; manager.GetCompartments(seats);
foreach (BaseCompartmentSlot seat : seats)
{
if (!ERR_CasualtyLoadingRules.TransportBench(seat, aircraft) || seat.GetOccupant() || !seat.IsCompartmentAccessible() || seat.IsGetInLockedFor(casualty)) continue;
if (seat.IsReservedBy(casualty)) return aircraft;
if (!seat.IsReserved() && distance < availableDistance) { available = aircraft; availableDistance = distance; }
}
}
if (available) return available;
return nearest;
}
protected void ERR_UpdateCasualtyAircraft(float now)
{
Vehicle aircraft = ExtractionVehicle();
if (!aircraft)
{
if (m_ERRCasualtyAircraftId != RplId.Invalid()) { m_ERRCasualtyAircraftId = RplId.Invalid(); Replication.BumpMe(); }
return;
}
RplComponent component = RplComponent.Cast(aircraft.FindComponent(RplComponent));
if (!component || m_ERRCasualtyAircraftId == component.Id()) return;
m_ERRCasualtyAircraftId = component.Id();
array<Vehicle> aircrafts = {}; CasualtyLoadingAircrafts(aircrafts);
RplId first = RplId.Invalid(), second = RplId.Invalid();
if (!aircrafts.IsEmpty()) first = ERR_CasualtyLoadingRules.EntityId(aircrafts[0]);
if (aircrafts.Count() > 1) second = ERR_CasualtyLoadingRules.EntityId(aircrafts[1]);
if (first == m_ERRCasualtyAircraftId && second == m_ERRCasualtySecondAircraftId) return;
m_ERRCasualtyAircraftId = first; m_ERRCasualtySecondAircraftId = second;
Replication.BumpMe();
}
protected void ERR_StopCasualtyLoading()
@@ -52,7 +52,7 @@ modded class SCR_PlayerController
IEntity caller = ERR_CasualtyLoadingRules.Entity(callerId);
Vehicle vehicle = Vehicle.Cast(ERR_CasualtyLoadingRules.Entity(vehicleId));
IEntity owner = ERR_CasualtyLoadingRules.Entity(ownerId);
if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && vehicle == mode.CasualtyLoadingAircraft() && ERR_CasualtyLoadingRules.Nearby(mode, caller, actor) && owner)
if (lease && mode && ERR_CasualtyLoadingRules.EntityId(mode) == modeId && ERR_CasualtyLoadingRules.EntityId(actor) == actorId && mode.CasualtyLoadingTransport(vehicle) && ERR_CasualtyLoadingRules.NearbyAircraft(mode, caller, actor, vehicle) && owner)
{
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(owner.FindComponent(BaseCompartmentManagerComponent));
SCR_CompartmentAccessComponent access = SCR_CompartmentAccessComponent.Cast(actor.FindComponent(SCR_CompartmentAccessComponent));
@@ -1,5 +1,5 @@
// First release supports only the registered Mi8 extraction aircraft's physical
// child transport benches. Nothing changes health, ownership, or seat classes.
// Only registered rescue transports and audited native child benches are eligible.
// Nothing changes health, ownership, or seat classes.
class ERR_CasualtyLoadingRules
{
static int RoundAction(int phase)
@@ -86,20 +86,30 @@ class ERR_CasualtyLoadingRules
return false;
}
static bool Nearby(ERR_GameMode mode, IEntity caller, IEntity casualty)
{
if (!mode) return false;
return NearbyAircraft(mode, caller, casualty, mode.CasualtyLoadingAircraftFor(caller, casualty));
}
static bool NearbyAircraft(ERR_GameMode mode, IEntity caller, IEntity casualty, Vehicle aircraft)
{
if (!mode || !mode.Active() || mode.Phase() != ERR_Phase.EXTRACTION || !caller || caller == casualty) return false;
CharacterControllerComponent rescuer = CharacterControllerComponent.Cast(caller.FindComponent(CharacterControllerComponent));
if (!ERR_Actors.CanFight(caller) || !rescuer || rescuer.GetLifeState() != ECharacterLifeState.ALIVE || !GroundSettled(casualty) || CompartmentAccessComponent.GetVehicleIn(caller)) return false;
Vehicle aircraft = mode.CasualtyLoadingAircraft();
if (!aircraft || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
if (!mode.CasualtyLoadingTransport(aircraft) || !Ranges(vector.Distance(caller.GetOrigin(), casualty.GetOrigin()), vector.Distance(caller.GetOrigin(), aircraft.GetOrigin()), vector.Distance(casualty.GetOrigin(), aircraft.GetOrigin())) || !AircraftReady(aircraft)) return false;
Faction faction = mode.PlayerFaction();
return faction && SCR_FactionManager.SGetFaction(caller) == faction && SCR_FactionManager.SGetFaction(casualty) == faction && LineOfSight(caller, casualty);
}
static bool TransportBenchPrefab(string prefab)
{
// Exact authored bench owners, including the two unarmed Huey side seats.
// Aircraft-root pilot seats, turret owners and patient slots stay excluded.
return prefab.EndsWith("Prefabs/Vehicles/Helicopters/Mi8MT/VehParts/Seats/VehPart_MI8_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/Mi8MT/VehParts/Seats/VehPart_MI8_CIV_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_cargo_openDoor.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_seats_gunners.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_CIV_seats_cargo.et") || prefab.EndsWith("Prefabs/Vehicles/Helicopters/UH1H/VehParts/Seats/VehPart_UH1H_CIV_seats_gunners.et");
}
static bool TransportBench(BaseCompartmentSlot seat, Vehicle aircraft)
{
if (!seat || !aircraft || seat.GetType() != ECompartmentType.CARGO || SCR_PatientCompartmentSlot.Cast(seat)) return false;
IEntity owner = seat.GetOwner();
if (!owner || owner == aircraft || !owner.GetPrefabData() || !owner.GetPrefabData().GetPrefabName().Contains("VehPart_MI8_seats")) return false;
if (!owner || owner == aircraft || !owner.GetPrefabData() || !TransportBenchPrefab(owner.GetPrefabData().GetPrefabName())) return false;
while (owner)
{
if (owner == aircraft) return true;
@@ -203,7 +213,7 @@ class ERR_CasualtyLoadingService
{
if (!Replication.IsServer() || m_Stopped || !ERR_CasualtyLoadingRules.Nearby(m_Mode, caller, casualty)) return false;
if (!CurrentLife(caller) || !CurrentLife(casualty) || !Settled(casualty, System.GetTickCount()) || PendingActor(caller, casualty)) return false;
return AvailableSeat(casualty, m_Mode.ExtractionVehicle()) != null;
return AvailableSeat(casualty, m_Mode.CasualtyLoadingAircraftFor(caller, casualty)) != null;
}
// Reasons never replace CanLoad/Begin authority or reserve a seat.
string RejectionReason(IEntity caller, IEntity casualty)
@@ -214,7 +224,7 @@ class ERR_CasualtyLoadingService
if (!CurrentLife(caller) || !CurrentLife(casualty)) return "Both prisoners must still be connected to this mission";
if (!Settled(casualty, System.GetTickCount())) return "Let the casualty rest on the ground for five seconds";
if (PendingActor(caller, casualty)) return "A casualty loading request is already in progress";
if (!AvailableSeat(casualty, m_Mode.ExtractionVehicle())) return "No accessible transport seat is available";
if (!AvailableSeat(casualty, m_Mode.CasualtyLoadingAircraftFor(caller, casualty))) return "No accessible transport seat is available";
return string.Empty;
}
protected void NotifyCaller(IEntity caller, string message)
@@ -238,10 +248,11 @@ class ERR_CasualtyLoadingService
if (!CanLoad(caller, casualty) || m_NextNonce >= 2147483646) return false;
ERR_PlayerLife callerLife = CurrentLife(caller);
ERR_PlayerLife casualtyLife = CurrentLife(casualty);
Vehicle aircraft = m_Mode.CasualtyLoadingAircraftFor(caller, casualty);
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(casualtyLife.ConnectionId));
if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(m_Mode.ExtractionVehicle()) == RplId.Invalid()) return false;
if (!controller || ERR_CasualtyLoadingRules.EntityId(m_Mode) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(caller) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(casualty) == RplId.Invalid() || ERR_CasualtyLoadingRules.EntityId(aircraft) == RplId.Invalid()) return false;
ERR_CasualtyLoadTicket ticket = new ERR_CasualtyLoadTicket();
ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = m_Mode.ExtractionVehicle();
ticket.Caller = caller; ticket.Casualty = casualty; ticket.Aircraft = aircraft;
ticket.Seat = AvailableSeat(casualty, ticket.Aircraft);
if (!ticket.Seat || ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()) == RplId.Invalid()) return false;
ticket.CallerAccount = callerLife.AccountId; ticket.CasualtyAccount = casualtyLife.AccountId;
@@ -269,7 +280,7 @@ class ERR_CasualtyLoadingService
ERR_CasualtyLoadTicket ticket = Ticket(controller, nonce);
if (!ticket || ticket.Committed) return;
int now = System.GetTickCount();
if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; }
if (!ValidTicket(ticket) || !ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !Settled(ticket.Casualty, now) || !ERR_CasualtyLoadingRules.NearbyAircraft(m_Mode, ticket.Caller, ticket.Casualty, ticket.Aircraft) || ticket.Seat.GetOccupant() || !ticket.Seat.IsCompartmentAccessible() || ticket.Seat.IsGetInLockedFor(ticket.Casualty) || !ticket.Seat.IsReservedBy(ticket.Casualty)) { Resolve(ticket, false); return; }
ticket.Committed = true; ticket.CommittedAt = now;
// Final authority/physical validation is the commit boundary. No retries.
controller.ERR_CommitCasualtyLoad(ERR_CasualtyLoadingRules.EntityId(m_Mode), nonce, ERR_CasualtyLoadingRules.EntityId(ticket.Caller), ERR_CasualtyLoadingRules.EntityId(ticket.Casualty), ERR_CasualtyLoadingRules.EntityId(ticket.Aircraft), ERR_CasualtyLoadingRules.EntityId(ticket.Seat.GetOwner()), ticket.Seat.GetCompartmentSlotID());
@@ -288,11 +299,11 @@ class ERR_CasualtyLoadingService
{
// Death cannot revoke an in-flight commit. Keep the vacant seat reserved
// while the captured body exists; its owner may still see the prior state.
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && m_Mode.CasualtyLoadingTransport(ticket.Aircraft) && ERR_Actors.Alive(ticket.Aircraft) && ticket.Casualty != null && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
}
protected bool ValidTicket(ERR_CasualtyLoadTicket ticket)
{
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && ticket.Aircraft == m_Mode.ExtractionVehicle() && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
return m_Mode && m_Mode.Active() && m_Mode.Phase() == ERR_Phase.EXTRACTION && m_Mode.CasualtyLoadingTransport(ticket.Aircraft) && ERR_Actors.Alive(ticket.Aircraft) && ERR_Actors.Alive(ticket.Casualty) && SameLife(ticket.Caller, ticket.CallerAccount, ticket.CallerConnection) && SameLife(ticket.Casualty, ticket.CasualtyAccount, ticket.CasualtyConnection) && ERR_CasualtyLoadingRules.TransportBench(ticket.Seat, ticket.Aircraft);
}
protected void NotifyResolution(ERR_CasualtyLoadTicket ticket, bool success)
{
@@ -353,7 +364,7 @@ class ERR_CasualtyLoadingService
// leaves room for replication. Timeouts do not issue compensating teleports.
if (!ERR_CasualtyLoadingRules.Within(ticket.CommittedAt, now, 8000)) Resolve(ticket, false);
}
else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.Nearby(m_Mode, ticket.Caller, ticket.Casualty)) Resolve(ticket, false);
else if (!ERR_CasualtyLoadingRules.Within(ticket.Started, now, 5000) || !ERR_CasualtyLoadingRules.NearbyAircraft(m_Mode, ticket.Caller, ticket.Casualty, ticket.Aircraft)) Resolve(ticket, false);
}
}
void RosterChanged(int connection) { if (GetGame()) Tick(GetGame().GetWorld().GetWorldTime()); }
@@ -49,6 +49,7 @@ class ERR_ChatClient
if (m_Command) m_Command.Remove(OnCommand);
m_Command = null;
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu();
if (menu) menu.Close();
// A retained former controller must not close the current owner's panel.
if (menu && GetGame() && m_Controller == GetGame().GetPlayerController()) menu.Close();
}
}
@@ -23,6 +23,8 @@ modded class ERR_AdminMenu
SCR_ButtonTextComponent enable = SCR_ButtonTextComponent.GetButtonText("Enable", row);
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
if (!enable || !disable) { Close(); return; }
ERR_SelectionStyle.Action(enable, true); ERR_SelectionStyle.Action(disable, false);
ERR_SelectionStyle.Status(row, "SelectionState", -1);
enable.m_OnClicked.Insert(OnModButton); disable.m_OnClicked.Insert(OnModButton);
}
UpdateActions();
@@ -49,7 +51,11 @@ modded class ERR_AdminMenu
SCR_ButtonTextComponent disable = SCR_ButtonTextComponent.GetButtonText("Disable", row);
if (enable) enable.SetEnabled(ready && m_ModSaved.Get(pack) == 0);
if (disable) disable.SetEnabled(ready && m_ModSaved.Get(pack) == 1);
int selected = -1; if (m_ModSaved.Contains(pack)) selected = m_ModSaved.Get(pack);
ERR_SelectionStyle.Pair(row, "Enable", "Disable", selected);
TextWidget state = TextWidget.Cast(row.FindAnyWidget("PackState"));
int badge = -1; if (ready) badge = m_ModActive.Get(pack);
ERR_SelectionStyle.Status(row, "SelectionState", badge);
if (!state) continue;
if (!ready) state.SetText("Waiting for server settings...");
else state.SetText("Current round: " + ERR_ContentRegistry.State(m_ModActive.Get(pack) == 1) + " | Saved preference: " + ERR_ContentRegistry.State(m_ModSaved.Get(pack) == 1));
@@ -103,6 +109,24 @@ modded class PauseMenuUI
m_ERRModsButton = SCR_ButtonTextComponent.Cast(widget.FindHandler(SCR_ButtonTextComponent));
if (m_ERRModsButton) m_ERRModsButton.m_OnClicked.Insert(ERR_OpenMods);
}
// Vanilla disables this button while limited, but does not enable it again
// when an admin mode arrives asynchronously. Refresh from native permissions.
protected void ERR_RefreshGameMasterButton()
{
if (!ERR_GameMode.Get() || !m_EditorUnlimitedOpenButton) return;
SCR_EditorManagerEntity editor = SCR_EditorManagerEntity.GetInstance();
m_EditorUnlimitedOpenButton.SetEnabled(editor && !editor.IsLimited() && editor.CanOpen());
}
protected override void OnEditorModeChanged(SCR_EditorModeEntity modeEntity)
{
super.OnEditorModeChanged(modeEntity);
ERR_RefreshGameMasterButton();
}
override void OnMenuUpdate(float tDelta)
{
super.OnMenuUpdate(tDelta);
ERR_RefreshGameMasterButton();
}
protected void ERR_OpenMods()
{
// Unpause local simulation before requesting an authenticated server snapshot.
@@ -1,4 +1,4 @@
// Separate, config-driven preferences. Missing newly installed options use their
// Content preferences in the shared profile. Newly installed options use their
// authored defaults. Failed writes preserve both previous state and backup.
class ERR_ContentSettings
{
@@ -20,7 +20,7 @@ class ERR_ContentSettings
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
map<string, int> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Values = loaded;
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
else if (ERR_ProfileConfig.Exists(m_Path) || ERR_ProfileConfig.Exists(m_Path + ".bak"))
{
m_ReadOnly = true;
Print("Co10 Escape Reintroduced: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
@@ -30,7 +30,10 @@ class ERR_ContentSettings
protected bool Read(string path, out map<string, int> values)
{
JsonLoadContext json = new JsonLoadContext();
if (!json.LoadFromFile(path)) return false;
return ERR_ProfileConfig.Load(path, json) && Parse(json, values);
}
static bool Parse(JsonLoadContext json, out map<string, int> values)
{
values = new map<string, int>();
foreach (ERR_ContentOption option : ERR_ContentRegistry.Definition().Options)
{
@@ -65,6 +68,11 @@ class ERR_ContentSettings
if (option == id) saved = value;
json.WriteValue(option, saved);
}
if (m_Path == PATH)
{
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
m_Values.Set(id, value); return true;
}
map<string, int> checked;
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Matches(checked, id, value)) return false;
map<string, int> previous;
@@ -72,7 +80,7 @@ class ERR_ContentSettings
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked) || !Matches(checked, id, value))
{
if (FileIO.FileExists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
if (ERR_ProfileConfig.Exists(m_Path + ".bak")) FileIO.CopyFile(m_Path + ".bak", m_Path);
else if (!hadPrevious) FileIO.DeleteFile(m_Path);
return false;
}
@@ -0,0 +1,15 @@
// Owned static aftermath; no aircraft, crew, flight or inventory systems.
class ERR_CrashSiteResources
{
static ResourceName SitePrefab() { return "{174211DDE2F6422B}Prefabs/ERR/CrashSite.et"; }
static ResourceName WreckPrefab() { return "{486DDD0B811047EE}Prefabs/ERR/CrashWreck.et"; }
static bool Bounds(out vector minimum, out vector maximum)
{
// Exact owned static wrapper measured in WORKBENCH on 2026-10-03.
// Native bounds: (-1.43719, 0.512451, -8.79651) to (1.4325, 3.98193, 3.94385).
// Expand by 1mm then round outward to millimetres; terrain survey adds its own clearance.
minimum = "-1.439 0.511 -8.798";
maximum = "1.434 3.983 3.945";
return true;
}
}
@@ -15,6 +15,7 @@ class ERR_DiscoveryMapRules
class ERR_DiscoveryMarker : SCR_MapMarkerBase
{
protected string m_Icon = "supplies";
protected ref ERR_ObjectiveMapPresentation m_ERRPresentation;
static string Icon(ERR_LocationKind kind, string id)
{
if (kind == ERR_LocationKind.EXTRACTION) return "helicopters";
@@ -22,6 +23,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission";
if (kind == ERR_LocationKind.OUTPOST) return "building";
if (kind == ERR_LocationKind.HOSPITAL || id.Contains("medical")) return "medical";
if (kind == ERR_LocationKind.PRISON) return "prison";
return "supplies";
}
void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2)
@@ -29,27 +31,37 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
m_Icon = "mark-question";
if (knowledge >= 2) m_Icon = Icon(kind, id);
ApplyIcon();
RefreshPresentation();
}
protected void ApplyIcon()
{
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
if (!GetRootWidget() || !widget) return;
if (m_Icon == "mark-question")
{
widget.SetImage("{E23427CAC80DA8B7}UI/Textures/Icons/icons_mapMarkersUI.imageset", m_Icon);
widget.SetGlowImage("{67B3A6DC2D712B52}UI/Textures/Icons/icons_mapMarkersUI-glow.imageset", m_Icon);
widget.SetImage("{E0248EBF90751BA5}UI/Textures/ERR/SiteMarkers.imageset", m_Icon);
widget.SetGlowImage("{E0248EBF90751BA5}UI/Textures/ERR/SiteMarkers.imageset", m_Icon);
// Preserve the atlas colours instead of tinting the violet disc dark grey.
widget.SetColor(Color.FromSRGBA(255, 255, 255, 255));
}
else
// Production authored objectives use a separate map UI element. They never
// enter the player-annotation registry or change its content restrictions.
void StartPresentation()
{
widget.SetImage("{3262679C50EF4F01}UI/Textures/Icons/icons_wrapperUI.imageset", m_Icon);
widget.SetGlowImage("{00FE3DBDFD15227B}UI/Textures/Icons/icons_wrapperUI-glow.imageset", m_Icon);
if (!m_ERRPresentation) m_ERRPresentation = new ERR_ObjectiveMapPresentation();
RefreshPresentation();
m_ERRPresentation.Start();
}
widget.SetColor(Color.FromSRGBA(35, 45, 50, 255));
void RefreshPresentation()
{
if (!m_ERRPresentation) return;
int point[2];
GetWorldPos(point);
m_ERRPresentation.SetData(Vector(point[0], 0, point[1]), GetCustomText(), m_Icon);
}
void ERR_DiscoveryMarker() { SetCanBeRemovedByOwner(false); }
void SetDiscoveryPosition(vector position, int knowledge)
{
SetWorldPos(ERR_DiscoveryMapRules.Coordinate(position[0], knowledge), ERR_DiscoveryMapRules.Coordinate(position[2], knowledge));
RefreshPresentation();
}
override void OnCreateMarker(bool skipProfanityFilter = false)
{
@@ -61,6 +73,8 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
}
override void OnDelete()
{
if (m_ERRPresentation) m_ERRPresentation.Stop();
m_ERRPresentation = null;
SCR_MapEntity.GetOnMapClose().Remove(OnMapClosed);
SCR_MapEntity.GetOnLayerChanged().Remove(OnMapLayerChanged);
super.OnDelete();
@@ -70,6 +84,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
override void SetCustomText(string text)
{
super.SetCustomText(text);
RefreshPresentation();
// Native SetCustomText updates data only; an already-open map needs its
// existing widget updated too. Closed maps use the saved text on creation.
SCR_MapMarkerWidgetComponent widget = GetMarkerComponent();
@@ -81,6 +96,10 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
// must also leave the disabled list or map movement can resurrect it later.
modded class SCR_MapMarkerManagerComponent
{
void ERR_InsertDiscoveryMarker(ERR_DiscoveryMarker marker)
{
if (marker) marker.StartPresentation();
}
void ERR_RemoveDiscoveryMarker(ERR_DiscoveryMarker marker)
{
if (!marker || marker.GetMarkerID() != -1) return;
@@ -115,6 +134,7 @@ modded class ERR_LocationComponent
m_ERRMarker = null; m_ERRShown = 0; m_ERRShownLabel = "";
return;
}
if (m_ERRMarker) m_ERRMarker.RefreshPresentation();
if (Knowledge() == m_ERRShown && Label() == m_ERRShownLabel) return;
SCR_MapMarkerManagerComponent manager = SCR_MapMarkerManagerComponent.GetInstance();
if (!manager || !manager.GetMarkerConfig()) return;
@@ -123,7 +143,7 @@ modded class ERR_LocationComponent
{
if (!Label().IsEmpty()) label = Label();
else if (Kind() == ERR_LocationKind.RADIO) label = "Radio station";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Initial helicopter pickup site";
else if (Kind() == ERR_LocationKind.EXTRACTION) label = "Extraction pickup - throw smoke here";
else if (Kind() == ERR_LocationKind.DEPOT) label = "Supply cache";
else if (Kind() == ERR_LocationKind.MORTAR) label = "Mortar site";
else label = Id();
@@ -136,15 +156,13 @@ modded class ERR_LocationComponent
m_ERRMarker.SetMarkerConfigID(0);
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge());
m_ERRMarker.SetCustomText(label);
manager.InsertStaticMarker(m_ERRMarker, true);
manager.ERR_InsertDiscoveryMarker(m_ERRMarker);
}
m_ERRMarker.SetDiscoveryPosition(Position(), Knowledge());
m_ERRMarker.SetLocationIcon(Kind(), Id(), Knowledge());
m_ERRMarker.SetCustomText(label);
// Reactivate a previously culled rough marker if the exact point moves
// into the open map's view. Native update performs normal visibility checks.
SCR_MapEntity mapEntity = SCR_MapEntity.GetMapInstance();
if (mapEntity && mapEntity.IsOpen()) manager.SetStaticMarkerDisabled(m_ERRMarker, false);
// The objective presentation performs its own normal viewport culling.
m_ERRMarker.RefreshPresentation();
m_ERRShown = Knowledge();
m_ERRShownLabel = Label();
}
@@ -53,6 +53,8 @@ class ERR_DiscoveryService
foreach (IEntity folder : folders)
{
if (!folder || m_Spent.Contains(folder) || m_Spent.Count() >= 4096) continue;
bool mineRecovered = false;
if (m_Mode.MineIntel()) mineRecovered = m_Mode.MineIntel().Recover(player, folder);
array<ERR_LocationComponent> locations = {};
m_Mode.Locations(locations);
array<ERR_LocationComponent> eligible = {};
@@ -70,7 +72,7 @@ class ERR_DiscoveryService
}
if (eligible.IsEmpty())
{
if (!document && MarkSpent(folder)) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
if (!document && !m_Mode.WorldPreparing() && MarkSpent(folder) && !mineRecovered) m_Mode.ERR_IntelExhausted(!sealedTarget.IsEmpty());
continue;
}
ERR_LocationComponent chosen = eligible.GetRandomElement();
@@ -24,25 +24,82 @@ class ERR_EpinephrineEffect : SCR_DotDamageEffect
}
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
}
// Separate stronger field injector. Standard epinephrine above keeps its
// resilience-over-time behavior; neither item restores a dead character.
class ERR_CombatEpinephrineRules
{
static float Heal(float current, bool leg)
{
float amount = 0.15;
if (leg) amount = 0.5;
return Math.Min(current + amount, 1.0);
}
static float Blood(float current, float threshold) { return Math.Min(1, current + threshold + 0.01); }
}
[BaseContainerProps()]
class ERR_ConsumableCombatEpinephrine : 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));
if (!damage || damage.GetState() == EDamageState.DESTROYED) { failReason = SCR_EConsumableFailReason.UNDAMAGED; return false; }
if (damage.IsBleeding()) return true;
HitZone blood = damage.GetBloodHitZone(), resilience = damage.GetResilienceHitZone();
if ((blood && blood.GetHealthScaled() < 1) || (resilience && resilience.GetHealthScaled() < 1)) return true;
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
foreach (HitZone zone : zones) if (zone && zone.GetHealthScaled() < 1) return true;
failReason = SCR_EConsumableFailReason.UNDAMAGED; return false;
}
}
class ERR_CombatEpinephrineEffect : SCR_InstantDamageEffect
{
override EDamageType GetDefaultDamageType() { return EDamageType.HEALING; }
override void OnEffectAdded(SCR_ExtendedDamageManagerComponent dmgManager)
{
super.OnEffectAdded(dmgManager);
if (!Replication.IsServer()) return;
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(dmgManager);
if (!damage || damage.GetState() == EDamageState.DESTROYED) return;
foreach (ECharacterHitZoneGroup group : {ECharacterHitZoneGroup.HEAD, ECharacterHitZoneGroup.UPPERTORSO, ECharacterHitZoneGroup.LOWERTORSO, ECharacterHitZoneGroup.LEFTARM, ECharacterHitZoneGroup.RIGHTARM, ECharacterHitZoneGroup.LEFTLEG, ECharacterHitZoneGroup.RIGHTLEG}) damage.RemoveGroupBleeding(group);
SCR_CharacterBloodHitZone blood = damage.GetBloodHitZone();
if (blood && blood.GetHealthScaled() <= blood.GetDamageStateThreshold(ECharacterBloodState.FAINTING))
blood.SetHealthScaled(ERR_CombatEpinephrineRules.Blood(blood.GetHealthScaled(), blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS)));
SCR_CharacterResilienceHitZone resilience = damage.GetResilienceHitZone();
if (resilience) resilience.SetHealthScaled(1);
array<HitZone> zones = {}; damage.GetPhysicalHitZones(zones);
foreach (HitZone zone : zones)
{
SCR_CharacterHitZone physical = SCR_CharacterHitZone.Cast(zone);
if (!physical || zone.GetHealthScaled() >= 1) continue;
int group = physical.GetHitZoneGroup();
zone.SetHealthScaled(ERR_CombatEpinephrineRules.Heal(zone.GetHealthScaled(), group == ECharacterHitZoneGroup.LEFTLEG || group == ECharacterHitZoneGroup.RIGHTLEG));
}
CharacterControllerComponent controller = CharacterControllerComponent.Cast(damage.GetOwner().FindComponent(CharacterControllerComponent));
if (controller && controller.IsUnconscious()) controller.SetUnconscious(false);
}
}
// 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;
protected bool m_ERRHoldingCombatEpinephrine;
override bool CanBeShownScript(IEntity user)
{
m_ERRHoldingEpinephrine = false;
m_ERRHoldingEpinephrine = false; m_ERRHoldingCombatEpinephrine = 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());
m_ERRHoldingCombatEpinephrine = item && ERR_ConsumableCombatEpinephrine.Cast(item.GetConsumableEffect());
}
return shown;
}
override bool GetActionNameScript(out string outName)
{
if (m_ERRHoldingCombatEpinephrine) { outName = "Administer combat epinephrine"; return true; }
if (m_ERRHoldingEpinephrine) { outName = "Administer epinephrine"; return true; }
return super.GetActionNameScript(outName);
}
@@ -11,7 +11,7 @@ class ERR_EscapeGameOverScreenInfo : SCR_BaseGameOverScreenInfo
{
string title = "Round ended";
ERR_PersonalEscapeResult result = PersonalResult(playerId);
if (result == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped Everon";
if (result == ERR_PersonalEscapeResult.ESCAPED) title = "You escaped";
else if (result == ERR_PersonalEscapeResult.MISSED) title = "The helicopter escaped";
#ifdef WORKBENCH
PrintFormat("ERR RESULT UI: player=%1 title=%2", playerId, title);
@@ -0,0 +1,49 @@
// Finite objective announcements. Text always arrives; optional TTS shares the
// configured speech service. No provider key is replicated to players.
class ERR_ExtractionSpeechReceiver : ERR_SpeechReceiver
{
protected ERR_GameMode m_Mode;
protected int m_Stage;
void ERR_ExtractionSpeechReceiver(ERR_GameMode mode, int stage) { m_Mode = mode; m_Stage = stage; }
override void Complete(string url, string text)
{
if (GetGame() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active() && m_Mode.PickupStage() == m_Stage)
m_Mode.DeliverExtractionRadio(text, url);
}
}
modded class ERR_GameMode
{
[RplProp()] protected int m_PickupStage;
int PickupStage() { return m_PickupStage; }
void PublishPickupStage(int stage)
{
if (!Replication.IsServer() || stage == m_PickupStage) return;
m_PickupStage = stage; Replication.BumpMe();
}
void DeliverExtractionRadio(string text, string url = "")
{
if (!Replication.IsServer() || !Active()) return;
array<int> players = {}; GetGame().GetPlayerManager().GetPlayers(players);
foreach (int id : players)
{
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_ExtractionRadio(text, url);
}
}
}
modded class SCR_PlayerController
{
void ERR_ExtractionRadio(string text, string url)
{
if (Replication.IsServer()) Rpc(ERR_ReceiveExtractionRadio, text, url);
}
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
protected void ERR_ReceiveExtractionRadio(string text, string url)
{
if (this != GetGame().GetPlayerController() || !ERR_GameMode.Get()) return;
if (!url.IsEmpty()) { GetGame().ERR_PlaySpeechPreview(url, text, "Extraction radio"); return; }
SCR_HintManagerComponent hints = SCR_HintManagerComponent.GetInstance();
if (hints) hints.ShowCustom(text, "Extraction radio", 20);
SCR_ChatComponent.RadioProtocolMessage(text);
}
}
@@ -1,10 +1,229 @@
enum ERR_ExtractionState { WAITING, ESCAPED, LOST }
// Mission progression consumes the result; this service never spawns a vehicle.
class ERR_RescueRules
{
static float BoardingSince(int alive, int aboard, float since, float now)
{
if (alive <= 0 || aboard != alive || now < 0) return -1;
if (since < 0 || now < since) return now;
return since;
}
static bool CanDepart(float since, float now)
{
return since >= 0 && now >= since && now - since >= 8000;
}
}
// Round-owned smoke signal, friendly rescue dispatch, boarding and escape result.
class ERR_ExtractionService
{
static const int REQUIRED_SEATS = 12;
protected ref ERR_PlayerRoster m_Roster;
protected ERR_GameMode m_Mode;
protected vector m_Pickup;
protected bool m_PickupConfigured, m_Signal;
protected ref array<IEntity> m_PreviousSignals = {};
protected int m_Stage, m_DispatchAttempts;
protected float m_NextDispatch, m_AllAboardSince = -1;
protected ref array<ERR_RescueFlightComponent> m_Escorts = {};
protected ERR_RescueFlightComponent m_Transport;
protected ref array<ERR_RescueFlightComponent> m_Transports = {};
protected ref array<vector> m_TransportPickups = {};
protected string m_RescueFaction;
protected string m_RadioPending;
protected float m_RadioUntil;
bool CarryingSurvivorsToSafety()
{
if (m_Stopped || m_Stage != 5 || !FleetAlive()) return false;
array<ref ERR_PlayerLife> lives = {}; m_Roster.Participants(lives);
int aboard;
foreach (ERR_PlayerLife life : lives)
{
if (!ERR_Actors.Alive(life.Actor)) continue;
if (!IsTransport(Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(life.Actor)))) return false;
aboard++;
}
return aboard > 0;
}
bool PickupConfigured() { return m_PickupConfigured; }
vector PickupPosition() { return m_Pickup; }
bool ConfigurePickup(vector position)
{
if (!Replication.IsServer() || m_PickupConfigured || m_Begun || m_Stopped) return false;
m_Mode = ERR_GameMode.Get(); if (!m_Mode) return false;
if (!m_Mode.PlayerFaction()) return false;
m_RescueFaction = m_Mode.PlayerFaction().GetFactionKey();
if (!ERR_RescueLanding.Survey(position, ERR_RescueAssets.TransportCount(m_RescueFaction), m_TransportPickups))
{
PrintFormat("Reintroduced [extraction]: pickup rejected before preparation; faction=%1 required transports=%2 primary=%3; no safe secondary landing footprint", m_RescueFaction, ERR_RescueAssets.TransportCount(m_RescueFaction), position);
return false;
}
m_Pickup = position; m_PickupConfigured = true; return true;
}
protected void Stage(int value, string message)
{
m_Stage = value; m_Mode.PublishPickupStage(value);
PrintFormat("Reintroduced [extraction]: stage=%1 %2", value, message);
m_Mode.DeliverExtractionRadio(message);
m_RadioPending = message; m_RadioUntil = GetGame().GetWorld().GetWorldTime() + 30000;
}
void PreparingRoute()
{
if (!Replication.IsServer() || !m_Mode || m_Stopped) return;
Stage(0, "Escapists, this is rescue command. We are preparing your escape route and assembling transports with armed escort helicopters. Hold the radio position until preparations are complete. Keep your smoke grenades ready and stand by for pickup coordinates.");
}
void OpenPickup()
{
if (!Replication.IsServer() || !m_Mode || m_Stage != 0 || m_Stopped) return;
GetGame().GetWorld().QueryEntitiesBySphere(m_Pickup, 25, PreviousSmoke, null, EQueryEntitiesFlags.ALL);
Stage(1, "Your escape route is ready. The exact pickup point is marked on your map. Gather your team there and throw a smoke grenade within 25 metres of the mark. We will send the transport and armed escorts when we see your smoke.");
}
protected void Radio(float now)
{
if (m_RadioPending.IsEmpty()) return;
if (now > m_RadioUntil) { m_RadioPending = ""; return; }
if (m_Mode.ERR_Speech() && m_Mode.ERR_Speech().Request(m_RadioPending, new ERR_ExtractionSpeechReceiver(m_Mode, m_Stage))) m_RadioPending = "";
}
protected bool PreviousSmoke(IEntity entity)
{
if (!entity || entity.GetParent() || m_PreviousSignals.Count() >= 32) return true;
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(entity.FindComponent(BaseWeaponComponent));
BaseTriggerComponent trigger = BaseTriggerComponent.Cast(entity.FindComponent(BaseTriggerComponent));
if (weapon && weapon.GetWeaponType() == EWeaponType.WT_SMOKEGRENADE && trigger && trigger.WasTriggered()) m_PreviousSignals.Insert(entity);
return true;
}
protected bool Smoke(IEntity entity)
{
if (!entity || entity.GetParent() || m_PreviousSignals.Contains(entity)) return true;
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(entity.FindComponent(BaseWeaponComponent));
BaseTriggerComponent trigger = BaseTriggerComponent.Cast(entity.FindComponent(BaseTriggerComponent));
if (!weapon || weapon.GetWeaponType() != EWeaponType.WT_SMOKEGRENADE || !trigger || !trigger.WasTriggered()) return true;
Instigator instigator = trigger.GetInstigator();
if (!instigator || !m_Roster.CurrentActor(instigator.GetInstigatorEntity())) return true;
// Authority-side grenade state, independent of a dedicated server's renderer.
// Canisters already triggered at this point before the reveal are ignored.
if (Math.AbsFloat(entity.GetOrigin()[1] - m_Pickup[1]) > 8) return true;
m_Signal = true; return false;
}
protected bool Approach(out vector position)
{
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
array<IEntity> players = {}; ERR_Actors.Players(players, true);
// Smaller terrains can use a shorter approach; reserve room for both escorts.
for (int band = 0; band < 3; band++)
for (int attempt = 0; attempt < 12; attempt++)
{
float distance = 1600 - band * 400;
float angle = attempt * Math.PI2 / 12;
position = m_Pickup + Vector(Math.Cos(angle) * distance, 0, Math.Sin(angle) * distance);
if (position[0] < low[0] + 400 || position[0] > high[0] - 400 || position[2] < low[2] + 400 || position[2] > high[2] - 400) continue;
bool near = false;
foreach (IEntity player : players) if (vector.DistanceXZ(player.GetOrigin(), position) < Math.Min(1000, distance - 300)) near = true;
if (near) continue;
position[1] = SCR_TerrainHelper.GetTerrainY(position, GetGame().GetWorld(), noUnderwater: true) + 200;
return true;
}
return false;
}
protected bool LandingPads()
{
int count = ERR_RescueAssets.TransportCount(m_RescueFaction);
// A vehicle or other obstruction may occupy the reserved extra footprint
// during the round. Reuse it only while clear, otherwise survey once again
// before allocating any aircraft. The smoke marker is not rechecked against
// the signalling players gathered there; native landing safety remains active.
if (m_TransportPickups.Count() == count)
if (count == 1 || ERR_RescueLanding.SecondaryClear(m_TransportPickups[1])) return true;
return ERR_RescueLanding.Survey(m_Pickup, count, m_TransportPickups);
}
protected ERR_RescueFlightComponent SpawnFlight(vector origin, int serial, bool escort)
{
ResourceName prefab = ERR_RescueAssets.Aircraft(m_RescueFaction, escort);
Vehicle aircraft = Vehicle.Cast(ERR_Actors.Spawn(prefab, origin));
if (!aircraft) return null;
m_Mode.Own(aircraft);
ERR_RescueFlightComponent flight = ERR_RescueFlightComponent.Cast(aircraft.FindComponent(ERR_RescueFlightComponent));
if (!flight) { ERR_Actors.DeleteSpawned(aircraft); return null; }
vector pickup = m_Pickup;
if (!escort) pickup = m_TransportPickups[serial];
flight.StartRescue(m_Mode, origin, pickup, serial, escort);
PrintFormat("Reintroduced [extraction]: selected faction=%1 escort=%2 serial=%3 prefab=%4 pickup=%5", m_RescueFaction, escort, serial, prefab, pickup);
return flight;
}
protected void Dispatch(float now)
{
if (now < m_NextDispatch) return;
m_NextDispatch = now + 10000;
if (++m_DispatchAttempts > 6) { Stage(6, "Rescue command cannot establish a safe approach. Extraction has failed."); Resolve(ERR_ExtractionState.LOST); return; }
vector origin; if (!LandingPads() || !Approach(origin)) return;
for (int transportIndex = 0; transportIndex < ERR_RescueAssets.TransportCount(m_RescueFaction); transportIndex++)
{
vector transportOrigin = origin + (m_TransportPickups[transportIndex] - m_Pickup) + Vector(0, transportIndex * 40, 0);
ERR_RescueFlightComponent transport = SpawnFlight(transportOrigin, transportIndex, false);
if (!transport)
{
foreach (ERR_RescueFlightComponent partial : m_Transports) if (partial) partial.Release(false);
Stage(6, "Rescue command could not assemble the required transport capacity. Extraction has failed."); Resolve(ERR_ExtractionState.LOST); return;
}
m_Transports.Insert(transport);
}
m_Transport = m_Transports[0];
if (!Assign(m_Transport.Aircraft())) { foreach (ERR_RescueFlightComponent invalid : m_Transports) invalid.Release(false); Resolve(ERR_ExtractionState.LOST); return; }
// Different approach lanes and altitudes keep rotor discs well separated.
for (int index = 0; index < 2; index++)
{
float side = -180; if (index == 1) side = 180;
vector escortOrigin = origin + Vector(side, 60 + index * 40, 180);
ERR_RescueFlightComponent escort = SpawnFlight(escortOrigin, index + m_Transports.Count(), true);
if (escort) m_Escorts.Insert(escort);
}
string inbound = "Smoke confirmed. Rescue transport and armed cover are inbound. Clear the marked landing point. Bring your wounded; we wait for all surviving prisoners to board.";
if (m_Transports.Count() > 1) inbound = "Smoke confirmed. Two rescue transports and armed cover are inbound. The helicopters will land separately near the marker. Use either transport and bring your wounded; both wait until everyone is aboard.";
Stage(3, inbound);
}
protected void RescueTick(float now)
{
Radio(now);
if (m_Stage == 1)
{
GetGame().GetWorld().QueryEntitiesBySphere(m_Pickup, 25, Smoke, null, EQueryEntitiesFlags.ALL);
if (m_Signal) Stage(2, "Signal received. Launching the rescue flight. Keep the landing point clear.");
}
if (m_Stage == 2) Dispatch(now);
if (!m_Transport) return;
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Update(now);
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort && !escort.Lost()) escort.Update(now);
if (!FleetAlive()) { Stage(6, "A rescue transport is down or unable to continue. Extraction has failed."); Resolve(ERR_ExtractionState.LOST); return; }
bool landed = FleetLanded();
if (m_Stage == 3 && landed) Stage(4, "Rescue transports are on the ground. Everyone aboard, including your wounded. Use either rescue transport if there are two. Escorts are covering the pickup. We leave when all surviving prisoners are inside.");
if (m_Stage != 4) return;
if (!landed) { m_AllAboardSince = -1; return; }
array<ref ERR_PlayerLife> lives = {}; m_Roster.Participants(lives);
int alive, aboard;
foreach (ERR_PlayerLife life : lives)
{
if (!ERR_Actors.Alive(life.Actor)) continue;
alive++;
if (IsTransport(Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(life.Actor)))) aboard++;
}
m_AllAboardSince = ERR_RescueRules.BoardingSince(alive, aboard, m_AllAboardSince, now);
if (!ERR_RescueRules.CanDepart(m_AllAboardSince, now)) return;
vector direction = m_Pickup - m_Center; direction[1] = 0;
if (direction.Length() < 1) direction = "1 0 0";
direction.Normalize();
float safeDistance = Math.Max(m_Radius + 400, vector.DistanceXZ(m_Pickup, m_Center) + 2000);
vector safety = m_Center + direction * safeDistance;
safety[1] = SCR_TerrainHelper.GetTerrainY(safety, GetGame().GetWorld(), noUnderwater: true);
vector lateral = Vector(-direction[2], 0, direction[0]);
foreach (int departureIndex, ERR_RescueFlightComponent outbound : m_Transports)
{
vector destination = safety + lateral * (departureIndex * 120);
destination[1] = SCR_TerrainHelper.GetTerrainY(destination, GetGame().GetWorld(), noUnderwater: true);
outbound.Depart(destination);
}
foreach (ERR_RescueFlightComponent cover : m_Escorts) if (cover && !cover.Lost()) cover.Depart(safety + "0 100 200");
Stage(5, "All survivors aboard. Lifting now. Stay inside the transport; escorts are taking you to safety.");
}
protected Vehicle m_Vehicle;
protected bool m_Assigned;
protected bool m_Begun;
@@ -18,6 +237,34 @@ class ERR_ExtractionService
void ERR_ExtractionService(ERR_PlayerRoster roster) { m_Roster = roster; }
Vehicle VehicleEntity() { return m_Vehicle; }
int Vehicles(out array<Vehicle> vehicles)
{
vehicles.Clear();
foreach (ERR_RescueFlightComponent transport : m_Transports)
if (transport && transport.Aircraft() && !vehicles.Contains(transport.Aircraft())) vehicles.Insert(transport.Aircraft());
if (m_Assigned && m_Vehicle && !vehicles.Contains(m_Vehicle)) vehicles.Insert(m_Vehicle);
return vehicles.Count();
}
bool IsTransport(Vehicle aircraft)
{
if (!aircraft || !m_Assigned) return false;
array<Vehicle> vehicles = {}; Vehicles(vehicles);
return vehicles.Contains(aircraft);
}
protected bool FleetAlive()
{
if (!m_Transport || m_Transports.IsEmpty()) return false;
foreach (ERR_RescueFlightComponent transport : m_Transports)
if (!transport || transport.Lost() || !ERR_Actors.Alive(transport.Aircraft())) return false;
return true;
}
protected bool FleetLanded()
{
bool landed = true;
foreach (ERR_RescueFlightComponent transport : m_Transports)
if (!transport || !transport.Landed()) landed = false;
return landed && !m_Transports.IsEmpty();
}
ERR_ExtractionState State() { return m_State; }
ScriptInvoker OnResolved() { return m_OnResolved; }
int EscapedCount() { return m_Escapees.Count(); }
@@ -34,18 +281,23 @@ class ERR_ExtractionService
int Seats(out array<BaseCompartmentSlot> seats)
{
seats.Clear();
if (!m_Vehicle) return 0;
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(m_Vehicle.FindComponent(BaseCompartmentManagerComponent));
if (!manager) return 0;
array<Vehicle> vehicles = {}; Vehicles(vehicles);
foreach (Vehicle aircraft : vehicles) CollectSeats(aircraft, seats);
return seats.Count();
}
protected void CollectSeats(IEntity entity, array<BaseCompartmentSlot> seats)
{
BaseCompartmentManagerComponent manager = BaseCompartmentManagerComponent.Cast(entity.FindComponent(BaseCompartmentManagerComponent));
array<BaseCompartmentSlot> all = {};
manager.GetCompartments(all);
if (manager) manager.GetCompartments(all);
foreach (BaseCompartmentSlot slot : all)
{
if (!slot) continue;
ECompartmentType type = SCR_CompartmentAccessComponent.GetCompartmentType(slot);
if (type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) seats.Insert(slot);
if ((type == ECompartmentType.PILOT || type == ECompartmentType.CARGO) && !seats.Contains(slot)) seats.Insert(slot);
}
return seats.Count();
IEntity child = entity.GetChildren();
while (child) { CollectSeats(child, seats); child = child.GetSibling(); }
}
bool Ready()
{
@@ -58,7 +310,7 @@ class ERR_ExtractionService
}
bool Begin(vector center, float radius)
{
if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !Ready()) return false;
if (!Replication.IsServer() || m_Begun || m_Stopped || !m_Roster || radius <= 0 || !m_PickupConfigured) return false;
m_Center = center;
m_Radius = radius;
m_Begun = true;
@@ -68,13 +320,17 @@ class ERR_ExtractionService
{
if (!Replication.IsServer() || !m_Begun || m_Stopped || m_State != ERR_ExtractionState.WAITING) return;
if (phase < ERR_Phase.PRISON || ERR_MissionRules.Terminal(phase)) return;
if (!ERR_Actors.Alive(m_Vehicle)) { Resolve(ERR_ExtractionState.LOST); return; }
if (phase != ERR_Phase.EXTRACTION || vector.DistanceXZ(m_Vehicle.GetOrigin(), m_Center) <= m_Radius) return;
if (m_Mode) RescueTick(GetGame().GetWorld().GetWorldTime());
if (m_State != ERR_ExtractionState.WAITING || !m_Assigned) return;
if (!FleetAlive()) { Resolve(ERR_ExtractionState.LOST); return; }
if (m_Stage != 5 || phase != ERR_Phase.EXTRACTION) return;
foreach (ERR_RescueFlightComponent departing : m_Transports)
if (vector.DistanceXZ(departing.Aircraft().GetOrigin(), m_Pickup) < 1000 || vector.DistanceXZ(departing.Aircraft().GetOrigin(), m_Center) <= m_Radius) return;
array<ref ERR_PlayerLife> lives = {};
m_Roster.Participants(lives);
foreach (ERR_PlayerLife life : lives)
{
if (!ERR_Actors.Alive(life.Actor) || CompartmentAccessComponent.GetVehicleIn(life.Actor) != m_Vehicle) continue;
if (!ERR_Actors.Alive(life.Actor) || !IsTransport(Vehicle.Cast(CompartmentAccessComponent.GetVehicleIn(life.Actor)))) continue;
// Reserved disconnected characters and unconscious passengers remain alive.
// A recycled connection ID is never an identity for the escape snapshot.
m_Escapees.Insert(life.AccountId);
@@ -97,5 +353,11 @@ class ERR_ExtractionService
foreach (ERR_PlayerLife life : lives)
if (life.Connected && m_Roster.Get(life.ConnectionId) == life && m_Escapees.Contains(life.AccountId)) winners.Insert(life.ConnectionId);
}
void Stop() { m_Stopped = true; }
void Stop()
{
m_Stopped = true; m_RadioPending = ""; m_PreviousSignals.Clear();
foreach (ERR_RescueFlightComponent transport : m_Transports) if (transport) transport.Release(false);
foreach (ERR_RescueFlightComponent escort : m_Escorts) if (escort) escort.Release(false);
m_Escorts.Clear(); m_Transports.Clear(); m_Transport = null;
}
}
@@ -30,6 +30,9 @@ modded class ERR_AdminMenu
bool ready = m_Ready && m_FactionSaved.Count() == 3 && m_FactionActive.Count() == 3;
foreach (SCR_ButtonTextComponent button : m_FactionButtons)
if (button) button.SetEnabled(ready);
bool allRandom = ready;
foreach (string savedRole : ERR_FactionChoices.Roles()) if (m_FactionSaved.Get(savedRole) != "random") allRandom = false;
ERR_SelectionStyle.Choice(SCR_ButtonTextComponent.GetButtonText("FactionsRandomAll", GetRootWidget()), allRandom);
foreach (string role : ERR_FactionChoices.Roles())
{
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
@@ -96,13 +96,16 @@ class ERR_FactionSelection
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
map<string, string> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Saved = loaded;
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
else if (ERR_ProfileConfig.Exists(m_Path) || ERR_ProfileConfig.Exists(m_Path + ".bak"))
{ m_ReadOnly = true; Print("Co10 Escape Reintroduced: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); }
}
protected bool Read(string path, out map<string, string> values)
{
JsonLoadContext json = new JsonLoadContext();
if (!json.LoadFromFile(path)) return false;
return ERR_ProfileConfig.Load(path, json) && Parse(json, values);
}
static bool Parse(JsonLoadContext json, out map<string, string> values)
{
values = new map<string, string>();
foreach (string role : ERR_FactionChoices.Roles())
{
@@ -146,6 +149,11 @@ class ERR_FactionSelection
if (!ERR_FactionChoices.Distinct(values)) return false;
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
foreach (string role : roles) json.WriteValue(role, values.Get(role));
if (m_Path == PATH)
{
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
m_Saved = values; return true;
}
map<string, string> checked;
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Same(values, checked)) return false;
map<string, string> previous;
@@ -26,6 +26,23 @@ class ERR_GameMode : SCR_BaseGameMode
m_SelectedPlayerFaction = player; m_SelectedEnemyFaction = enemy; Replication.BumpMe();
}
protected ref ERR_Settings m_Settings;
[RplProp()] protected int m_StartMode;
protected bool m_StartModeFrozen;
protected bool m_StartPrepared;
int StartMode() { return m_StartMode; }
protected bool FreezeStartMode()
{
if (!Replication.IsServer() || m_Stopped || m_NativeEndHandled || m_Phase != ERR_Phase.PREPARING || !m_Settings) return false;
if (m_StartModeFrozen) return true;
int selected = m_Settings.Get("startMode");
if (!ERR_StartRules.Valid(selected)) return false;
m_StartMode = selected; m_StartModeFrozen = true;
Replication.BumpMe();
return true;
}
[RplProp(onRplName: "ApplyLocalViewDistance")]
protected int m_MissionViewDistance;
protected ref ERR_ViewDistance m_LocalViewDistance = new ERR_ViewDistance();
protected ref ERR_ContentPolicy m_Content;
protected ref ERR_ContentSettings m_ContentSettings;
protected ref ERR_TimeService m_Time = new ERR_TimeService();
@@ -35,6 +52,9 @@ class ERR_GameMode : SCR_BaseGameMode
protected ref ERR_PlayerRoster m_Roster = new ERR_PlayerRoster();
protected ref ERR_AdminService m_Admin = new ERR_AdminService();
protected ref ERR_CommandService m_Command;
protected ref ERR_Performance m_Performance;
ERR_Performance Performance() { return m_Performance; }
protected ref ERR_CivilianTraffic m_CivilianTraffic;
protected ref ERR_ForceLedger m_Forces = new ERR_ForceLedger();
protected ref ERR_ForceSimulation m_ForceSimulation;
protected ref array<AIAgent> m_ForceAgents = {};
@@ -73,6 +93,27 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_PlayerRoster Players() { return m_Roster; }
ERR_AdminService Admin() { return m_Admin; }
ERR_Settings Settings() { return m_Settings; }
// Publish the server-bounded value; replicated state also reaches late joiners.
void PublishViewDistance()
{
if (!Replication.IsServer() || !m_Settings || m_Stopped || m_NativeEndHandled) return;
m_MissionViewDistance = ERR_ViewDistance.ServerDistance(m_Settings.Get("viewDistanceMetres"), GetGame().GetViewDistanceServerLimit());
Replication.BumpMe();
ApplyLocalViewDistance();
}
protected void ApplyLocalViewDistance()
{
if (!GetGame() || System.IsConsoleApp() || m_Stopped || m_NativeEndHandled) return;
// Inventory and video menus temporarily own the native camera distance.
if (GetGame().GetMenuManager() && GetGame().GetMenuManager().GetTopMenu()) return;
m_LocalViewDistance.Apply(m_MissionViewDistance);
}
protected void StopLocalViewDistance()
{
if (!GetGame()) return;
GetGame().GetCallqueue().Remove(ApplyLocalViewDistance);
m_LocalViewDistance.Restore();
}
ERR_ContentPolicy Content() { return m_Content; }
ERR_ContentSettings ContentSettings() { return m_ContentSettings; }
bool IsMissionEnemy(Faction faction)
@@ -88,6 +129,7 @@ class ERR_GameMode : SCR_BaseGameMode
string RecoveryStatus() { return m_RecoveryStatus; }
ERR_ForceLedger Forces() { return m_Forces; }
ERR_CommandService Command() { return m_Command; }
ERR_CivilianTraffic CivilianTraffic() { return m_CivilianTraffic; }
ERR_Objectives Objectives() { return m_Objectives; }
ERR_ExtractionService Extraction() { return m_Extraction; }
string EndReason() { return m_EndReason; }
@@ -130,14 +172,18 @@ class ERR_GameMode : SCR_BaseGameMode
ERR_FactionCatalog Factions() { return m_Factions; }
Faction PlayerFaction()
{
if (!m_Factions) return null;
if (!m_SelectedPlayerFaction.IsEmpty()) return GetGame().GetFactionManager().GetFactionByKey(m_SelectedPlayerFaction);
if (!m_Factions || !GetGame()) return null;
FactionManager manager = GetGame().GetFactionManager();
if (!manager) return null;
if (!m_SelectedPlayerFaction.IsEmpty()) return manager.GetFactionByKey(m_SelectedPlayerFaction);
return m_Factions.PlayerFaction();
}
Faction EnemyFaction()
{
if (!m_Factions) return null;
if (!m_SelectedEnemyFaction.IsEmpty()) return GetGame().GetFactionManager().GetFactionByKey(m_SelectedEnemyFaction);
if (!m_Factions || !GetGame()) return null;
FactionManager manager = GetGame().GetFactionManager();
if (!manager) return null;
if (!m_SelectedEnemyFaction.IsEmpty()) return manager.GetFactionByKey(m_SelectedEnemyFaction);
return m_Factions.EnemyFaction();
}
string PreparationError() { return m_PreparationError; }
@@ -201,7 +247,7 @@ class ERR_GameMode : SCR_BaseGameMode
}
void RegisterLocation(ERR_LocationComponent location)
{
if (!location || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
if (!location || !location.Prepared() || location.Id().IsEmpty() || m_Locations.Contains(location)) return;
foreach (ERR_LocationComponent existing : m_Locations)
{
if (existing.Id() == location.Id())
@@ -212,9 +258,29 @@ class ERR_GameMode : SCR_BaseGameMode
}
m_Locations.Insert(location);
}
bool WorldPreparing()
{
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
return scenario && scenario.BackgroundPending();
}
void WorldPreparationFailed(string reason)
{
if (!Replication.IsServer() || !Active()) return;
m_EndReason = "World generation failed: " + reason;
Print(m_EndReason, LogLevel.ERROR);
Transition(ERR_Phase.DEFEAT);
}
bool HasLocation(ERR_LocationComponent location) { return location && m_Locations.Contains(location); }
void UnregisterLocation(ERR_LocationComponent location) { m_Locations.RemoveItem(location); }
void Locations(out array<ERR_LocationComponent> locations) { locations.Copy(m_Locations); }
// Read only, once per telemetry window. Does not query/stream the terrain.
int OwnedRootCount()
{
int count;
foreach (IEntity entity : m_OwnedEntities) if (entity) count++;
return count;
}
void Own(IEntity entity) { if (entity && !m_OwnedEntities.Contains(entity)) m_OwnedEntities.Insert(entity); }
bool SetExtractionVehicle(Vehicle vehicle)
{
@@ -285,10 +351,11 @@ class ERR_GameMode : SCR_BaseGameMode
}
bool Transition(ERR_Phase phase)
{
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase)) return false;
if (!Replication.IsServer() || !m_PreparationError.IsEmpty() || !ERR_MissionRules.CanTransition(m_Phase, phase, m_StartModeFrozen && m_StartPrepared && m_StartMode == 1)) return false;
if (m_Phase == ERR_Phase.PREPARING && !m_StartPrepared) return false;
ERR_Phase previous = m_Phase;
m_Phase = phase;
if (previous == ERR_Phase.PRISON && phase == ERR_Phase.RADIO_SEARCH)
if ((previous == ERR_Phase.PRISON || previous == ERR_Phase.PREPARING) && phase == ERR_Phase.RADIO_SEARCH)
{
// Dormant patrols must not catch up the time spent waiting in prison.
for (int i = 0; i < m_Forces.Count(); i++)
@@ -330,13 +397,14 @@ class ERR_GameMode : SCR_BaseGameMode
{
if (!Replication.IsServer() || !m_RadioDefence || location != m_RadioDefence.Location()) return;
if (!Transition(ERR_Phase.RADIO_DEFENCE)) m_RadioDefence.Stop();
else m_Extraction.PreparingRoute();
}
protected void RadioCompleted(ERR_LocationComponent location)
{
if (!Replication.IsServer() || m_Phase != ERR_Phase.RADIO_DEFENCE || !m_RadioDefence || m_RadioDefence.State() != ERR_RadioDefenceState.COMPLETED || location != m_RadioDefence.Location()) return;
foreach (ERR_LocationComponent candidate : m_Locations)
if (candidate.Kind() == ERR_LocationKind.EXTRACTION) candidate.Discover(0, 100);
Transition(ERR_Phase.EXTRACTION);
if (Transition(ERR_Phase.EXTRACTION)) m_Extraction.OpenPickup();
}
void PlayerSpawned(int id, IEntity actor)
@@ -387,13 +455,17 @@ class ERR_GameMode : SCR_BaseGameMode
if (SCR_Global.IsEditMode()) return;
m_Factions = ERR_FactionCatalog.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_FactionDefinition));
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(BindLocalObserver, 250, true);
if (!System.IsConsoleApp()) GetGame().GetCallqueue().CallLater(ApplyLocalViewDistance, 1000, true);
if (!Replication.IsServer()) return;
m_Settings = new ERR_Settings(SettingsPath());
if (!System.IsCLIParam("errNoPerf")) m_Performance = new ERR_Performance();
PublishViewDistance();
m_ContentSettings = new ERR_ContentSettings();
m_Content = new ERR_ContentPolicy(m_ContentSettings);
m_FactionSelection = new ERR_FactionSelection();
m_Admin.Register(new ERR_ContentAdminProvider(m_Content));
m_Admin.Register(new ERR_FactionAdminProvider());
m_Admin.Register(new ERR_TerrainAdminProvider());
OnStop().Insert(m_Environment.Stop);
m_Recovery = new ERR_RecoveryService(this);
m_Recovery.OnChanged().Insert(RecoveryChanged);
@@ -421,6 +493,8 @@ class ERR_GameMode : SCR_BaseGameMode
OnStop().Insert(m_Objectives.Stop);
m_ForceSimulation = new ERR_ForceSimulation(m_Forces);
m_Command = CreateCommandService();
m_CivilianTraffic = new ERR_CivilianTraffic(this);
OnStop().Insert(m_CivilianTraffic.Stop);
GetGame().GetCallqueue().CallLater(ServerTick, 1000, true);
GetGame().GetCallqueue().CallLater(StartScenario, 1500, false);
}
@@ -435,7 +509,7 @@ class ERR_GameMode : SCR_BaseGameMode
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
if (!controller) return;
controller.ERR_StartObserverMonitor();
GetGame().GetCallqueue().Remove(BindLocalObserver);
// Keep watching for a replacement local controller; Start is idempotent.
}
protected void StartScenario()
@@ -444,6 +518,7 @@ class ERR_GameMode : SCR_BaseGameMode
if (!m_FactionSelection || !m_FactionSelection.Prepare(this)) { FailPreparation("Three enabled faction profiles are required for the mission roles."); return; }
if (!m_Factions || !m_Factions.WorldReady()) { FailPreparation("The configured player and enemy factions or required assets are missing, invalid or not hostile."); return; }
if (!m_Content || !m_Content.PrepareFactions(this)) { FailPreparation("The content registry or enabled faction profiles are invalid. Check faction keys, guard groups and the mission faction manager."); return; }
if (!FreezeStartMode()) { FailPreparation("The selected mission start is invalid."); return; }
// Scenario content is created through a separate authoring component.
ERR_ScenarioComponent scenario = ERR_ScenarioComponent.Cast(FindComponent(ERR_ScenarioComponent));
ERR_PrepareResult prepared = ERR_PrepareResult.FAILED;
@@ -462,12 +537,14 @@ class ERR_GameMode : SCR_BaseGameMode
}
if (!m_Extraction || !m_Extraction.Begin(m_EscapeCenter, m_EscapeRadius))
{
FailPreparation("The extraction vehicle must be a helicopter with a pilot position and at least twelve pilot/passenger seats, with a valid escape boundary.");
FailPreparation("The extraction pickup must be a prepared landing zone with a valid escape boundary.");
return;
}
if (!m_Environment.Start(this)) { FailPreparation("The selected date or vanilla weather profile could not be initialized."); return; }
if (!m_Time.Start(this)) { FailPreparation("The world clock could not be initialized."); return; }
Transition(ERR_Phase.PRISON);
m_StartPrepared = true;
if (m_StartMode == 1) Transition(ERR_Phase.RADIO_SEARCH);
else Transition(ERR_Phase.PRISON);
}
protected void ExtractionResolved(ERR_ExtractionState result)
@@ -486,6 +563,15 @@ class ERR_GameMode : SCR_BaseGameMode
}
protected void ServerTick()
{
if (!Replication.IsServer() || !Active()) return;
int started;
if (m_Performance) started = m_Performance.BeginTick();
ServerTickMeasured();
if (m_Performance) m_Performance.EndTick(started);
}
protected void ServerTickMeasured()
{
if (!Replication.IsServer() || !Active()) return;
float now = GetWorld().GetWorldTime();
@@ -502,27 +588,41 @@ class ERR_GameMode : SCR_BaseGameMode
Transition(ERR_Phase.DEFEAT);
return;
}
int perfStamp;
if (m_Performance) perfStamp = System.GetTickCount();
if (m_Command) m_Command.Maintain(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.MAINTENANCE, perfStamp);
if (m_CivilianTraffic) m_CivilianTraffic.Tick(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.CIVILIANS, perfStamp);
// Maintenance can finish or stop the round; do not resume tactical work.
if (!Active()) return;
if (m_Phase >= ERR_Phase.RADIO_SEARCH) TickForces(now);
if (GetGame().GetPlayerManager().GetPlayerCount() == 0)
if (m_Phase >= ERR_Phase.RADIO_SEARCH)
{
m_NoSurvivorsSince = -1;
return;
TickForces(now);
if (m_Performance) m_Performance.Mark(ERR_PerfMetric.FORCES, perfStamp);
}
if ((m_StartedPlayers || InitialAdmissionsExhausted()) && !m_Roster.HasSurvivor())
{
if (m_NoSurvivorsSince < 0) m_NoSurvivorsSince = now;
if (m_NoSurvivorsSince < 0)
{
m_NoSurvivorsSince = now;
PrintFormat("Reintroduced [round lifecycle]: no survivors; starting 30s grace; previouslyPlayed=%1 connected=%2", m_StartedPlayers, GetGame().GetPlayerManager().GetPlayerCount());
}
if (now - m_NoSurvivorsSince >= 30000)
{
m_EndReason = "No surviving prisoners remain.";
if (!m_StartedPlayers) m_EndReason = "No prisoners could be admitted to this round.";
PrintFormat("Reintroduced [round lifecycle]: ending exhausted round; connected=%1; %2", GetGame().GetPlayerManager().GetPlayerCount(), m_EndReason);
Transition(ERR_Phase.DEFEAT);
return;
}
}
else m_NoSurvivorsSince = -1;
// A played round must finish after its last reserved body expires, even
// with nobody connected. Keep tactical/observer work idle until players
// return; a fresh unplayed round still waits without a restart loop.
if (GetGame().GetPlayerManager().GetPlayerCount() == 0) return;
if (m_Performance) perfStamp = System.GetTickCount();
array<int> connected = {};
GetGame().GetPlayerManager().GetPlayers(connected);
foreach (int id : connected)
@@ -530,8 +630,14 @@ class ERR_GameMode : SCR_BaseGameMode
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerManager().GetPlayerController(id));
if (controller) controller.ERR_UpdateObserverServer();
}
if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH) m_Command.Tick(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.OBSERVERS, perfStamp);
if (m_Command && m_Phase >= ERR_Phase.RADIO_SEARCH)
{
m_Command.Tick(now);
if (m_Performance) perfStamp = m_Performance.Mark(ERR_PerfMetric.HQ_TICK, perfStamp);
}
m_OnTick.Invoke(now);
if (m_Performance) m_Performance.Mark(ERR_PerfMetric.LISTENERS, perfStamp);
}
// Vanilla editor/session termination enters POSTGAME directly, without our
@@ -540,6 +646,7 @@ class ERR_GameMode : SCR_BaseGameMode
{
if (m_NativeEndHandled) return;
m_NativeEndHandled = true;
StopLocalViewDistance();
GetGame().GetCallqueue().Remove(BindLocalObserver);
if (Replication.IsServer())
{
@@ -564,11 +671,13 @@ class ERR_GameMode : SCR_BaseGameMode
{
if (m_Stopped) return;
m_Stopped = true;
StopLocalViewDistance();
GetGame().GetCallqueue().Remove(StartScenario);
GetGame().GetCallqueue().Remove(ServerTick);
GetGame().GetCallqueue().Remove(BindLocalObserver);
if (m_Command) m_Command.Stop();
m_OnStop.Invoke();
if (m_Performance) m_Performance.Stop(m_Phase);
m_Forces.Clear();
m_ForceAgents.Clear();
if (Replication.IsServer())
@@ -1,5 +1,5 @@
// Clear-ground sampling adapted from the owner's improvised-prison extension (see migration provenance).
// Searches create no entities and finish before any site starts spawning.
// Searches create no entities; startup placements can precede the rest of the island.
class ERR_CampTerrain
{
protected ref array<vector> m_Airfields = {};
@@ -12,6 +12,7 @@ class ERR_CampTerrain
protected bool AddAirport(IEntity entity) { m_Airfields.Insert(entity.GetOrigin()); return true; }
void Collect(vector low, vector high)
{
ERR_AirfieldPlacement.AuthoredAirports(m_Airfields);
GetGame().GetWorld().QueryEntitiesByAABB(Vector(low[0], -1000, low[2]), Vector(high[0], 5000, high[2]), AddAirport, Airport, EQueryEntitiesFlags.ALL);
}
static vector Rotate(vector offset, float heading)
@@ -24,7 +25,7 @@ class ERR_CampTerrain
vector rotation[3]; Math3D.AnglesToMatrix(Vector(heading, 0, 0), rotation);
return Vector(vector.Dot(offset, rotation[0]), offset[1], vector.Dot(offset, rotation[2]));
}
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest)
bool Clear(vector center, float heading, float halfSize, float maximumRange, out float lowest, float clearanceHeight = 4)
{
lowest = float.MAX; float highest = -float.MAX;
for (int x = -1; x <= 1; x++)
@@ -48,7 +49,7 @@ class ERR_CampTerrain
vector cell = center + Rotate(Vector(cx, 0, cz), heading);
float ground = GetGame().GetWorld().GetSurfaceY(cell[0], cell[2]);
TraceBox trace = new TraceBox();
trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + "0 4 0";
trace.Start = Vector(cell[0], ground + 0.12, cell[2]); trace.End = trace.Start + Vector(0, clearanceHeight, 0);
// Bounding the rotated cell conservatively also protects the exit corridor.
trace.Mins = "-1.42 0 -1.42"; trace.Maxs = "1.42 0.1 1.42";
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
@@ -69,6 +70,13 @@ class ERR_CampTerrain
aiWorld.GetRoadNetworkManager().GetClosestRoad(position, road, distance, skipNavlinks: true);
if (road && distance < 12) return false;
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
if (ERR_StartSetup.IsCrash(site))
{
vector offset, center; float halfSize, height;
if (!ERR_StartSetup.Geometry(offset, center, halfSize, height)) return false;
float wreckFloor;
if (!Clear(position + Rotate(center, heading), heading, halfSize, 0.6, wreckFloor, height)) return false;
}
float guardHeight;
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
if (site.ResponseCount > 0 && !Clear(position + Rotate(site.ResponseOffset, heading), heading, 6, 0.6, guardHeight)) return false;
@@ -86,12 +94,31 @@ class ERR_GeneratedSites
protected ERR_PrepareResult m_Result = ERR_PrepareResult.PENDING;
protected string m_Error;
string Error() { return m_Error; }
protected bool m_StartupOnly;
static bool StartupSite(ERR_SiteDefinition site)
{
return site && (site.Kind == ERR_LocationKind.PRISON || site.Kind == ERR_LocationKind.EXTRACTION);
}
void StartupOnly(bool value)
{
if (m_StartupOnly && !value) m_Began = System.GetTickCount();
m_StartupOnly = value;
}
bool StartupReady()
{
if (!m_Started || !m_Definition || m_Result == ERR_PrepareResult.FAILED) return false;
foreach (ERR_SiteDefinition site : m_Definition.Sites)
if (StartupSite(site) && (site.RandomClear || m_Definition.TerrainGenerated) && !site.PlacementResolved) return false;
return true;
}
bool Begin(ERR_ScenarioDefinition definition)
{
if (m_Started || !Replication.IsServer() || !definition || !definition.Valid() || !GetGame().GetWorldEntity()) return false;
m_Started = true; m_Definition = definition;
// Prison resolves first, irrespective of authored array order.
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
foreach (ERR_SiteDefinition extraction : definition.Sites) if (extraction.RandomClear && extraction.Kind == ERR_LocationKind.EXTRACTION) m_Pending.Insert(extraction);
foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio);
foreach (ERR_SiteDefinition mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar);
foreach (ERR_SiteDefinition optional : definition.Sites)
@@ -138,6 +165,12 @@ class ERR_GeneratedSites
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;
}
protected bool RescueFits(ERR_SiteDefinition site, vector position, float heading)
{
if (site.Kind != ERR_LocationKind.EXTRACTION) return true;
vector primary = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(site.AircraftOffset, heading));
return ERR_RescueLanding.Fits(primary);
}
ERR_PrepareResult Tick()
{
if (m_Result != ERR_PrepareResult.PENDING) return m_Result;
@@ -145,6 +178,7 @@ class ERR_GeneratedSites
int tick = System.GetTickCount();
while (m_Index < m_Pending.Count())
{
if (m_StartupOnly && StartupReady()) return ERR_PrepareResult.PENDING;
ERR_SiteDefinition site = m_Pending[m_Index];
bool fallback = m_Attempts >= 12000 || System.GetTickCount() - m_Began >= 30000;
vector position = site.Position; float heading = site.Heading;
@@ -154,7 +188,7 @@ class ERR_GeneratedSites
heading = Math.RandomFloat(0, 360); m_Attempts++;
}
float floor;
if (Separated(site, position, fallback) && m_Terrain.Site(site, position, heading, floor))
if (Separated(site, position, fallback) && m_Terrain.Site(site, position, heading, floor) && RescueFits(site, position, heading))
{
position[1] = floor + 0.02;
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
@@ -0,0 +1,49 @@
// Vanilla damage-state invokers supply one argument. Preserve the native UI
// update while supplying its optional attachment-refresh argument explicitly.
modded class SCR_InventoryHitZonePointContainerUI
{
protected ERR_GameMode m_ERRHitZoneMode;
protected ref array<SCR_HitZone> m_ERRHitZoneSources = {};
override void InitializeHitZoneUI(BaseInventoryStorageComponent storage, SCR_InventoryMenuUI menuManager, int hitZoneId, IEntity player, bool updateAttachmentStorage = true)
{
ERR_ClearHitZoneCallbacks();
super.InitializeHitZoneUI(storage, menuManager, hitZoneId, player, updateAttachmentStorage);
m_ERRHitZoneMode = ERR_GameMode.Get();
if (!m_ERRHitZoneMode) return;
ERR_BindHitZoneCallback(m_pBloodHitZone);
foreach (HitZone hz : m_aGroupHitZones)
ERR_BindHitZoneCallback(SCR_CharacterHitZone.Cast(hz));
}
protected void ERR_BindHitZoneCallback(SCR_HitZone hz)
{
if (!hz || m_ERRHitZoneSources.Contains(hz)) return;
ScriptInvoker changed = hz.GetOnDamageStateChanged(false);
if (!changed) return;
changed.Remove(UpdateHitZoneState);
changed.Insert(ERR_HitZoneStateChanged);
m_ERRHitZoneSources.Insert(hz);
}
protected void ERR_HitZoneStateChanged(SCR_HitZone hz)
{
if (!m_ERRHitZoneMode || m_ERRHitZoneMode != ERR_GameMode.Get() || !m_ERRHitZoneSources.Contains(hz)) return;
UpdateHitZoneState(hz, true);
}
protected void ERR_ClearHitZoneCallbacks()
{
m_ERRHitZoneMode = null;
foreach (SCR_HitZone hz : m_ERRHitZoneSources)
{
if (!hz) continue;
ScriptInvoker changed = hz.GetOnDamageStateChanged(false);
if (changed) changed.Remove(ERR_HitZoneStateChanged);
}
m_ERRHitZoneSources.Clear();
}
override void HandlerDeattached(Widget w)
{
ERR_ClearHitZoneCallbacks();
super.HandlerDeattached(w);
}
void ~SCR_InventoryHitZonePointContainerUI() { ERR_ClearHitZoneCallbacks(); }
}
@@ -0,0 +1,230 @@
// One server-owned working selection and named snapshots. Absent addons stay
// dormant: evaluation only visits the sources of a currently loaded resource.
class ERR_ItemPreferences
{
static const string PATH = "$profile:EscapistsItems.json";
protected string m_Path;
protected bool m_ReadOnly;
protected int m_Revision;
protected ref array<string> m_Records = {"active"};
protected ref map<string, int> m_Rules = new map<string, int>();
protected ref map<string, string> m_Loaded = new map<string, string>();
void ERR_ItemPreferences(string path = PATH)
{
m_Path = path;
if (!Replication.IsServer()) { m_ReadOnly = true; return; }
array<string> addons = {}; GameProject.GetLoadedAddons(addons);
foreach (string guid : addons)
{
string id = guid; id.ToLower();
m_Loaded.Set(GameProject.GetAddonID(guid), id);
}
array<string> loaded;
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Records = loaded;
else if (ERR_ProfileConfig.Exists(m_Path) || ERR_ProfileConfig.Exists(m_Path + ".bak"))
{
m_ReadOnly = true;
Print("Reintroduced [items]: invalid preferences; defaults active, damaged files preserved", LogLevel.ERROR);
}
Rebuild();
}
static bool Name(string value)
{
if (value.IsEmpty() || value.Length() > 24) return false;
string allowed = "abcdefghijklmnopqrstuvwxyz0123456789_-";
for (int i = 0; i < value.Length(); i++) if (allowed.IndexOf(value.Substring(i, 1)) < 0) return false;
return true;
}
static bool Guid(string value)
{
if (value.Length() != 16) return false;
for (int i = 0; i < 16; i++) if ("0123456789abcdef".IndexOf(value.Substring(i, 1)) < 0) return false;
return true;
}
static bool WeightKey(string key)
{
return key == "w*" || (key.Length() == 17 && key.Substring(0, 1) == "w" && Guid(key.Substring(1, 16)));
}
static bool WeightValue(string text)
{
int value;
return ERR_CommandArguments.Integer(text, value) && value >= 0 && value <= 100;
}
static bool Valid(array<string> records)
{
if (!records || records.Count() > 65536 || !records.Contains("active")) return false;
array<string> names = {};
set<string> keys = new set<string>();
foreach (string row : records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 1)
{
if (!Name(row) || names.Contains(row)) return false;
names.Insert(row);
}
else
{
if (parts.Count() != 4 || !Name(parts[0]) || !Guid(parts[1])) return false;
if (WeightKey(parts[2])) { if (!WeightValue(parts[3])) return false; }
else if ((parts[2] != "*" && !Guid(parts[2])) || (parts[3] != "0" && parts[3] != "1")) return false;
}
}
if (names.Count() > 17) return false;
foreach (string row : records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 1) continue;
string key = parts[0] + "|" + parts[1] + "|" + parts[2];
if (!names.Contains(parts[0]) || keys.Contains(key)) return false;
keys.Insert(key);
}
return true;
}
protected bool Read(string path, out array<string> records)
{
JsonLoadContext json = new JsonLoadContext();
return ERR_ProfileConfig.Load(path, json) && Parse(json, records);
}
static bool Parse(JsonLoadContext json, out array<string> records)
{
records = {}; int version;
return json.ReadValue("version", version) && (version == 1 || version == 2) && json.ReadValue("records", records) && Valid(records);
}
protected bool Equal(array<string> a, array<string> b)
{
if (!a || !b || a.Count() != b.Count()) return false;
for (int i = 0; i < a.Count(); i++) if (a[i] != b[i]) return false;
return true;
}
protected void Rebuild()
{
m_Rules.Clear();
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == "active") m_Rules.Set(parts[1] + "|" + parts[2], parts[3].ToInt());
}
m_Revision++;
}
protected bool Save(array<string> records)
{
if (!Replication.IsServer() || m_ReadOnly || !Valid(records)) return false;
JsonSaveContext json = new JsonSaveContext(); json.EnableTypeDiscriminator(false);
json.WriteValue("version", 2); json.WriteValue("records", records);
if (m_Path == PATH)
{
if (!ERR_ProfileConfig.Save(PATH, json)) return false;
m_Records = records; Rebuild(); return true;
}
array<string> checked, previous;
if (!json.SaveToFile(m_Path + ".tmp") || !Read(m_Path + ".tmp", checked) || !Equal(records, checked)) return false;
bool hadPrevious = Read(m_Path, previous);
if (hadPrevious && !FileIO.CopyFile(m_Path, m_Path + ".bak")) return false;
if (!FileIO.CopyFile(m_Path + ".tmp", m_Path) || !Read(m_Path, checked) || !Equal(records, checked))
{
if (hadPrevious) FileIO.CopyFile(m_Path + ".bak", m_Path);
else FileIO.DeleteFile(m_Path);
return false;
}
m_Records = checked; Rebuild(); return true;
}
int Revision() { return m_Revision; }
bool Enabled(string addon, string item)
{
string key = addon + "|" + item;
if (m_Rules.Contains(key)) return m_Rules.Get(key) == 1;
key = addon + "|*";
return !m_Rules.Contains(key) || m_Rules.Get(key) == 1;
}
bool Allows(ResourceName prefab)
{
string item = ERR_TerrainCatalog.Key(prefab);
if (item.IsEmpty()) return true;
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
foreach (string project : sources)
if (m_Loaded.Contains(project) && !Enabled(m_Loaded.Get(project), item)) return false;
return true;
}
bool Set(string addon, string item, bool enabled)
{
if (!Guid(addon) || (item != "*" && !Guid(item))) return false;
array<string> next = {};
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == "active" && parts[1] == addon && (item == "*" || parts[2] == item) && !WeightKey(parts[2])) continue;
next.Insert(row);
}
string value = "0"; if (enabled) value = "1";
next.Insert("active|" + addon + "|" + item + "|" + value);
return Save(next);
}
// Relative lottery weights, separate from enable/disable permissions. Shared
// resource contributors use the highest multipliers, never duplicate tickets.
// Zero from any contributor excludes the item, just like a disabled source.
int WeightSetting(string addon, string item)
{
string key = addon + "|w" + item;
if (m_Rules.Contains(key)) return m_Rules.Get(key);
return 1;
}
int Weight(ResourceName prefab)
{
string item = ERR_TerrainCatalog.Key(prefab);
int modWeight = 1, itemWeight = 1;
array<string> sources = SCR_AddonTool.GetResourceAddons(prefab);
foreach (string project : sources)
{
if (!m_Loaded.Contains(project)) continue;
string addon = m_Loaded.Get(project);
int modValue = WeightSetting(addon, "*"), itemValue = WeightSetting(addon, item);
if (modValue == 0 || itemValue == 0) return 0;
modWeight = Math.Max(modWeight, modValue); itemWeight = Math.Max(itemWeight, itemValue);
}
return modWeight * itemWeight;
}
bool SetWeight(string addon, string item, int weight)
{
if (!Guid(addon) || (item != "*" && !Guid(item)) || weight < 0 || weight > 100) return false;
string key = "w" + item;
array<string> next = {};
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == "active" && parts[1] == addon && parts[2] == key) continue;
next.Insert(row);
}
if (weight != 1) next.Insert("active|" + addon + "|" + key + "|" + weight.ToString());
return Save(next);
}
bool Preset(string action, string name)
{
if (!Name(name) || name == "active" || (action != "save" && action != "load" && action != "delete")) return false;
if (action != "save" && !m_Records.Contains(name)) return false;
string target = name, source = "active";
if (action == "load") { target = "active"; source = name; }
array<string> next = {};
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts[0] != target) next.Insert(row);
}
if (action != "delete")
{
next.Insert(target);
foreach (string row : m_Records)
{
array<string> parts = {}; row.Split("|", parts, false);
if (parts.Count() == 4 && parts[0] == source) next.Insert(target + "|" + parts[1] + "|" + parts[2] + "|" + parts[3]);
}
}
return Save(next);
}
void Snapshot(array<string> values)
{
foreach (string row : m_Records) if (row != "active" && row.IndexOf("|") < 0) values.Insert("itemPreset=" + row);
if (m_ReadOnly) values.Insert("itemsStorage=Read-only: preference file needs repair");
else values.Insert("itemsStorage=Auto-saved; applies to future random equipment. Presets include selections and probability weights for all mods.");
}
}
@@ -0,0 +1,57 @@
// A support station's cleanup must not require a still-living item catalog.
// Remember the subscriptions made by successful native initialization, rather
// than rerunning startup validation after that catalog has been destroyed.
modded class SCR_BaseItemSupportStationComponent
{
protected bool m_ERRItemStationScoped;
protected bool m_ERRItemStationInitializing;
protected bool m_ERRItemStationInitialized;
protected bool m_ERRItemStationDeleted;
protected SCR_ResourceComponent m_ERRItemStationResource;
protected SCR_SupportStationManagerComponent m_ERRItemStationManager;
override void OnPostInit(IEntity owner)
{
m_ERRItemStationScoped = !SCR_Global.IsEditMode() && (ERR_GameMode.Get() || ERR_TerrainTakeoverSettings.Enabled());
super.OnPostInit(owner);
}
protected override void DelayedInit(IEntity owner)
{
if (m_ERRItemStationDeleted) return;
// A preloader/preview is not a playable world. Its transient equipment may
// be created before any game mode/catalog exists and immediately discarded.
if (m_ERRItemStationScoped && owner && GetGame() && owner.GetWorld() != GetGame().GetWorld() && !SCR_EntityCatalogManagerComponent.GetInstance()) return;
m_ERRItemStationInitializing = m_ERRItemStationScoped;
super.DelayedInit(owner);
m_ERRItemStationInitializing = false;
if (m_ERRItemStationInitialized && UsesRange()) m_ERRItemStationManager = SCR_SupportStationManagerComponent.GetInstance();
}
protected override void AddRemoveOnDamageStateChanged(IEntity owner, bool subscribe)
{
super.AddRemoveOnDamageStateChanged(owner, subscribe);
// This hook is reached only after the complete native virtual validation.
if (!subscribe || !m_ERRItemStationInitializing) return;
m_ERRItemStationInitialized = true;
if (m_ResourceConsumer || m_ResourceGenerator) m_ERRItemStationResource = GetResourceComponent();
}
override void OnDelete(IEntity owner)
{
if (!m_ERRItemStationScoped || SCR_Global.IsEditMode())
{
super.OnDelete(owner);
return;
}
if (m_ERRItemStationDeleted) return;
m_ERRItemStationDeleted = true;
if (GetGame()) GetGame().GetCallqueue().Remove(DelayedInit);
if (m_ERRItemStationInitialized)
{
AddRemoveOnDamageStateChanged(owner, false);
// Use the managers this station joined, never a new round's singleton.
if (m_ERRItemStationManager) m_ERRItemStationManager.RemoveSupportStation(this);
if (m_ERRItemStationResource) m_ERRItemStationResource.TEMP_GetOnInteractorReplicated().Remove(TEMP_OnInteractorReplicated);
}
m_ERRItemStationManager = null;
m_ERRItemStationResource = null;
}
}
@@ -32,6 +32,7 @@ class ERR_JournalText
if (end < 0) return Short(message);
string result = message.Substring(0, end);
result.Replace("aggression-uaz-", "UAZ patrol ");
result.Replace("aggression-truck-", "Troop-truck patrol ");
return result;
}
static string YesNo(string value)
@@ -45,7 +46,7 @@ class ERR_JournalText
if (kind == "REGISTER") return "Group " + Field(message, "group") + " joined HQ: " + Field(message, "members") + " soldiers, faction " + Field(message, "faction") + ". Assignment: " + Field(message, "force") + ".";
if (kind == "SPAWN MEMBER") return "Group " + Field(message, "group") + ": soldier spawned at " + Field(message, "position") + ".";
if (kind == "CONTACT LOCAL") return "Group " + Field(message, "group") + " spotted a target at " + Field(message, "observed") + ". Local combat has started; the report still needs to reach HQ.";
if (kind == "AGGRESSION") return "HQ has identified " + Field(message, "bodies") + " bodies. Aggression: " + Field(message, "aggression") + ". Total hunter UAZ allowance: " + Field(message, "allowance") + ".";
if (kind == "AGGRESSION") return "HQ has identified " + Field(message, "bodies") + " bodies. Aggression: " + Field(message, "aggression") + ". Total hunting vehicle allowance: " + Field(message, "allowance") + ".";
if (kind == "HUNTER SPAWN") return Subject(message) + " deployed with " + Field(message, "crew") + " crew at " + Field(message, "position") + ". Deployment source: " + Field(message, "base") + ".";
if (kind == "HUNTER READY") return Subject(message) + ": driver and gunner are seated; registered with HQ.";
if (kind == "HUNTER ORDER") return Subject(message) + " received its radio order. Drive to " + Field(message, "destination") + ".";
@@ -87,8 +88,17 @@ 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 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 RECOVERY TEAM") return "Recovery team assigned. Soldier preparation may still be pending. " + Short(message);
if (kind == "BODY RECOVERY READY") return "Recovery team has soldiers and a working manpack; waiting for its search order. " + Short(message);
if (kind == "BODY RECOVERY STATUS") return "Body recovery waiting: " + Short(message);
if (kind == "BODY RECOVERY END")
{
if (Field(message, "delivered") == "1") return "HQ received the completed body inspection. " + Short(message);
return "Body recovery ended without an inspection report reaching HQ. " + 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 == "ALARM REPORT") return "Squad reporting a heard vehicle alarm 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 == "OBJECTIVE DEFENCE") return "Objective security: " + Short(message);
@@ -13,6 +13,14 @@ class ERR_LocationComponent : ScriptComponent
[RplProp()]
protected int m_Knowledge;
[RplProp(onRplName: "IdentityChanged")] protected bool m_Prepared = true;
bool Prepared() { return m_Prepared; }
void SetPrepared(bool ready)
{
if (!Replication.IsServer()) return;
m_Prepared = ready;
IdentityChanged(); Replication.BumpMe();
}
string Id() { return m_Id; }
string Label() { return m_Label; }
ERR_LocationKind Kind() { return m_Kind; }
@@ -23,7 +31,7 @@ class ERR_LocationComponent : ScriptComponent
{
if (!Replication.IsServer() || id.IsEmpty()) return false;
ERR_GameMode mode = ERR_GameMode.Get();
if (!mode || mode.Phase() != ERR_Phase.PREPARING) return false;
if (!mode || (mode.Phase() != ERR_Phase.PREPARING && !mode.Active())) return false;
mode.UnregisterLocation(this);
m_Id = id;
m_Label = label;
@@ -41,7 +49,7 @@ class ERR_LocationComponent : ScriptComponent
}
bool Discover(int roll, int chance)
{
if (!Replication.IsServer() || m_Knowledge >= 2) return false;
if (!Replication.IsServer() || !m_Prepared || m_Knowledge >= 2) return false;
int previous = m_Knowledge;
m_Knowledge = ERR_MissionRules.Discovery(m_Knowledge, roll, chance);
Replication.BumpMe();
@@ -17,11 +17,15 @@ class ERR_MissionRules
return phase == ERR_Phase.VICTORY || phase == ERR_Phase.DEFEAT;
}
static bool CanTransition(int from, int to)
static bool CanTransition(int from, int to, bool crashStart = false)
{
if (Terminal(from) || from == to) return false;
if (to == ERR_Phase.DEFEAT) return from >= ERR_Phase.PRISON && from <= ERR_Phase.EXTRACTION;
if (from == ERR_Phase.PREPARING) return to == ERR_Phase.PRISON;
if (from == ERR_Phase.PREPARING)
{
if (crashStart) return to == ERR_Phase.RADIO_SEARCH;
return to == ERR_Phase.PRISON;
}
if (from == ERR_Phase.PRISON) return to == ERR_Phase.RADIO_SEARCH;
if (from == ERR_Phase.RADIO_SEARCH) return to == ERR_Phase.RADIO_DEFENCE;
if (from == ERR_Phase.RADIO_DEFENCE) return to == ERR_Phase.EXTRACTION;
@@ -54,14 +54,23 @@ modded class ERR_AdminMenu
foreach (string key : ERR_AdminMenuFields.Keys())
{
ERR_ProbeFieldPage(key);
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("Random_" + key, GetRootWidget());
string prefix = "Random_";
string choice = "Random";
if (key == "viewDistanceMetres") { prefix = "Default_"; choice = "default"; }
else if (!ERR_Settings.Randomizable(key))
{
// Fixed-only settings (counts, start mode) deliberately have no Random button.
if (GetRootWidget().FindAnyWidget("Random_" + key) || !SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget())) return false;
continue;
}
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText(prefix + key, GetRootWidget());
SCR_EditBoxComponent edit = SCR_EditBoxComponent.GetEditBoxComponent(key, GetRootWidget());
bool available = ERR_AdminMenuFields.Available(key, m_HQ, m_Intel);
if (!button || !edit || button.IsEnabled() != available) return false;
if (!available) continue;
string previous = edit.GetValue();
OnButton(button);
if (edit.GetValue() != "Random") return false;
if (edit.GetValue() != choice) return false;
edit.SetValue(previous);
}
ERR_ProbeFieldPage("startTimeMinutes");
@@ -57,6 +57,12 @@ class ERR_NativeAdminSettingsProbe
ERR_Settings settings = new ERR_Settings(path);
foreach (string key : ERR_AdminMenuFields.Keys())
{
if (!ERR_Settings.Randomizable(key))
{
int unsupported;
if (!Check(!ERR_CommandArguments.Selection(key, "random", unsupported) && !settings.Set(key, -1), "fixed-only setting rejects random: " + key)) return false;
continue;
}
int parsed;
if (!Check(ERR_CommandArguments.Selection(key, "random", parsed) && parsed == -1 && settings.Set(key, parsed), "random selection saved: " + key)) return false;
ERR_Settings reload = new ERR_Settings(path);
@@ -87,7 +87,8 @@ modded class ERR_GameMode
float now = GetWorld().GetWorldTime();
if (m_ERRAggressionStage == 0)
{
if (!ERR_AggressionCheck(UnlockPrison(player), "native prison release")) return;
// The shared smoke flow may already have lockpicked the prison; either way the round must be past it.
if (!ERR_AggressionCheck(UnlockPrison(player) || Phase() >= ERR_Phase.RADIO_SEARCH, "native prison release")) return;
player.SetOrigin(ERR_Actors.Ground(Sites().Find("everon-medical-1").Location.Position() + "300 0 0"));
if (!ERR_AggressionCheck(hq.Watch().HunterTarget() == 3, "opening allowance is three without identified bodies")) return;
if (!ERR_AggressionCheck(ERR_HQWatchRules.Aggression(90) == 30 && ERR_HQWatchRules.Hunters(30) == 15 && ERR_HQWatchRules.Hunters(1000) == 15, "maximum 30 aggression / 15 UAZ allowance")) return;
@@ -20,6 +20,7 @@ modded class ERR_GameMode
protected ref ERR_NativeLocalAdmissionProbe m_ERRLocalAdmissionProbe;
protected ref ERR_NativePoweredFlightProbe m_ERRPoweredFlightProbe;
protected ref ERR_NativePrisonBreachProbe m_ERRPrisonBreachProbe;
protected ref ERR_NativeInventoryHitZoneProbe m_ERRInventoryHitZoneProbe;
protected ref ERR_NativeItemStationTeardownProbe m_ERRItemStationTeardownProbe;
protected ref ERR_NativeObjectiveLossProbe m_ERRObjectiveLossProbe;
protected ref ERR_NativeMedicalProbe m_ERRBaseMedicalProbe;
@@ -54,7 +55,7 @@ modded class ERR_GameMode
string enabled;
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
string nativeEndCase;
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"};
array<string> independentFixtures = {"coordinated-ai", "optional-site-skip", "infantry-combat", "medical-combat", "hunter-faction", "unconscious-camera", "objective-map", "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_NativeProgressionIdentityProbe.BeginRound();
@@ -100,6 +101,7 @@ modded class ERR_GameMode
if (m_ERRLocalAdmissionProbe) m_ERRLocalAdmissionProbe.Stop();
if (m_ERRPoweredFlightProbe) m_ERRPoweredFlightProbe.Stop();
if (m_ERRPrisonBreachProbe) m_ERRPrisonBreachProbe.Stop();
if (m_ERRInventoryHitZoneProbe) m_ERRInventoryHitZoneProbe.Stop();
if (m_ERRItemStationTeardownProbe) m_ERRItemStationTeardownProbe.Stop();
if (m_ERRCasualtyServiceProbe) m_ERRCasualtyServiceProbe.Stop();
if (m_ERRObjectiveLossProbe) m_ERRObjectiveLossProbe.Stop();
@@ -172,6 +174,13 @@ modded class ERR_GameMode
{
if (!ERR_MissionRules.Terminal(current)) return;
GetGame().GetCallqueue().Remove(ERR_BaseTick);
if (m_ERRExtractionCase == "inventory-hitzone")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2102, "native inventory callback fixture finished before explicit reset")) return;
ERR_StopHealthProbes();
Print("ERR SMOKE COMPLETE: real one-argument blood event reaches native inventory updater and detached points stay silent; synthetic notification, no injury/manual UI/multiplayer proof");
return;
}
if (m_ERRExtractionCase == "station-teardown")
{
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 2002, "native station teardown fixture finished before explicit reset")) return;
@@ -336,6 +345,13 @@ modded class ERR_GameMode
Print("ERR SMOKE COMPLETE: expected startup-failure");
return;
}
if (m_ERRBaseStep == 2100)
{
int result = m_ERRInventoryHitZoneProbe.Tick(now);
if (result < 0) { ERR_BaseCheck(false, "native inventory hitzone callback fixture"); return; }
if (result == 1) { m_ERRBaseStep = 2102; Transition(ERR_Phase.DEFEAT); }
return;
}
if (m_ERRBaseStep == 2000)
{
int result = m_ERRItemStationTeardownProbe.Tick(now);
@@ -510,6 +526,12 @@ modded class ERR_GameMode
if (!ERR_NativeDiscoveryMapProbe.Run()) { GetGame().GetCallqueue().Remove(ERR_BaseTick); return; }
m_ERRDiscoveryMapChecked = true;
}
if (m_ERRExtractionCase == "inventory-hitzone")
{
m_ERRInventoryHitZoneProbe = new ERR_NativeInventoryHitZoneProbe(this, m_ERRBasePlayer);
m_ERRBaseStep = 2100;
return;
}
if (m_ERRExtractionCase == "station-teardown")
{
m_ERRItemStationTeardownProbe = new ERR_NativeItemStationTeardownProbe();
@@ -45,7 +45,9 @@ class ERR_NativeContractsProbe
mode.Admin().Execute(mode, true, "settings", replies);
if (!Check(replies.Count() == ERR_Settings.Keys().Count(), "base settings listing available")) return false;
mode.Admin().Execute(mode, true, "status", replies);
if (!Check(!replies.IsEmpty() && replies[0] == "Escape the prison", "base mission status available before companions")) return false;
string initialStatus = "Escape the prison";
if (mode.StartMode() == 1) initialStatus = "Locate the radio station";
if (!Check(!replies.IsEmpty() && replies[0] == initialStatus, "base mission status available before companions")) return false;
mode.Admin().Execute(mode, true, "mission radio set " + original.ToString(), replies);
if (!Check(mode.Settings().Get("radioDefenceSeconds") == original, "test setting restored")) return false;
return true;
@@ -0,0 +1,542 @@
#ifdef WORKBENCH
// Explicit local fixture: existing infantry, native movement/perception/projectiles.
// Orders and the prisoner's firing-range placement are arranged inputs. This is
// not proof of HQ radio delivery, multiplayer balance or natural target discovery.
modded class ERR_ScenarioComponent
{
protected override bool ERR_ConfigurePatrolHQFixture(string outcome)
{
if (outcome != "coordinated-ai") return super.ERR_ConfigurePatrolHQFixture(outcome);
if (m_TerrainIndependent || !m_RuntimeDefinition) return false;
// The historical native fixture predates mobile site squads. Populate these
// normal finite responders before the mission starts, never replace a squad
// or create soldiers while an execution is being measured.
array<string> selected = {"everon-outpost-1", "everon-supply-2"};
foreach (string siteId : selected)
{
ERR_SiteDefinition site = m_RuntimeDefinition.Find(siteId);
if (!site || !site.Guarded || site.ResponseCount != 0) return false;
site.ResponseCount = 2; site.ResponseOffset = "-20 0 -20";
}
return m_RuntimeDefinition.Valid();
}
}
modded class ERR_HQ
{
bool ERR_CoordinatedFixtureUnits(array<ref ERR_HQUnit> selected)
{
string outcome;
if (!System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "coordinated-ai" || !m_Mode || m_Mode.Phase() != ERR_Phase.PRISON) return false;
selected.Clear();
array<string> sites = {"everon-supply-2", "everon-outpost-1"};
foreach (string id : sites)
{
ERR_SiteRuntime site = m_Mode.Sites().Find(id);
if (!site || !site.Responders || !site.Responders.Group) { selected.Clear(); return false; }
foreach (ERR_HQUnit unit : m_Units)
{
if (unit.Group != site.Responders.Group || unit.HomeGuard || !unit.Eligible(m_Mode) || !unit.StatusSampler.OwnsRoster(unit.Group)
|| unit.Assignment || unit.PendingTask || unit.Downlink || unit.StatusSampler.InitialStrength() != 2) continue;
selected.Insert(unit);
break;
}
}
if (selected.Count() == 2) return true;
selected.Clear(); return false;
}
}
modded class ERR_HQExecution
{
protected int m_FixtureLastState = -2;
protected int m_FixtureLastShots = -1;
protected bool m_FixtureLastOwned;
protected int m_FixtureRawProjectiles;
override void Shot(IEntity actor, int slot, BaseWeaponComponent weapon)
{
m_FixtureRawProjectiles++;
super.Shot(actor, slot, weapon);
}
int ERR_FixtureActualSuppressShots() { return m_ShotCount; }
void ERR_FixtureSuppressChanges(float now)
{
int state = -1; bool activityOwned;
if (m_Suppress) state = m_Suppress.GetActionState();
if (m_Unit && m_Unit.Utility) activityOwned = m_Suppress && m_Unit.Utility.GetExecutedAction() == m_Suppress;
if (state == m_FixtureLastState && m_ShotCount == m_FixtureLastShots && activityOwned == m_FixtureLastOwned) return;
m_FixtureLastState = state; m_FixtureLastShots = m_ShotCount; m_FixtureLastOwned = activityOwned;
ERR_FixtureSuppressDiagnostic(now);
if (m_Unit && m_Unit.Utility) PrintFormat("ERR COORDINATED NATIVE GROUP ACTION: %1", m_Unit.Utility.GetExecutedAction());
}
void ERR_FixtureSuppressDiagnostic(float now)
{
int state = -1; bool activityOwned;
if (m_Suppress) state = m_Suppress.GetActionState();
if (m_Unit && m_Unit.Utility) activityOwned = m_Suppress && m_Unit.Utility.GetExecutedAction() == m_Suppress;
PrintFormat("ERR COORDINATED SUPPRESSION: activity=%1 state=%2 executingOwned=%3 ammoReady=%4 contactAge=%5 attempted=%6 shots=%7", m_Suppress != null, state, activityOwned, m_AmmoReady, now - m_ContactAt, m_SuppressAttempted, m_ShotCount);
PrintFormat("ERR COORDINATED SUPPRESSION BUDGET: callbacks=%1 attempts=%2 areaAge=%3 support=%4", m_FixtureRawProjectiles, m_SuppressAttempts, now - m_SuppressSeenAt, m_SupportReady);
if (!m_Unit || !m_Unit.StatusSampler) return;
for (int slot = 0; slot < m_Unit.StatusSampler.InitialStrength() && slot < 2; slot++)
{
IEntity actor = m_Unit.StatusSampler.InitialMember(slot);
if (!actor || !m_Unit.StatusSampler.CanAttribute(slot, actor, false)) continue;
AIAgent agent = SCR_AIUtils.GetAIAgent(actor);
if (!agent) continue;
SCR_AIUtilityComponent utility = SCR_AIUtilityComponent.Cast(agent.FindComponent(SCR_AIUtilityComponent));
if (!utility) continue;
SCR_AISuppressBehavior behavior = SCR_AISuppressBehavior.Cast(utility.GetExecutedAction());
bool sameActivity = behavior && behavior.GetGroupActivityContext() == m_Suppress;
PrintFormat("ERR COORDINATED SOLDIER ACTION: slot=%1 behavior=%2 exactActivity=%3", slot, utility.GetExecutedAction(), sameActivity);
}
}
}
modded class SCR_AIGroupUtilityComponent
{
IEntity ERR_FixtureVisualObserver(IEntity subject, float now, IEntity required = null, float observedAfter = -1)
{
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
{
if (!info || !info.m_Perception) continue;
AIAgent agent = AIAgent.Cast(info.GetOwner());
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity()) || (required && agent.GetControlledEntity() != required)) continue;
array<BaseTarget> targets = {}; info.m_Perception.GetTargetsList(targets, ETargetCategory.ENEMY);
foreach (BaseTarget target : targets)
{
ERR_HQVisualContact seen = ERR_HQObservation.Capture(target, now);
if (seen && seen.Entity == subject && seen.ObservedAt >= observedAfter && vector.DistanceXZ(seen.Position, subject.GetOrigin()) <= 5) return agent.GetControlledEntity();
}
}
return null;
}
void ERR_FixturePerceptionDiagnostic()
{
int members; int enemies;
foreach (SCR_AIInfoComponent info : m_aInfoComponents)
{
if (!info || !info.m_Perception) continue;
members++;
array<BaseTarget> targets = {}; info.m_Perception.GetTargetsList(targets, ETargetCategory.ENEMY);
foreach (BaseTarget target : targets)
{
if (!target) continue;
enemies++;
if (enemies > 4) continue;
PrintFormat("ERR COORDINATED PERCEPTION TARGET: age=%1 trace=%2 exposure=%3 distance=%4 position=%5", target.GetTimeSinceSeen(), target.GetTraceFraction(), target.GetExposure(), target.GetDistance(), target.GetLastSeenPosition());
}
}
PrintFormat("ERR COORDINATED PERCEPTION: members=%1 enemyEntries=%2", members, enemies);
}
}
class ERR_NativeCoordinatedAIProbe
{
protected ERR_GameMode m_Mode;
protected IEntity m_Player;
protected DamageManagerComponent m_PlayerDamage;
protected bool m_PlayerDamageEnabled;
protected vector m_PlayerOrigin;
protected ref array<ref ERR_HQUnit> m_Units = {};
protected ref array<ref ERR_HQExecution> m_Executions = {};
protected ref array<vector> m_Origins = {};
protected ref array<vector> m_Destinations = {};
protected int m_Step;
protected int m_ExecutionId;
protected int m_MessageId;
protected float m_Deadline;
protected float m_NextDiagnostic;
protected float m_SupportDeadline;
protected int m_AmmoBefore;
protected int m_AmmoAfter;
protected int m_NativeMoveFailures;
protected bool m_FixOrdered;
protected bool m_ApproachSeen;
protected int m_ApproachArrangements;
protected float m_NextApproach;
protected float m_ArrangementAt = -1;
protected IEntity m_FiringWitness;
protected bool m_Stopped;
protected bool m_BlockedOnly;
protected bool m_ArrivalProved;
protected vector m_SettlePosition;
protected float m_SettleSince;
protected vector m_Unreachable;
void ERR_NativeCoordinatedAIProbe(ERR_GameMode mode, IEntity player)
{
m_Mode = mode; m_Player = player; m_PlayerOrigin = player.GetOrigin();
string blocked;
m_BlockedOnly = System.GetCLIParam("errCoordinatedBlockedOnly", blocked) && blocked == "1";
m_PlayerDamage = DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent));
if (m_PlayerDamage) { m_PlayerDamageEnabled = m_PlayerDamage.IsDamageHandlingEnabled(); m_PlayerDamage.EnableDamageHandling(false); }
m_Deadline = GetGame().GetWorld().GetWorldTime() + 30000;
}
bool BlockedOnly() { return m_BlockedOnly; }
protected bool Check(bool success, string detail)
{
if (success) { Print("ERR SMOKE PASS: coordinated AI " + detail); return true; }
Print("ERR SMOKE FAIL: coordinated AI " + detail, LogLevel.ERROR); return false;
}
protected ERR_HQExecutionReport Report(int slot, float now)
{
if (slot < 0 || slot >= m_Executions.Count()) return null;
return m_Executions[slot].Snapshot(m_Units[slot].Id, ++m_MessageId, now);
}
protected void PlacePrisoner(vector position)
{
ChimeraCharacter character = ChimeraCharacter.Cast(m_Player);
if (!character) return;
vector transform[4]; m_Player.GetWorldTransform(transform); transform[3] = position;
// The character physics controller owns its body transform; entity origin
// alone is not a lasting teleport. This matches the native HQ response proof.
character.Teleport(transform);
}
protected bool Destination(ERR_HQUnit unit, vector origin, float radius, out vector chosen)
{
AIPathfindingComponent path = AIPathfindingComponent.Cast(unit.Group.FindComponent(AIPathfindingComponent));
if (!path) return false;
NavmeshWorldComponent nav = path.GetNavmeshComponent();
if (!nav) return false;
for (int direction = 0; direction < 8; direction++)
{
float bearing = direction * 45 * Math.DEG2RAD;
vector candidate = ERR_Actors.Ground(origin + Vector(Math.Cos(bearing) * radius, 0, Math.Sin(bearing) * radius));
if (candidate[1] < 1 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), candidate)) continue;
if (!nav.IsTileLoaded(candidate)) { nav.LoadTileIn(candidate); continue; }
vector reachable;
if (!path.GetClosestPositionOnNavmesh(candidate, "3 3 3", reachable) || vector.DistanceXZ(origin, reachable) < radius - 5) continue;
if (reachable[1] < 1 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), reachable)) continue;
chosen = ERR_Actors.Ground(reachable); return true;
}
return false;
}
protected bool Order(int slot, vector destination, ERR_HQOperationRole role, float now, float supportUntil = 0, vector supportPosition = "0 0 0")
{
ERR_HQUnit unit = m_Units[slot];
if (slot < m_Executions.Count()) m_Executions[slot].Stop(now);
unit.ReleaseAssignment();
ERR_HQTask task = new ERR_HQTask();
task.OperationId = 9001; task.OperationRevision = 1; task.ExecutionId = ++m_ExecutionId;
task.BeliefId = 9001; task.BeliefRevision = 1; task.HypothesisId = slot + 1;
task.OperationRole = role; task.Role = ERR_HQPursuitRole.SEARCH;
task.Destination = destination; task.QueuedAt = now; task.ExpiresAt = now + 300000;
task.SupportUntil = supportUntil; task.SupportPosition = supportPosition;
task.ExpectedOutcome = "explicit native execution fixture";
unit.Task = task; unit.Assignment = new ERR_HQReport();
ResourceName waypointResource = ERR_Resources.DefendWaypoint();
if (role == ERR_HQOperationRole.FLANK) waypointResource = ERR_Resources.MoveWaypoint();
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(waypointResource, destination));
if (!unit.Waypoint) return false;
unit.Waypoint.SetCompletionRadius(25);
if (role == ERR_HQOperationRole.FLANK) unit.Waypoint.SetCompletionRadius(5);
unit.Group.AddWaypointAt(unit.Waypoint, 0);
ERR_HQExecution execution = new ERR_HQExecution(); execution.Receive(task, now);
if (slot < m_Executions.Count()) m_Executions[slot] = execution;
else m_Executions.Insert(execution);
// Production DeliverOrder samples immediately on receipt, while the current
// native contact is still fresh; the fixture must preserve that timing.
execution.Update(unit, now);
if (role == ERR_HQOperationRole.FIX) execution.ERR_FixtureSuppressChanges(now);
PrintFormat("ERR COORDINATED FIXTURE ORDER: unit=%1 execution=%2 role=%3 origin=%4 destination=%5", unit.Id, task.ExecutionId, role, unit.StatusSampler.InitialMember(0).GetOrigin(), destination);
return true;
}
protected int Rounds(ERR_HQUnit unit)
{
ERR_HQAmmoSnapshot ammo = new ERR_HQAmmoSnapshot(); ammo.Complete = true;
for (int slot = 0; slot < unit.StatusSampler.InitialStrength(); slot++)
{
IEntity actor = unit.StatusSampler.InitialMember(slot);
if (!ERR_Actors.CanFight(actor) || !unit.StatusSampler.CanAttribute(slot, actor, false)) return -1;
ERR_HQAmmoSampler.Inspect(actor, unit.Group, ammo);
}
if (!ammo.Complete || ammo.Members != unit.StatusSampler.InitialStrength()) return -1;
return ammo.SmallArmsRounds + ammo.SmallArmsChambers;
}
protected bool FiringArea(ERR_HQUnit unit, out vector chosen, float radius = 75)
{
vector origin = unit.StatusSampler.InitialMember(0).GetOrigin();
vector basis[4]; unit.StatusSampler.InitialMember(0).GetTransform(basis);
float facing = Math.Atan2(basis[2][2], basis[2][0]);
array<float> angles = {0, -15, 15, -30, 30, -45, 45, -60, 60};
foreach (float angle : angles)
{
float bearing = facing + angle * Math.DEG2RAD;
vector point = ERR_Actors.Ground(origin + Vector(Math.Cos(bearing) * radius, 0, Math.Sin(bearing) * radius));
if (point[1] < 1 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) continue;
// Candidate geometry is not visibility proof. Native perception below must
// separately confirm the real target after every arranged placement.
TraceParam trace = new TraceParam(); trace.Start = origin + "0 0.65 0"; trace.End = point + "0 1.2 0";
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS; trace.Exclude = unit.StatusSampler.InitialMember(0);
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
// Start only where a low-eye continuation is geometrically possible. The
// subsequent retreat still requires a real original native visual witness.
vector continuation = ERR_Actors.Ground(origin + Vector(Math.Cos(bearing) * 50, 0, Math.Sin(bearing) * 50));
if (continuation[1] < 1 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), continuation)) continue;
trace.End = continuation + "0 1.4 0";
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
chosen = point; return true;
}
return false;
}
protected bool RetreatArea(ERR_HQUnit unit, float now, out vector chosen)
{
array<float> ranges = {80, 70, 60, 50, 45};
array<float> angles = {0, -5, 5, -10, 10, -20, 20, -30, 30, -45, 45};
// At most two original observers and55 detached geometric candidates each.
// No geometric success substitutes for actual native reacquisition afterward.
for (int member = 0; member < unit.StatusSampler.InitialStrength() && member < 2; member++)
{
IEntity actor = unit.StatusSampler.InitialMember(member);
if (!actor || !unit.StatusSampler.CanAttribute(member, actor, false)) continue;
IEntity observer = unit.Utility.ERR_FixtureVisualObserver(m_Player, now, actor, m_ArrangementAt + 100);
if (!observer || vector.DistanceXZ(observer.GetOrigin(), m_Player.GetOrigin()) > 20) continue;
vector origin = observer.GetOrigin();
vector away = m_Player.GetOrigin() - origin;
float facing = Math.Atan2(away[2], away[0]);
foreach (float radius : ranges)
{
foreach (float angle : angles)
{
float bearing = facing + angle * Math.DEG2RAD;
vector point = ERR_Actors.Ground(origin + Vector(Math.Cos(bearing) * radius, 0, Math.Sin(bearing) * radius));
if (point[1] < 1 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) continue;
TraceParam trace = new TraceParam(); trace.Start = origin + "0 0.65 0"; trace.End = point + "0 1.4 0";
trace.Flags = TraceFlags.WORLD | TraceFlags.ENTS;
array<IEntity> excluded = {observer, m_Player}; trace.ExcludeArray = excluded;
if (GetGame().GetWorld().TraceMove(trace, null) < 1) continue;
m_FiringWitness = observer; chosen = point; return true;
}
}
}
return false;
}
protected void MoveFailed(int result, IEntity vehicleUsed, bool isWaypointRelated, vector destination)
{
if (m_Step != 5 || vehicleUsed || !isWaypointRelated || vector.DistanceXZ(destination, m_Unreachable) > 50) return;
m_NativeMoveFailures++;
PrintFormat("ERR COORDINATED NATIVE MOVE FAILURE: result=%1 destination=%2 callbacks=%3", result, destination, m_NativeMoveFailures);
}
protected bool BeginBlocked(float now)
{
m_Executions[0].Stop(now); m_Units[0].ReleaseAssignment();
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
m_Unreachable = Vector(low[0] - 250, 0, low[2] - 250);
m_Units[0].Utility.GetOnMoveFailed().Insert(MoveFailed);
// A surviving authored route can still make real progress toward the same
// broad direction. Allow the unmodified task's 300s expiry as the final bound.
m_Step = 5; m_Deadline = now + 310000;
return Check(Order(0, m_Unreachable, ERR_HQOperationRole.BLOCK, now), "explicit unreachable waypoint queued to test native failure handling");
}
protected void Diagnostic(float now)
{
if (now < m_NextDiagnostic) return;
m_NextDiagnostic = now + 10000;
for (int i = 0; i < m_Units.Count(); i++)
{
ERR_HQExecutionReport report = Report(i, now);
if (!report) continue;
PrintFormat("ERR COORDINATED FIXTURE STATUS: stage=%1 unit=%2 state=%3 distance=%4 contact=%5 support=%6 shots=%7 physical=%8", m_Step, report.SourceId, report.State, report.DistanceRemaining, report.HasContact, report.SupportReady, m_Executions[i].ERR_FixtureActualSuppressShots(), report.Position);
PrintFormat("ERR COORDINATED FIXTURE LOD: unit=%1 group=%2 active=%3", report.SourceId, m_Units[i].Group.GetLOD(), m_Units[i].Group.IsAIActivated());
if (m_Step >= 2) m_Executions[i].ERR_FixtureSuppressDiagnostic(now);
if (m_Step == 2 && i == 0)
{
m_Units[i].Utility.ERR_FixturePerceptionDiagnostic();
Faction prisonerFaction = SCR_FactionManager.SGetFaction(m_Player);
bool enemyFaction = prisonerFaction && m_Units[i].Group.GetFaction().IsFactionEnemy(prisonerFaction);
PrintFormat("ERR COORDINATED TARGET: actual=%1 enemyFaction=%2 range=%3", m_Player.GetOrigin(), enemyFaction, vector.DistanceXZ(m_Player.GetOrigin(), report.Position));
}
}
}
int Tick(float now)
{
if (m_Stopped) return -1;
if (now > m_Deadline) { Check(false, "bounded stage deadline " + m_Step.ToString()); return -1; }
if (m_Step == 0)
{
ERR_HQ hq = ERR_HQ.Get(); if (!hq || !hq.ERR_CoordinatedFixtureUnits(m_Units)) return 0;
m_Origins.Clear(); m_Destinations.Clear();
foreach (ERR_HQUnit initial : m_Units)
{
vector origin = initial.StatusSampler.InitialMember(0).GetOrigin(), destination;
if (!Destination(initial, origin, 90, destination)) { m_Units.Clear(); return 0; }
m_Origins.Insert(origin); m_Destinations.Insert(destination);
}
if (!Check(m_Units.Count() == 2 && m_Units[0].Group != m_Units[1].Group, "two original registered infantry squads selected; no soldier spawn or teleport")) return -1;
for (int n = 0; n < 2; n++) if (!Check(Order(n, m_Destinations[n], ERR_HQOperationRole.OBSERVE, now), "native waypoint and production execution received")) return -1;
m_Step = 1; m_Deadline = now + 180000; return 0;
}
for (int index = 0; index < m_Executions.Count(); index++) m_Executions[index].Update(m_Units[index], now);
if (m_Step == 2 && m_FixOrdered) m_Executions[0].ERR_FixtureSuppressChanges(now);
Diagnostic(now);
if (m_Step == 1)
{
bool ready = true;
for (int j = 0; j < 2; j++)
{
ERR_HQExecutionReport observed = Report(j, now);
if (!observed || (observed.State != ERR_HQExecutionState.ARRIVED && observed.State != ERR_HQExecutionState.ACTING && observed.State != ERR_HQExecutionState.COMPLETED)) { ready = false; continue; }
if (vector.DistanceXZ(m_Origins[j], observed.Position) < 45) { Check(false, "arrival requires real movement from original position"); return -1; }
}
if (!ready) return 0;
if (!m_ArrivalProved)
{
if (!Check(true, "both native squads moved over45m and production execution recorded physical arrival")) return -1;
m_ArrivalProved = true;
m_SettlePosition = m_Units[0].StatusSampler.InitialMember(0).GetOrigin(); m_SettleSince = now;
}
if (m_BlockedOnly)
{
m_Executions[1].Stop(now); ERR_HQExecutionReport cancelled = Report(1, now);
if (!Check(cancelled && cancelled.State == ERR_HQExecutionState.SUPERSEDED, "cancellation produces terminal execution report")) return -1;
m_Units[1].ReleaseAssignment();
if (!BeginBlocked(now)) return -1;
return 0;
}
// Physical arrival allows a 40m quorum, while the native hold can still be
// travelling. Arrange the encounter only after its lead actor has stayed
// within a 2m patch for ten seconds; never freeze or teleport the infantry.
vector settled = m_Units[0].StatusSampler.InitialMember(0).GetOrigin();
if (vector.DistanceXZ(settled, m_SettlePosition) > 2) { m_SettlePosition = settled; m_SettleSince = now; }
if (now - m_SettleSince < 10000) return 0;
if (!Check(true, "native hold actor physically settled before arranged target placement")) return -1;
vector target;
if (!Check(FiringArea(m_Units[0], target, 8), "unobstructed arranged close native sighting area found")) return -1;
PlacePrisoner(target); m_ArrangementAt = now;
PrintFormat("ERR COORDINATED TARGET ARRANGEMENT: target=%1 actualAfterMove=%2", target, m_Player.GetOrigin());
m_AmmoBefore = Rounds(m_Units[0]);
if (!Check(m_AmmoBefore > m_Units[0].StatusSampler.InitialStrength() * 20, "actual existing rifle ammunition exceeds protected reserve")) return -1;
m_Step = 2; m_Deadline = now + 120000; m_ApproachArrangements = 1; m_NextApproach = now + 15000; return 0;
}
if (m_Step == 2)
{
if (!m_FixOrdered)
{
// Bounded fixture-only repair for native Defend looking away or changing
// stance. Move only the canonical prisoner; never seed a target or move AI.
if (!m_ApproachSeen && m_ApproachArrangements < 3 && now >= m_NextApproach)
{
vector approach;
m_ApproachArrangements++; m_NextApproach = now + 15000;
if (FiringArea(m_Units[0], approach, 8))
{
PlacePrisoner(approach); m_ArrangementAt = now;
PrintFormat("ERR COORDINATED APPROACH RETRY: arrangement=%1 target=%2 originalDeadline=%3; actual native sighting still required", m_ApproachArrangements, approach, m_Deadline);
}
}
array<ref ERR_HQVisualContact> visible = {}; ERR_HQObservation.Gather(m_Units[0].Utility, now, visible);
foreach (ERR_HQVisualContact seen : visible)
{
if (seen.Entity != m_Player || seen.ObservedAt < m_ArrangementAt + 100
|| vector.DistanceXZ(seen.Position, m_Player.GetOrigin()) > 5) continue;
if (!m_ApproachSeen)
{
vector distant;
if (!Check(RetreatArea(m_Units[0], now, distant), "genuine original observer found; clear retreat extends its actual sight line")) return -1;
PlacePrisoner(distant); m_ArrangementAt = now; m_ApproachSeen = true;
PrintFormat("ERR COORDINATED TARGET ARRANGEMENT: observed close approach followed by arranged target retreat=%1; native perception must reacquire", distant);
return 0;
}
if (!m_FiringWitness || vector.DistanceXZ(seen.Position, m_Player.GetOrigin()) > 5 || vector.DistanceXZ(seen.Position, m_FiringWitness.GetOrigin()) < 40) continue;
if (!Check(Order(0, m_FiringWitness.GetOrigin(), ERR_HQOperationRole.FIX, now, 0, seen.Position), "fix order follows genuine native visual acquisition of the arranged target")) return -1;
m_FixOrdered = true; m_Deadline = now + 90000; break;
}
return 0;
}
ERR_HQExecutionReport support = Report(0, now);
if (!support || !support.SupportReady) return 0;
m_AmmoAfter = Rounds(m_Units[0]);
if (!Check(support.HasContact && m_Executions[0].ERR_FixtureActualSuppressShots() > 0 && m_AmmoAfter >= 0 && m_AmmoAfter < m_AmmoBefore,
"native visual contact, owned suppression activity, real projectile callbacks and finite ammo decrease prove support")) return -1;
vector flankOrigin = m_Units[1].StatusSampler.InitialMember(0).GetOrigin(), bound;
if (!Check(Destination(m_Units[1], flankOrigin, 90, bound), "deliberately overlong90m bound lies on native navigation for lease-expiry test")) return -1;
m_SupportDeadline = support.SupportUntil;
if (!Check(Order(1, bound, ERR_HQOperationRole.FLANK, now, m_SupportDeadline, support.ContactPosition), "overlong expiry-test bound copies actual support lease without refreshing it")) return -1;
m_Step = 3; m_Deadline = now + 30000; return 0;
}
if (m_Step == 3)
{
if (now <= m_SupportDeadline + 2500) return 0;
ERR_HQExecutionReport flank = Report(1, now); array<AIWaypoint> queue = {}; m_Units[1].Group.GetWaypoints(queue);
if (!Check(flank && flank.State == ERR_HQExecutionState.BLOCKED && !queue.Contains(m_Units[1].Waypoint), "expired delivered support stops bound by removing its native waypoint")) return -1;
m_Executions[1].Stop(now); ERR_HQExecutionReport stopped = Report(1, now);
if (!Check(stopped && stopped.State == ERR_HQExecutionState.SUPERSEDED, "cancellation produces terminal execution report")) return -1;
m_Units[1].ReleaseAssignment();
PlacePrisoner(m_PlayerOrigin);
m_Step = 4; m_Deadline = now + 15000; return 0;
}
if (m_Step == 4)
{
ERR_HQExecutionReport lost = Report(0, now);
if (!lost || lost.SupportReady) return 0;
if (lost.HasContact && now <= m_SupportDeadline + 10000) return 0;
if (!Check(true, "support validity expires after target removal; no permanent ready flag")) return -1;
if (!BeginBlocked(now)) return -1;
return 0;
}
if (m_Step == 5)
{
ERR_HQExecutionReport blocked = Report(0, now);
if (!blocked) return 0;
if (blocked.State == ERR_HQExecutionState.ARRIVED || blocked.State == ERR_HQExecutionState.ACTING || blocked.State == ERR_HQExecutionState.COMPLETED)
{ Check(false, "unreachable destination never becomes arrival or searched evidence"); return -1; }
if (blocked.State != ERR_HQExecutionState.FAILED) return 0;
if (!Check(m_NativeMoveFailures > 0 && blocked.SearchedRadius == 0, "native movement failure reaches bounded execution failure without negative-search evidence")) return -1;
PrintFormat("ERR COORDINATED TERMINAL FAILURE: detail=%1 distance=%2 searchedRadius=%3 nativeCallbacks=%4", blocked.Detail, blocked.DistanceRemaining, blocked.SearchedRadius, m_NativeMoveFailures);
return 1;
}
return 0;
}
void Stop()
{
if (m_Stopped) return; m_Stopped = true;
float now = GetGame().GetWorld().GetWorldTime();
for (int slot = 0; slot < m_Executions.Count(); slot++)
{
m_Executions[slot].Stop(now);
if (m_Units[slot] && m_Units[slot].Group) m_Units[slot].ReleaseAssignment();
}
if (!m_Units.IsEmpty() && m_Units[0].Utility) m_Units[0].Utility.GetOnMoveFailed().Remove(MoveFailed);
if (m_Player) PlacePrisoner(m_PlayerOrigin);
if (m_PlayerDamage) m_PlayerDamage.EnableDamageHandling(m_PlayerDamageEnabled);
}
}
modded class ERR_GameMode
{
protected ref ERR_NativeCoordinatedAIProbe m_ERRCoordinatedProbe;
protected float m_ERRCoordinatedDeadline;
protected override void EOnInit(IEntity owner)
{
super.EOnInit(owner);
string enabled, outcome;
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1"
|| !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "coordinated-ai") return;
m_ERRCoordinatedDeadline = System.GetTickCount() + 720000;
OnStop().Insert(ERR_StopCoordinatedProbe);
GetGame().GetCallqueue().CallLater(ERR_CoordinatedProbeTick, 500, true);
}
protected void ERR_StopCoordinatedProbe()
{
GetGame().GetCallqueue().Remove(ERR_CoordinatedProbeTick);
if (m_ERRCoordinatedProbe) m_ERRCoordinatedProbe.Stop();
}
protected void ERR_CoordinatedProbeTick()
{
if (System.GetTickCount() >= m_ERRCoordinatedDeadline)
{ Print("ERR SMOKE FAIL: coordinated AI fixture startup or overall deadline", LogLevel.ERROR); ERR_StopCoordinatedProbe(); return; }
if (Phase() != ERR_Phase.PRISON || !GetGame().GetPlayerController()) return;
IEntity player = GetGame().GetPlayerController().GetControlledEntity(); if (!player) return;
if (!m_ERRCoordinatedProbe) m_ERRCoordinatedProbe = new ERR_NativeCoordinatedAIProbe(this, player);
int result = m_ERRCoordinatedProbe.Tick(GetWorld().GetWorldTime());
if (result == 0) return;
ERR_StopCoordinatedProbe();
if (result < 0) return;
Transition(ERR_Phase.DEFEAT);
if (m_ERRCoordinatedProbe.BlockedOnly())
Print("ERR SMOKE COMPLETE: coordinated native movement, physical arrival, cancellation and native blocked outcome only; suppression, multiplayer and HQ radio not qualified by this explicit variant");
else
Print("ERR SMOKE COMPLETE: coordinated native movement, physical arrival, projectile-proved support, support expiry and native blocked outcome; arranged orders, not multiplayer or HQ radio qualification");
}
}
#endif
@@ -1,4 +1,21 @@
#ifdef WORKBENCH
class ERR_ProbeAutomaticGrant : ERR_AutomaticEquipmentGrant
{
ResourceName Prefab;
bool Weapon;
override protected ERR_AutomaticEquipmentItem Choose(ERR_GameMode mode)
{
ERR_AutomaticEquipmentItem choice = new ERR_AutomaticEquipmentItem(); choice.Prefab = Prefab; choice.Weapon = Weapon; return choice;
}
int MagazinesRequested() { return m_Magazines; }
bool Carried(IEntity actor)
{
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
return m_Issued && inventory && inventory.Contains(m_Issued);
}
bool AmmunitionComplete() { return m_Magazines == 2 && m_Operation && m_Operation.Finished && m_Operation.Succeeded; }
}
// Real native inventory fixture; deliberately not registered with the ordinary equipment service.
class ERR_ProbeBrokenLauncherGrant : ERR_EnemyLauncherGrant
{
@@ -9,6 +26,8 @@ class ERR_ProbeBrokenLauncherGrant : ERR_EnemyLauncherGrant
}
modded class ERR_GameMode
{
protected ref ERR_ProbeAutomaticGrant m_ERRAutoGun = new ERR_ProbeAutomaticGrant();
protected ref ERR_ProbeAutomaticGrant m_ERRAutoItem = new ERR_ProbeAutomaticGrant();
protected int m_ERRLoadoutDeadline;
protected int m_ERRLoadoutStage;
protected SCR_AIGroup m_ERRLoadoutGroup;
@@ -49,12 +68,28 @@ modded class ERR_GameMode
m_ERRLoadoutGroup.m_aUnitPrefabSlots = {
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et",
"{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et"};
m_ERRLoadoutGroup.RequestSpawn(2); m_ERRLoadoutStage = 1; return;
m_ERRLoadoutGroup.m_aUnitPrefabSlots.Insert("{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et");
m_ERRLoadoutGroup.m_aUnitPrefabSlots.Insert("{5436629450D8387A}Prefabs/Characters/Factions/OPFOR/USSR_Army/Character_USSR_SL.et");
if (!ERR_LoadoutCheck(Settings().Set("automaticEquipmentChancePercent", 100) && ERR_AutomaticEquipmentCatalog.Count(this) > 0, "automatic equipment catalog and setting")) return;
// Choose a real discovered mod rifle, not a hardcoded Escape integration entry.
for (int candidate = 0; candidate < 512; candidate++)
{
ERR_AutomaticEquipmentItem discovered = ERR_AutomaticEquipmentCatalog.Pick(this);
if (!discovered || !discovered.Weapon || (!discovered.Prefab.Contains("/AK74M/") && !discovered.Prefab.Contains("/M4A1/"))) continue;
m_ERRAutoGun.Prefab = discovered.Prefab; m_ERRAutoGun.Weapon = true; break;
}
if (!ERR_LoadoutCheck(!m_ERRAutoGun.Prefab.IsEmpty(), "discovered mod rifle=" + m_ERRAutoGun.Prefab)) return;
m_ERRAutoItem.Prefab = ERR_EnemyMedicalCatalog.MORPHINE;
for (int sample = 0; sample < 12; sample++) Print("ERR AUTO CATALOG SAMPLE: " + ERR_AutomaticEquipmentCatalog.Pick(this).Prefab);
m_ERRLoadoutGroup.RequestSpawn(4); m_ERRLoadoutStage = 1; return;
}
array<AIAgent> agents = {}; m_ERRLoadoutGroup.GetAgents(agents);
if (agents.Count() != 2 || !agents[0].GetControlledEntity() || !agents[1].GetControlledEntity()) return;
if (agents.Count() != 4) return;
foreach (AIAgent ready : agents) if (!ready.GetControlledEntity()) return;
IEntity first = agents[0].GetControlledEntity(); IEntity second = agents[1].GetControlledEntity();
foreach (AIAgent agent : agents) agent.DeactivateAI();
m_ERRAutoGun.Check(this, m_ERRLoadoutGroup, agents[2].GetControlledEntity());
m_ERRAutoItem.Check(this, m_ERRLoadoutGroup, agents[3].GetControlledEntity());
if (m_ERRLoadoutStage == 1)
{
m_ERRNativeAA.Check(this, m_ERRLoadoutGroup, first);
@@ -77,6 +112,8 @@ modded class ERR_GameMode
Print("ERR ADMIN CAPTURE PAGE: 1"); m_ERRLoadoutStage = 3; return;
}
if (m_ERRLoadoutStage++ < 8) return;
if (!m_ERRAutoGun.Done() || !m_ERRAutoItem.Done()) return;
if (!ERR_LoadoutCheck(m_ERRAutoGun.Carried(agents[2].GetControlledEntity()) && m_ERRAutoGun.AmmunitionComplete() && m_ERRAutoItem.Carried(agents[3].GetControlledEntity()), "automatic catalog item and spare-slot rifle still carried; both matching magazine insertions completed")) return;
System.MakeScreenshot("$logs:err-enemy-loadout-menu");
GetGame().GetCallqueue().Remove(ERR_LoadoutTick);
Print("ERR SMOKE COMPLETE: enemy-loadout physical preferred/fallback launchers, AP mines and HQ menu");
@@ -62,6 +62,31 @@ class ERR_NativeEnvironmentProbe
m_OwnsSettings = false;
}
}
protected bool ViewDistanceChecks()
{
if (!Check(m_TestSettings.Get("viewDistanceMetres") == 0, "legacy file defaults view distance without an override")) return false;
if (!Check(m_TestSettings.Set("viewDistanceMetres", 3000), "view distance saves in isolated settings")) return false;
ERR_Settings reloaded = new ERR_Settings("$profile:erri-native-environment-probe.json");
if (!Check(reloaded.Get("viewDistanceMetres") == 3000 && !m_TestSettings.Set("viewDistanceMetres", 499) && !m_TestSettings.Set("viewDistanceMetres", 10001), "view distance reload and invalid bounds preserve saved value")) return false;
array<string> replies = {};
int previous = m_Mode.Settings().Get("viewDistanceMetres");
m_Mode.Admin().Execute(m_Mode, false, "mission viewdistance set 4000", replies);
if (!Check(m_Mode.Settings().Get("viewDistanceMetres") == previous && replies.Count() == 1 && replies[0].Contains("Administrator"), "view distance requires server-authorized admin")) return false;
float original = GetGame().GetViewDistance();
ERR_ViewDistance local = new ERR_ViewDistance();
local.Apply(0);
if (!Check(GetGame().GetViewDistance() == original, "default leaves native camera distance unchanged")) return false;
float expected = ERR_ViewDistance.LocalDistance(3000, GetGame().GetMinimumViewDistance(), GetGame().GetMaximumViewDistance(), GetGame().GetViewDistanceServerLimit());
local.Apply(3000);
float actual = GetGame().GetViewDistance();
local.Apply(0);
bool restored = Math.AbsFloat(GetGame().GetViewDistance() - original) <= 0.5;
PrintFormat("ERR VIEW DISTANCE: original=%1 requested=3000 expected=%2 actual=%3 restored=%4", original, expected, actual, restored);
if (!Check(Math.AbsFloat(actual - expected) <= 0.5 && restored, "native camera applies bounded mission distance and restores original")) return false;
local.Apply(10000);
local.Restore();
return Check(Math.AbsFloat(GetGame().GetViewDistance() - original) <= 0.5, "teardown releases temporary camera distance");
}
protected bool SettingsChecks()
{
string path = "$profile:erri-native-environment-probe.json";
@@ -70,7 +95,7 @@ class ERR_NativeEnvironmentProbe
JsonSaveContext json = new JsonSaveContext();
json.EnableTypeDiscriminator(false);
foreach (string key : ERR_Settings.Keys())
if (key != "startDateCode" && key != "weatherProfile") json.WriteValue(key, m_Mode.Settings().Get(key));
if (key != "startDateCode" && key != "weatherProfile" && key != "viewDistanceMetres") json.WriteValue(key, m_Mode.Settings().Get(key));
if (!Check(json.SaveToFile(path), "owned legacy settings fixture written")) return false;
m_TestSettings = new ERR_Settings(path);
JsonLoadContext unchanged = new JsonLoadContext();
@@ -112,7 +137,7 @@ class ERR_NativeEnvironmentProbe
m_Clock = m_Manager.GetTimeOfTheDay(); m_DayDuration = m_Manager.GetDayDuration(); m_DayAdvances = m_Manager.GetIsDayAutoAdvanced();
m_Snapshot = true;
m_Deadline = now + 15000;
if (!SettingsChecks()) return -1;
if (!SettingsChecks() || !ViewDistanceChecks()) return -1;
if (!Check(CurrentWeather() == m_Weather && m_Manager.GetYear() == m_Year && m_Manager.GetMonth() == m_Month && m_Manager.GetDay() == m_Day, "saving fixture environment settings does not mutate native world")) return -1;
for (int profile = 1; profile <= 3; profile++)
if (!Check(ERR_EnvironmentService.HasWeatherState(m_Manager, ERR_EnvironmentService.WeatherStateName(profile)), "selected vanilla weather family exists: " + ERR_EnvironmentService.ProfileName(profile))) return -1;
@@ -64,12 +64,14 @@ modded class ERR_GameMode
if (!controller || !controller.GetControlledEntity()) return;
if (m_ERRGeneratedStage == 0)
{
if (!ERR_GeneratedCheck(ERR_HQ.Get() && ERR_HQ.Get().Roadblocks().ERR_ProbePreparedRoadblocks(), "native roadblocks: four guards per post, finite ledger and available HQ squads")) return;
m_ERRGeneratedPlayer = controller.GetControlledEntity();
if (ERR_GeneratedTeamSlots(m_ERRGeneratedPlayer) != 1) return;
ERR_SiteRuntime prison = Sites().Find("everon-prison-1"); ERR_SiteRuntime radio = Sites().Find("everon-radio-1");
if (!ERR_GeneratedCheck(prison && radio && prison.Definition.PlacementResolved && radio.Definition.PlacementResolved, "both generated locations committed before admission")) return;
if (!ERR_GeneratedCheck(vector.DistanceXZ(prison.Definition.Position, radio.Definition.Position) >= 1000 && vector.DistanceXZ(prison.Definition.Position, "6090 0 7160") > 1, "fresh island prison and separated radio")) return;
vector local = ERR_CampTerrain.Local(m_ERRGeneratedPlayer.GetOrigin() - PrisonPosition(), PrisonHeading());
PrintFormat("ERR SMOKE DETAIL: generated prison local=%1 heading=%2 gateRegistered=%3 player=%4 prison=%5", local, PrisonHeading(), PrisonEscape().Registered(), m_ERRGeneratedPlayer.GetOrigin(), PrisonPosition());
if (!ERR_GeneratedCheck(Math.AbsFloat(local[0]) <= 5.5 && Math.AbsFloat(local[2]) <= 5.5 && PrisonEscape().Registered(), "player inside rotated prison with intact registered gate")) return;
BaseInventoryStorageComponent bag = BaseInventoryStorageComponent.Cast(prison.Container.FindComponent(BaseInventoryStorageComponent));
array<IEntity> contents = {}; if (bag) bag.GetAll(contents);
@@ -0,0 +1,166 @@
#ifdef WORKBENCH
// Read-only fixture access to native UI identities. No replacement widgets.
modded class SCR_InventoryMenuUI
{
bool ERR_HitZoneProbeReady(IEntity actor)
{
return ERR_NativeInventoryHitZoneProbe.Enabled() && m_Player == actor && m_pStorageListUI
&& m_pStorageListUI.GetStorage() && m_PlayerRenderAttributes && m_wAttachmentPointsContainer;
}
bool ERR_HitZoneProbePoints(out array<ref SCR_InventoryHitZonePointContainerUI> points)
{
points.Clear();
if (!ERR_NativeInventoryHitZoneProbe.Enabled() || m_aHitZonePoints.Count() != 7) return false;
foreach (SCR_InventoryHitZonePointContainerUI point : m_aHitZonePoints) points.Insert(point);
return true;
}
}
modded class SCR_InventoryHitZonePointContainerUI
{
protected SCR_HitZone m_ERRProbeBlood;
protected int m_ERRProbeUpdates;
protected bool m_ERRProbeFalseRefresh;
protected override void UpdateHitZoneState(SCR_HitZone hz, bool updateAttachmentStorage = true)
{
super.UpdateHitZoneState(hz, updateAttachmentStorage);
if (m_ERRProbeBlood && hz == m_ERRProbeBlood)
{
m_ERRProbeUpdates++;
if (!updateAttachmentStorage) m_ERRProbeFalseRefresh = true;
}
}
bool ERR_HitZoneProbeWatch(ERR_GameMode mode, SCR_HitZone blood)
{
if (!ERR_NativeInventoryHitZoneProbe.Enabled() || !mode || mode != m_ERRHitZoneMode
|| !blood || !m_ERRHitZoneSources.Contains(blood)) return false;
m_ERRProbeBlood = blood; m_ERRProbeUpdates = 0; m_ERRProbeFalseRefresh = false;
return true;
}
bool ERR_HitZoneProbeOnce() { return m_ERRProbeUpdates == 1 && !m_ERRProbeFalseRefresh; }
bool ERR_HitZoneProbeDetached() { return !m_ERRHitZoneMode && m_ERRHitZoneSources.IsEmpty(); }
void ERR_HitZoneProbeStop() { m_ERRProbeBlood = null; }
}
// Emits one real one-argument damage-state notification without changing health.
// It proves callback/native UI integration, not a natural injury or manual UI use.
class ERR_NativeInventoryHitZoneProbe
{
protected ERR_GameMode m_Mode;
protected SCR_PlayerController m_Controller;
protected IEntity m_Actor;
protected SCR_InventoryStorageManagerComponent m_Inventory;
protected SCR_CharacterBloodHitZone m_Blood;
protected SCR_InventoryMenuUI m_Menu;
protected ref array<ref SCR_InventoryHitZonePointContainerUI> m_Points = {};
protected int m_Deadline;
protected int m_Stage;
protected int m_Result;
protected bool m_Stopped;
protected bool m_CloseRequested;
protected float m_Health;
static bool Enabled()
{
string enabled, outcome;
return Replication.IsServer() && System.GetCLIParam("errBaseSmoke", enabled) && enabled == "1"
&& System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "inventory-hitzone";
}
void ERR_NativeInventoryHitZoneProbe(ERR_GameMode mode, IEntity actor)
{
m_Mode = mode; m_Actor = actor; m_Deadline = System.GetTickCount() + 30000;
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
}
protected bool Check(bool condition, string detail)
{
if (condition) { Print("ERR SMOKE PASS: inventory hitzone " + detail); return true; }
Print("ERR SMOKE FAIL: inventory hitzone " + detail, LogLevel.ERROR);
m_Result = -1; Stop(); return false;
}
protected bool Context()
{
return Enabled() && m_Mode && m_Mode == ERR_GameMode.Get() && m_Mode.Active()
&& m_Controller && m_Controller == GetGame().GetPlayerController() && m_Controller.GetControlledEntity() == m_Actor
&& m_Mode.Players() && m_Mode.Players().CurrentActor(m_Actor) && ERR_Actors.CanFight(m_Actor);
}
protected bool OwnsMenu()
{
return m_Menu && GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.Inventory20Menu) == m_Menu;
}
protected bool UnchangedHealth() { return m_Blood && m_Blood.GetHealth() == m_Health; }
int Tick(float now)
{
if (m_Result != 0) return m_Result;
if (m_Stopped) return -1;
if (!Context()) { Check(false, "original conscious local round body changed"); return -1; }
if (System.GetTickCount() >= m_Deadline) { Check(false, "fixed 30-second UI/callback deadline"); return -1; }
if (m_Stage == 0)
{
if (!Check(!GetGame().GetMenuManager().GetTopMenu(), "no existing menu adopted")) return -1;
m_Inventory = SCR_InventoryStorageManagerComponent.Cast(m_Actor.FindComponent(SCR_InventoryStorageManagerComponent));
SCR_CharacterDamageManagerComponent damage = SCR_CharacterDamageManagerComponent.Cast(m_Actor.FindComponent(SCR_CharacterDamageManagerComponent));
if (damage) m_Blood = damage.GetBloodHitZone();
if (!Check(m_Inventory && m_Blood, "original native inventory and blood event source present")) return -1;
m_Health = m_Blood.GetHealth();
m_Inventory.OpenInventory();
m_Menu = SCR_InventoryMenuUI.Cast(GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.Inventory20Menu));
if (!Check(OwnsMenu(), "ordinary native inventory opens without input automation")) return -1;
m_Stage = 1; return 0;
}
if (m_Stage == 1)
{
if (!Check(OwnsMenu() && UnchangedHealth(), "owned inventory and original blood health retained")) return -1;
if (!m_Menu.ERR_HitZoneProbeReady(m_Actor)) return 0;
m_Menu.InspectItem(null); // Native character inspection creates its normal seven containers over several frames.
m_Stage = 2; return 0;
}
if (m_Stage == 2)
{
if (!Check(OwnsMenu() && UnchangedHealth(), "native inspection retains original menu and health")) return -1;
if (!m_Menu.ERR_HitZoneProbePoints(m_Points)) return 0;
array<SCR_InventoryHitZonePointContainerUI> seen = {};
foreach (SCR_InventoryHitZonePointContainerUI point : m_Points)
{
if (!Check(point && !seen.Contains(point) && point.ERR_HitZoneProbeWatch(m_Mode, m_Blood), "unique native point retains original blood subscription")) return -1;
seen.Insert(point);
}
if (!Check(UnchangedHealth() && m_Blood.GetOnDamageStateChanged(false), "original blood event and health retained before notification")) return -1;
m_Blood.GetOnDamageStateChanged(false).Invoke(m_Blood);
foreach (SCR_InventoryHitZonePointContainerUI updated : m_Points)
if (!Check(updated.ERR_HitZoneProbeOnce(), "one-argument event reached native updater once with explicit true")) return -1;
if (!Check(UnchangedHealth() && OwnsMenu(), "notification changes no blood health or menu ownership")) return -1;
bool closeAccepted = GetGame().GetMenuManager().CloseMenu(m_Menu);
m_CloseRequested = closeAccepted;
if (!Check(closeAccepted, "exact owned inventory close request accepted")) return -1;
m_Stage = 3; return 0;
}
if (m_Stage == 3)
{
MenuBase current = GetGame().GetMenuManager().FindMenuByPreset(ChimeraMenuPreset.Inventory20Menu);
if (current == m_Menu && m_Menu) return 0;
if (!Check(!current, "native inventory closes without adopting a replacement")) return -1;
foreach (SCR_InventoryHitZonePointContainerUI detached : m_Points)
if (!Check(detached && detached.ERR_HitZoneProbeDetached(), "native detach cleared original point subscriptions")) return -1;
if (!Check(UnchangedHealth() && m_Blood.GetOnDamageStateChanged(false), "original blood event and health retained before notification")) return -1;
m_Blood.GetOnDamageStateChanged(false).Invoke(m_Blood);
foreach (SCR_InventoryHitZonePointContainerUI oldPoint : m_Points)
if (!Check(oldPoint.ERR_HitZoneProbeOnce(), "closed original point receives no late native update")) return -1;
if (!Check(UnchangedHealth(), "blood health unchanged through complete callback proof")) return -1;
m_Menu = null; m_Result = 1; Stop(); return 1;
}
Check(false, "unexpected fixture stage"); return -1;
}
void Stop()
{
if (m_Stopped) return;
m_Stopped = true;
foreach (SCR_InventoryHitZonePointContainerUI point : m_Points) if (point) point.ERR_HitZoneProbeStop();
m_Points.Clear();
if (GetGame() && GetGame().GetMenuManager() && !m_CloseRequested && OwnsMenu())
{
m_CloseRequested = true;
GetGame().GetMenuManager().CloseMenu(m_Menu);
}
m_Menu = null; m_Blood = null; m_Inventory = null;
}
}
#endif
@@ -111,19 +111,20 @@ modded class ERR_GameMode
{ 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;
int epi, standardEpi, 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.EPI) standardEpi++;
if (prefab == ERR_EnemyMedicalCatalog.COMBAT_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 (epi < 6 || standardEpi < 2 || morphine < 6) return;
if (!ERR_MedicalCombatCheck(bag && saline >= 4 && dressings >= 4, "medic carries native medical bag, at least four saline/bandages, six morphine, six combat epinephrine and two standard epinephrine")) return;
if (m_ERRHunterFactionFixture)
{
if (!ERR_MedicalCombatCheck(hunter.Group.GetFaction() == EnemyFaction() && EnemyFaction().GetFactionKey() == "RHS_ION", "response crew belongs specifically to RHS ION")) return;
@@ -15,6 +15,7 @@ class ERR_NativeMedicalProbe
protected SCR_CharacterDamageManagerComponent m_Damage;
protected CharacterControllerComponent m_Controller;
protected SCR_CharacterBloodHitZone m_Blood;
protected SCR_CharacterHitZone m_Wound;
protected SCR_SalineStorageComponent m_SalineStorage;
protected SCR_InventoryStorageManagerComponent m_Inventory;
protected ref array<IEntity> m_Owned = {};
@@ -67,6 +68,7 @@ class ERR_NativeMedicalProbe
{
if (!m_Blood || !m_Controller || !m_Damage) return;
PrintFormat("ERR MEDICAL STATE: stage=%1 blood=%2 bloodState=%3 unconscious=%4 bleeding=%5 salineEffects=%6 attachedBags=%7", stage, m_Blood.GetHealth(), m_Blood.GetDamageState(), m_Controller.IsUnconscious(), m_Damage.IsBleeding(), SalineEffects(), AttachedBags());
PrintFormat("ERR MEDICAL ACTORS: casualtyAlive=%1 healerAlive=%2 bodyHealth=%3", ERR_Actors.Alive(m_Casualty), ERR_Actors.Alive(m_Healer), m_Damage.GetDefaultHitZone().GetHealthScaled());
}
void Stop()
{
@@ -83,6 +85,7 @@ class ERR_NativeMedicalProbe
m_Damage = null;
m_Controller = null;
m_Blood = null;
m_Wound = null;
m_SalineStorage = null;
m_Inventory = null;
}
@@ -103,8 +106,8 @@ class ERR_NativeMedicalProbe
if (now < m_Wait) return 0;
if (m_Step == 0)
{
// Keep fixture actors separate from the real prisoner spawn.
vector anchor = m_Mode.PrisonPosition() + "100 0 100";
// Treatment timing needs isolation from the response teams around prison.
vector anchor = m_Mode.PrisonPosition() + "-2000 0 -2000";
m_Casualty = Spawn(m_Mode.Factions().PrisonerPrefab, anchor);
m_Healer = Spawn(m_Mode.Factions().PrisonerPrefab, anchor + "3 0 0");
m_Dressing = Spawn("{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et", anchor + "4 0 0");
@@ -112,6 +115,7 @@ class ERR_NativeMedicalProbe
m_SpareSaline = Spawn("{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et", anchor + "6 0 0");
if (!Check(m_Casualty && m_Healer && m_Dressing && m_Saline && m_SpareSaline, "two isolated characters and finite stock-type medicine spawned")) return -1;
if (!Check(!ERR_Actors.Player(m_Casualty) && !ERR_Actors.Player(m_Healer), "neither medical actor is a real player")) return -1;
DamageManagerComponent.Cast(m_Healer.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
m_Deadline = now + 240000;
m_Wait = now + 1000;
m_Step = 1;
@@ -149,8 +153,29 @@ class ERR_NativeMedicalProbe
if (candidate && candidate.GetMaxBleedingRate() > 0) { wound = candidate; break; }
}
if (!Check(wound != null, "native left-arm hit zone can bleed")) return -1;
wound.SetHealthScaled(0.5);
m_Damage.AddBleedingEffectOnHitZone(wound);
m_Wound = wound;
// Enter through real bullet damage: directly adding a bleed cannot catch
// casualty protection accidentally suppressing native wound consequences.
vector hit[3];
SCR_DamageContext scratch = new SCR_DamageContext(EDamageType.KINETIC, 1, hit, m_Casualty, wound, Instigator.CreateInstigator(m_Healer), null, -1, -1);
scratch.damageEffect = new SCR_BaseBulletDamageEffect();
m_Damage.HandleDamage(scratch);
if (!Check(!m_Damage.IsBleeding(), "sub-threshold bullet damage does not force bleeding")) return -1;
SCR_DamageContext injury = new SCR_DamageContext(EDamageType.KINETIC, wound.GetMaxHealth() * 0.75, hit, m_Casualty, wound, Instigator.CreateInstigator(m_Healer), null, -1, -1);
// Account for the chosen prefab's native hit-zone damage multipliers. The
// remaining health must be below its bleed threshold without lethal trauma.
float low, high = 1000;
for (int sample = 0; sample < 20; sample++)
{
injury.damageValue = (low + high) * 0.5;
if (wound.ERR_ComputeWoundDamage(injury) < wound.GetMaxHealth() * 0.75) low = injury.damageValue;
else high = injury.damageValue;
}
injury.damageValue = high;
injury.damageEffect = new SCR_BaseBulletDamageEffect();
m_Damage.HandleDamage(injury);
PrintFormat("ERR MEDICAL WOUND: health=%1 critical=%2 alive=%3 bleeding=%4", wound.GetHealthScaled(), wound.GetCriticalHealthThreshold(), ERR_Actors.Alive(m_Casualty), m_Damage.IsBleeding());
if (!Check(ERR_Actors.Alive(m_Casualty) && wound.GetHealth() < wound.GetCriticalHealthThreshold() && m_Damage.IsBleeding(), "bullet wound starts native bleeding even when remaining health is below the wound threshold")) return -1;
float threshold = m_Blood.GetDamageStateThreshold(ECharacterBloodState.UNCONSCIOUS) * m_Blood.GetMaxHealth();
float destroyed = m_Blood.GetDamageStateThreshold(ECharacterBloodState.DESTROYED) * m_Blood.GetMaxHealth();
if (!Check(threshold - destroyed > 200, "native blood thresholds permit a survivable recovery fixture")) return -1;
@@ -250,6 +275,13 @@ class ERR_NativeMedicalProbe
{
if (!Check(!m_Epinephrine && m_Damage.GetResilienceHitZone().GetHealth() > m_BeforeEpi,
"epinephrine is consumed and resilience recovers over time")) return -1;
// Extreme trauma is separate from the moderate-wound recovery case above.
// It also checks re-injuring the same previously bandaged body part.
vector hit[3];
SCR_DamageContext injury = new SCR_DamageContext(EDamageType.KINETIC, 1000, hit, m_Casualty, m_Wound, Instigator.CreateInstigator(m_Healer), null, -1, -1);
injury.damageEffect = new SCR_BaseBulletDamageEffect();
m_Damage.HandleDamage(injury);
if (!Check(ERR_Actors.Alive(m_Casualty) && m_Wound.GetHealthScaled() < 0.1 && m_Damage.IsBleeding(), "otherwise fatal bullet reopens a bandaged wound while preserving the same living body")) return -1;
m_Damage.GetBloodHitZone().HandleDamage(100000, EDamageType.TRUE, null);
m_Wait = now + 1000; m_Step = 9; return 0;
}

Some files were not shown because too many files have changed in this diff Show More