Improve AI garrisons, player recovery and mission administration
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Escapists Reintroduced
|
# Co10 Escape Reintroduced
|
||||||
|
|
||||||
Author: **Jakub Jim Žáček**.
|
Author: **Jakub Jim Žáček**.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ Later ordinary input checks demonstrate walking, clean inventory interaction and
|
|||||||
|
|
||||||
## Workbench project and scenario
|
## Workbench project and scenario
|
||||||
|
|
||||||
Open **`addons/core/addon.gproj`**, titled **Escapists Reintroduced**. This is the
|
Open **`addons/core/addon.gproj`**, titled **Co10 Escape Reintroduced**. This is the
|
||||||
single project to develop, package and eventually publish as one Workshop mod.
|
single project to develop, package and eventually publish as one Workshop mod.
|
||||||
Use **`Missions/ERR_Everon.conf`** (`{C1E20667DCE324ED}`) for the full scenario.
|
Use **`Missions/ERR_Everon.conf`** (`{C1E20667DCE324ED}`) for the full scenario.
|
||||||
The Core directory and addon GUID `2528466DE220D91C` are retained for continuity.
|
The Core directory and addon GUID `2528466DE220D91C` are retained for continuity.
|
||||||
@@ -57,3 +57,7 @@ For bounded hidden, muted local native tests, see [runtime testing](docs/runtime
|
|||||||
These scripted local fixtures manipulate positions and selected native states. They do not establish manual escape/piloting, real remote reconnects, twelve connected players or improved FPS. Separate regular-game runs establish standalone startup/admission; an isolated native Linux run establishes empty-server preparation, with inherited resource errors reproduced by a vanilla control. [Current status](docs/current-status.md) separates those gaps from demonstrated development behavior. [Validation evidence](docs/validation.md) preserves exact source checkpoints and failed iterations; `docs/acceptance.json` keeps unverified release requirements pending.
|
These scripted local fixtures manipulate positions and selected native states. They do not establish manual escape/piloting, real remote reconnects, twelve connected players or improved FPS. Separate regular-game runs establish standalone startup/admission; an isolated native Linux run establishes empty-server preparation, with inherited resource errors reproduced by a vanilla control. [Current status](docs/current-status.md) separates those gaps from demonstrated development behavior. [Validation evidence](docs/validation.md) preserves exact source checkpoints and failed iterations; `docs/acceptance.json` keeps unverified release requirements pending.
|
||||||
|
|
||||||
The [development source bundle tool](docs/development-source-bundle.md) previews a deterministic handoff of the single editable project and writes only an explicitly requested new ZIP. It is **DEVELOPMENT SOURCE BUNDLE**, not a native `.pak` or qualified release; release-mode requests remain blocked by pending acceptance. No release package has been published.
|
The [development source bundle tool](docs/development-source-bundle.md) previews a deterministic handoff of the single editable project and writes only an explicitly requested new ZIP. It is **DEVELOPMENT SOURCE BUNDLE**, not a native `.pak` or qualified release; release-mode requests remain blocked by pending acceptance. No release package has been published.
|
||||||
|
|
||||||
|
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).
|
||||||
|
|||||||
@@ -71,51 +71,61 @@ ERR_ScenarioDefinition {
|
|||||||
}
|
}
|
||||||
ERR_SiteDefinition {
|
ERR_SiteDefinition {
|
||||||
Id "everon-medical-1"
|
Id "everon-medical-1"
|
||||||
Label "Medical stash"
|
Label "Field hospital 1"
|
||||||
Kind 4
|
Kind 6
|
||||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
Position 6340 0 7400
|
Position 6340 0 7400
|
||||||
Guarded 0
|
RandomClear 1
|
||||||
GuardOffset 16 0 0
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
DiscoverRadius 25
|
ContainerOffset 6 0 -3
|
||||||
|
DiscoverRadius 60
|
||||||
RevealOnEscape 1
|
RevealOnEscape 1
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{F8427C3EFC52644E}Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et" Offset 0 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 6 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -6 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 6 0 -7 }
|
||||||
|
}
|
||||||
Stock {
|
Stock {
|
||||||
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||||
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 6 }
|
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 8 }
|
||||||
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||||
ERR_SiteStock { Prefab "{13772C903CB5E4F7}Prefabs/Items/Equipment/Maps/Map_Paper_01/PaperMap_01_folded.et" Count 3 }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ERR_SiteDefinition {
|
ERR_SiteDefinition {
|
||||||
Id "everon-outpost-1"
|
Id "everon-outpost-1"
|
||||||
Label "Eastern guard outpost"
|
Label "Field camp 1"
|
||||||
Kind 3
|
Kind 3
|
||||||
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
Position 6740 0 7300
|
Position 6740 0 7300
|
||||||
|
RandomClear 1
|
||||||
Guarded 1
|
Guarded 1
|
||||||
GuardOffset 16 0 0
|
GuardCount 4
|
||||||
Patrol {
|
ResponseCount 2
|
||||||
ERR_SitePatrolPoint { Offset 18 0 0 }
|
GuardOffset 12 0 0
|
||||||
ERR_SitePatrolPoint { Offset 18 0 24 }
|
ResponseOffset -12 0 0
|
||||||
ERR_SitePatrolPoint { Offset -6 0 24 }
|
|
||||||
ERR_SitePatrolPoint { Offset -6 0 0 }
|
|
||||||
}
|
|
||||||
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
DiscoverRadius 35
|
ContainerOffset 0 0 -4
|
||||||
RevealOnEscape 0
|
DiscoverRadius 60
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||||
|
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||||
|
}
|
||||||
Stock {
|
Stock {
|
||||||
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.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 "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||||
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||||
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||||
}
|
}
|
||||||
Props {
|
|
||||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 0 0 -4 }
|
|
||||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset -4 0 0 Angles 90 0 0 }
|
|
||||||
ERR_SiteProp { Prefab "{4B6AC1D8701C35A1}Prefabs/ERR/FieldCover.et" Offset 4 0 0 Angles 90 0 0 }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ERR_SiteDefinition {
|
ERR_SiteDefinition {
|
||||||
Id "everon-mortar-1"
|
Id "everon-mortar-1"
|
||||||
@@ -153,5 +163,179 @@ ERR_ScenarioDefinition {
|
|||||||
RandomClear 1
|
RandomClear 1
|
||||||
DiscoverRadius 35
|
DiscoverRadius 35
|
||||||
}
|
}
|
||||||
|
ERR_SiteDefinition {
|
||||||
|
Id "everon-medical-2"
|
||||||
|
Label "Field hospital 2"
|
||||||
|
Kind 6
|
||||||
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
|
Position 3000 0 6000
|
||||||
|
RandomClear 1
|
||||||
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
|
ContainerOffset 6 0 -3
|
||||||
|
DiscoverRadius 60
|
||||||
|
RevealOnEscape 0
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{F8427C3EFC52644E}Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et" Offset 0 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 6 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -6 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 6 0 -7 }
|
||||||
|
}
|
||||||
|
Stock {
|
||||||
|
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||||
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||||
|
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 8 }
|
||||||
|
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ERR_SiteDefinition {
|
||||||
|
Id "everon-medical-3"
|
||||||
|
Label "Field hospital 3"
|
||||||
|
Kind 6
|
||||||
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
|
Position 8000 0 9500
|
||||||
|
RandomClear 1
|
||||||
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
|
ContainerOffset 6 0 -3
|
||||||
|
DiscoverRadius 60
|
||||||
|
RevealOnEscape 0
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{F8427C3EFC52644E}Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et" Offset 0 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 6 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -6 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 6 0 -7 }
|
||||||
|
}
|
||||||
|
Stock {
|
||||||
|
ERR_SiteStock { Prefab "{554159A6740C3F3E}Prefabs/ERR/ObjectiveDocument.et" Count 2 TargetId "everon-radio-1" }
|
||||||
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 12 }
|
||||||
|
ERR_SiteStock { Prefab "{527D7C5D2E476BDC}Prefabs/Items/Medicine/SalineBag_01/SalineBag_USSR_01.et" Count 8 }
|
||||||
|
ERR_SiteStock { Prefab "{0D9A5DCF89AE7AA9}Prefabs/Items/Medicine/MorphineInjection_01/MorphineInjection_01.et" Count 6 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ERR_SiteDefinition {
|
||||||
|
Id "everon-camp-2"
|
||||||
|
Label "Field camp 2"
|
||||||
|
Kind 3
|
||||||
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
|
Position 2500 0 4500
|
||||||
|
RandomClear 1
|
||||||
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
|
ContainerOffset 0 0 -4
|
||||||
|
DiscoverRadius 60
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||||
|
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||||
|
}
|
||||||
|
Stock {
|
||||||
|
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||||
|
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ERR_SiteDefinition {
|
||||||
|
Id "everon-camp-3"
|
||||||
|
Label "Field camp 3"
|
||||||
|
Kind 3
|
||||||
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
|
Position 5500 0 3500
|
||||||
|
RandomClear 1
|
||||||
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
|
ContainerOffset 0 0 -4
|
||||||
|
DiscoverRadius 60
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||||
|
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||||
|
}
|
||||||
|
Stock {
|
||||||
|
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||||
|
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ERR_SiteDefinition {
|
||||||
|
Id "everon-camp-4"
|
||||||
|
Label "Field camp 4"
|
||||||
|
Kind 3
|
||||||
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
|
Position 7800 0 6000
|
||||||
|
RandomClear 1
|
||||||
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
|
ContainerOffset 0 0 -4
|
||||||
|
DiscoverRadius 60
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||||
|
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||||
|
}
|
||||||
|
Stock {
|
||||||
|
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||||
|
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ERR_SiteDefinition {
|
||||||
|
Id "everon-camp-5"
|
||||||
|
Label "Field camp 5"
|
||||||
|
Kind 3
|
||||||
|
Prefab "{7C3D16C17C24D351}Prefabs/ERR/SupplySite.et"
|
||||||
|
Position 5200 0 10000
|
||||||
|
RandomClear 1
|
||||||
|
Guarded 1
|
||||||
|
GuardCount 4
|
||||||
|
ResponseCount 2
|
||||||
|
GuardOffset 12 0 0
|
||||||
|
ResponseOffset -12 0 0
|
||||||
|
Container "{40986810D0B1F4AE}Prefabs/ERR/SupplyCache.et"
|
||||||
|
ContainerOffset 0 0 -4
|
||||||
|
DiscoverRadius 60
|
||||||
|
Props {
|
||||||
|
ERR_SiteProp { Prefab "{BE5D5CFA90224646}Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et" Offset -4 0 2 }
|
||||||
|
ERR_SiteProp { Prefab "{01AE5FD77A9A4C21}Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et" Offset 5 0 2 Angles 180 0 0 }
|
||||||
|
ERR_SiteProp { Prefab "{53506DCB398C7EBE}Prefabs/Props/Furniture/Table_01.et" Offset 0 0 -1 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset -5 0 -7 }
|
||||||
|
ERR_SiteProp { Prefab "{CFFEB23FBEA00322}Prefabs/Props/Military/Sandbags/Sandbag_01_short_burlap.et" Offset 5 0 -7 }
|
||||||
|
}
|
||||||
|
Stock {
|
||||||
|
ERR_SiteStock { Prefab "{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{BBB50A815A2F916B}Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et" Count 10 }
|
||||||
|
ERR_SiteStock { Prefab "{243948B23D90BECB}Prefabs/Items/Equipment/Binoculars/Binoculars_B8/Binoculars_B8.et" Count 2 }
|
||||||
|
ERR_SiteStock { Prefab "{C3F1FA1E2EC2B345}Prefabs/Items/Medicine/FieldDressing_01/FieldDressing_USSR_01.et" Count 4 }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
SCR_MissionHeader {
|
SCR_MissionHeader {
|
||||||
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
||||||
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
|
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
|
||||||
m_sName "Escapists Reintroduced — Everon"
|
m_sName "Co10 Escape Reintroduced — Everon"
|
||||||
m_sAuthor "Jakub Jim Žáček"
|
m_sAuthor "Jakub Jim Žáček"
|
||||||
m_sDescription "Independent escape prototype. Development scenario; runtime validation in progress."
|
m_sDescription "Independent escape prototype. Development scenario; runtime validation in progress."
|
||||||
m_sGameMode "Escape"
|
m_sGameMode "Escape"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
SCR_MissionHeader {
|
SCR_MissionHeader {
|
||||||
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
World "{B6C516F86302C679}Worlds/ERR_Everon.ent"
|
||||||
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
|
SystemsConfig "{C50579A2EC48E8EF}Configs/Systems/MissionSystems.conf"
|
||||||
m_sName "Escapists Reintroduced — Everon"
|
m_sName "Co10 Escape Reintroduced — Everon"
|
||||||
m_sAuthor "Jakub Jim Žáček"
|
m_sAuthor "Jakub Jim Žáček"
|
||||||
m_sDescription "Legacy diagnostic header for the integrated Escapists Reintroduced mod."
|
m_sDescription "Legacy diagnostic header for the integrated Co10 Escape Reintroduced mod."
|
||||||
m_sGameMode "Escape"
|
m_sGameMode "Escape"
|
||||||
m_iPlayerCount 12
|
m_iPlayerCount 12
|
||||||
m_eSaveTypes 0
|
m_eSaveTypes 0
|
||||||
|
|||||||
@@ -6,7 +6,12 @@ GenericEntity {
|
|||||||
ERR_LocationComponent "{C19CA6CC83470879}" { m_Kind 1 }
|
ERR_LocationComponent "{C19CA6CC83470879}" { m_Kind 1 }
|
||||||
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
|
RplComponent "{54ED7463161A5622}" { Streamable Disabled }
|
||||||
ActionsManagerComponent "{B081EC0768636EAB}" {
|
ActionsManagerComponent "{B081EC0768636EAB}" {
|
||||||
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" { ContextName "ERR_Radio" Radius 2 } }
|
ActionContexts { UserActionContext "{C1A75A5D8B7EF1E4}" {
|
||||||
|
ContextName "ERR_Radio"
|
||||||
|
Position PointInfo "{A1D6B2AE8C5F6301}" { Offset 0 0.25 0 }
|
||||||
|
Radius 3
|
||||||
|
Omnidirectional 1
|
||||||
|
} }
|
||||||
additionalActions { ERR_TuneRadioAction "{7ABBB6B5D73DDD95}" {
|
additionalActions { ERR_TuneRadioAction "{7ABBB6B5D73DDD95}" {
|
||||||
ParentContextList { "ERR_Radio" }
|
ParentContextList { "ERR_Radio" }
|
||||||
UIInfo SCR_ActionUIInfo "{1B42BB7CEDECB994}" { Name "Contact extraction HQ" }
|
UIInfo SCR_ActionUIInfo "{1B42BB7CEDECB994}" { Name "Contact extraction HQ" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
SCR_ManualCamera {
|
SCR_ManualCamera {
|
||||||
ID "634904BCEDA1A285"
|
ID "634904BCEDA1A285"
|
||||||
m_bInputEnabled 0
|
m_bInputEnabled 1
|
||||||
m_bStartAtEntity 1
|
m_bStartAtEntity 1
|
||||||
m_aComponents {
|
m_aComponents {
|
||||||
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
ERR_FollowSurvivorCameraComponent "{965C2E8B2339B926}" {}
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ class ERR_AdminMenuFields
|
|||||||
{
|
{
|
||||||
static array<string> Keys()
|
static array<string> Keys()
|
||||||
{
|
{
|
||||||
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount"};
|
return {"radioDefenceSeconds", "aiIntelChancePercent", "intelRevealChancePercent", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "convoyCount", "antiAirChancePercent", "antiPersonnelMineChancePercent"};
|
||||||
}
|
}
|
||||||
static bool Available(string key, bool hq, bool intel)
|
static bool Available(string key, bool hq, bool intel)
|
||||||
{
|
{
|
||||||
if (key == "aiIntelChancePercent") return intel;
|
if (key == "aiIntelChancePercent") return intel;
|
||||||
if (key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
|
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres") return hq;
|
||||||
return Keys().Contains(key);
|
return Keys().Contains(key);
|
||||||
}
|
}
|
||||||
// A short journal has no older off-screen lines, even if its slider is zero.
|
// A short journal has no older off-screen lines, even if its slider is zero.
|
||||||
@@ -23,10 +23,9 @@ class ERR_AdminMenuFields
|
|||||||
}
|
}
|
||||||
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
|
// TextWidget interprets hash-prefixed tokens as localization IDs. Keep the
|
||||||
// server audit and retained entries intact; render their numeric IDs literally.
|
// server audit and retained entries intact; render their numeric IDs literally.
|
||||||
static string JournalText(string line)
|
static string JournalText(string line, bool details = false)
|
||||||
{
|
{
|
||||||
line.Replace("#", "No. ");
|
return ERR_JournalText.Menu(line, details);
|
||||||
return line;
|
|
||||||
}
|
}
|
||||||
static string Display(string key, string value)
|
static string Display(string key, string value)
|
||||||
{
|
{
|
||||||
@@ -49,6 +48,8 @@ class ERR_AdminMenuFields
|
|||||||
else if (key == "reportIntervalSeconds") prefix = "hq interval";
|
else if (key == "reportIntervalSeconds") prefix = "hq interval";
|
||||||
else if (key == "supportSquadLimit") prefix = "hq squads";
|
else if (key == "supportSquadLimit") prefix = "hq squads";
|
||||||
else if (key == "responseRadiusMetres") prefix = "hq radius";
|
else if (key == "responseRadiusMetres") prefix = "hq radius";
|
||||||
|
else if (key == "antiAirChancePercent") prefix = "hq aa";
|
||||||
|
else if (key == "antiPersonnelMineChancePercent") prefix = "hq mines";
|
||||||
else if (key == "convoyCount") prefix = "hq convoy count";
|
else if (key == "convoyCount") prefix = "hq convoy count";
|
||||||
value = value.Trim();
|
value = value.Trim();
|
||||||
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
|
if (prefix.IsEmpty() || value.IsEmpty() || value.Length() > 32) return "";
|
||||||
@@ -66,6 +67,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
protected ref array<string> m_AvailableFields = {};
|
protected ref array<string> m_AvailableFields = {};
|
||||||
protected ref array<string> m_LogLines = {};
|
protected ref array<string> m_LogLines = {};
|
||||||
protected bool m_Bound;
|
protected bool m_Bound;
|
||||||
|
protected bool m_LogDetails;
|
||||||
protected bool m_Ready;
|
protected bool m_Ready;
|
||||||
protected bool m_HQ;
|
protected bool m_HQ;
|
||||||
protected bool m_Intel;
|
protected bool m_Intel;
|
||||||
@@ -98,7 +100,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
m_Controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
|
m_Mode = ERR_GameMode.Cast(GetGame().GetGameMode());
|
||||||
if (!m_Controller) { Close(); return; }
|
if (!m_Controller) { Close(); return; }
|
||||||
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "LatestJournal", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"};
|
array<string> names = {"Close", "OpenSettings", "OpenWorld", "OpenFactions", "OpenHQ", "OpenJournal", "PauseJournal", "JournalDetails", "LatestJournal", "KeepWeaponOn", "KeepWeaponOff", "IntelSummary", "RerollIntel", "OpenMods", "OpenIntel", "OpenStatus"};
|
||||||
names.Insert("RandomDay_startTimeMinutes");
|
names.Insert("RandomDay_startTimeMinutes");
|
||||||
foreach (string key : ERR_AdminMenuFields.Keys())
|
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||||
{
|
{
|
||||||
@@ -221,6 +223,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
}
|
}
|
||||||
else if (name == "RerollIntel" || name == "IntelSummary") button.SetEnabled(m_Ready && m_Intel);
|
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 == "LatestJournal") button.SetEnabled(m_Ready && m_HQ);
|
||||||
|
else if (name == "KeepWeaponOn" || name == "KeepWeaponOff") button.SetEnabled(m_Ready);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
void ReceiveSnapshot(bool authorized, array<string> values, array<string> status)
|
||||||
@@ -241,6 +244,13 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
|
if (ERR_AdminMenuFields.Keys().Contains(resolvedKey)) SetText("Saved_" + resolvedKey, "Saved: Random (using " + entry.Substring(divider + 1, entry.Length() - divider - 1) + ")");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (key == "keepWeaponOnUnconscious")
|
||||||
|
{
|
||||||
|
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;
|
if (!ERR_AdminMenuFields.Keys().Contains(key)) continue;
|
||||||
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
|
string value = ERR_AdminMenuFields.Display(key, entry.Substring(divider + 1, entry.Length() - divider - 1));
|
||||||
m_AvailableFields.Insert(key);
|
m_AvailableFields.Insert(key);
|
||||||
@@ -262,7 +272,7 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
}
|
}
|
||||||
if (summary.IsEmpty()) summary = "No mission status is available yet.";
|
if (summary.IsEmpty()) summary = "No mission status is available yet.";
|
||||||
SetText("Status", summary);
|
SetText("Status", summary);
|
||||||
string connection = "Connected. Saved values update live; your drafts are kept until this menu closes.";
|
string connection = "Connected to server. Editing a value does not save it: press Apply. Unsaved edits are lost on close.";
|
||||||
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
|
if (!m_HQ || !m_Intel) connection = "Connected. Controls for companion systems absent from this mission are disabled.";
|
||||||
SetText("Connection", connection);
|
SetText("Connection", connection);
|
||||||
m_Ready = true; m_Age = 0; UpdateActions();
|
m_Ready = true; m_Age = 0; UpdateActions();
|
||||||
@@ -293,17 +303,21 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
m_LogLines.Insert(line);
|
m_LogLines.Insert(line);
|
||||||
}
|
}
|
||||||
m_LogCursor = cursor;
|
m_LogCursor = cursor;
|
||||||
|
RenderJournal();
|
||||||
|
m_ScrollToEnd = follow;
|
||||||
|
}
|
||||||
|
SetText("JournalState", "Live. Times use the server clock (HH:MM:SS). Up to 300 entries; pause to read older ones.");
|
||||||
|
}
|
||||||
|
protected void RenderJournal()
|
||||||
|
{
|
||||||
string content;
|
string content;
|
||||||
foreach (string entry : m_LogLines)
|
foreach (string entry : m_LogLines)
|
||||||
{
|
{
|
||||||
if (!content.IsEmpty()) content += "\n";
|
if (!content.IsEmpty()) content += "\n\n";
|
||||||
content += ERR_AdminMenuFields.JournalText(entry);
|
content += ERR_AdminMenuFields.JournalText(entry, m_LogDetails);
|
||||||
}
|
}
|
||||||
if (content.IsEmpty()) content = "Waiting for the first HQ event...";
|
if (content.IsEmpty()) content = "Waiting for the first HQ event...";
|
||||||
SetText("JournalText", content);
|
SetText("JournalText", content);
|
||||||
m_ScrollToEnd = follow;
|
|
||||||
}
|
|
||||||
SetText("JournalState", "Live. Up to 300 received entries are retained here. Pause to read older entries.");
|
|
||||||
}
|
}
|
||||||
protected void OnButton(SCR_ButtonBaseComponent button)
|
protected void OnButton(SCR_ButtonBaseComponent button)
|
||||||
{
|
{
|
||||||
@@ -312,6 +326,17 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
if (name == "Close") { Close(); return; }
|
if (name == "Close") { Close(); return; }
|
||||||
int page = Navigation().Find(name);
|
int page = Navigation().Find(name);
|
||||||
if (page >= 0) { ShowPage(page); return; }
|
if (page >= 0) { ShowPage(page); return; }
|
||||||
|
if (name == "JournalDetails")
|
||||||
|
{
|
||||||
|
m_LogDetails = !m_LogDetails;
|
||||||
|
SCR_ButtonTextComponent detailsButton = SCR_ButtonTextComponent.Cast(button);
|
||||||
|
if (detailsButton)
|
||||||
|
{
|
||||||
|
if (m_LogDetails) detailsButton.SetText("Show readable summaries");
|
||||||
|
else detailsButton.SetText("Show technical details");
|
||||||
|
}
|
||||||
|
RenderJournal(); return;
|
||||||
|
}
|
||||||
if (name == "PauseJournal")
|
if (name == "PauseJournal")
|
||||||
{
|
{
|
||||||
m_LogPaused = !m_LogPaused;
|
m_LogPaused = !m_LogPaused;
|
||||||
@@ -324,11 +349,18 @@ class ERR_AdminMenu : ChimeraMenuBase
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (label) label.SetText("Pause live");
|
if (label) label.SetText("Pause live");
|
||||||
SetText("JournalState", "Live - catching up..."); m_LogPoll = 1;
|
SetText("JournalState", "Live - catching up. Times use the server clock (HH:MM:SS)."); m_LogPoll = 1;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
|
if (!m_Controller || !m_Ready || m_Age > 6 || m_ClickDelay > 0) return;
|
||||||
|
if (name == "KeepWeaponOn" || name == "KeepWeaponOff")
|
||||||
|
{
|
||||||
|
string retain = "0"; if (name == "KeepWeaponOn") retain = "1";
|
||||||
|
m_ClickDelay = 0.3;
|
||||||
|
SetText("Feedback", "Saving weapon retention for future knockouts...");
|
||||||
|
m_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true); return;
|
||||||
|
}
|
||||||
if (name == "LatestJournal")
|
if (name == "LatestJournal")
|
||||||
{
|
{
|
||||||
if (!m_HQ || m_LogPending) return;
|
if (!m_HQ || m_LogPending) return;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ class ERR_CommandArguments
|
|||||||
{
|
{
|
||||||
static string CoreSetting(string section, string option)
|
static string CoreSetting(string section, string option)
|
||||||
{
|
{
|
||||||
|
if (section == "mission" && option == "keepweapon") return "keepWeaponOnUnconscious";
|
||||||
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
|
if (section == "mission" && option == "radio") return "radioDefenceSeconds";
|
||||||
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
|
if (section == "intel" && option == "reveal") return "intelRevealChancePercent";
|
||||||
return "";
|
return "";
|
||||||
@@ -106,6 +107,9 @@ class ERR_AdminService
|
|||||||
}
|
}
|
||||||
protected string SettingHelp(string key)
|
protected string SettingHelp(string key)
|
||||||
{
|
{
|
||||||
|
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.";
|
||||||
if (key == "intelRevealChancePercent")
|
if (key == "intelRevealChancePercent")
|
||||||
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 "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 "";
|
return "";
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ class ERR_ContentOption
|
|||||||
string NativeFactionKey;
|
string NativeFactionKey;
|
||||||
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
[Attribute("", UIWidgets.ResourceNamePicker, "Physical guard group belonging to this faction", "et")]
|
||||||
ResourceName GuardGroup;
|
ResourceName GuardGroup;
|
||||||
|
[Attribute("", UIWidgets.ResourceNamePicker, "Optional faction MANPADS override; must spawn loaded and usable. Otherwise FIM-92 is used.", "et")]
|
||||||
|
ResourceName AntiAirLauncher;
|
||||||
[Attribute("", UIWidgets.EditBox, "Exact resource GUIDs for optional equipment owned by this pack")]
|
[Attribute("", UIWidgets.EditBox, "Exact resource GUIDs for optional equipment owned by this pack")]
|
||||||
ref array<string> EquipmentGuids;
|
ref array<string> EquipmentGuids;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ERR_ContentSettings
|
|||||||
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
else if (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||||
{
|
{
|
||||||
m_ReadOnly = true;
|
m_ReadOnly = true;
|
||||||
Print("Escapists: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
|
Print("Co10 Escape Reintroduced: invalid content preferences; authored defaults active, existing files preserved", LogLevel.ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool Enabled(string id) { return m_Values.Contains(id) && m_Values.Get(id) == 1; }
|
bool Enabled(string id) { return m_Values.Contains(id) && m_Values.Get(id) == 1; }
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class ERR_DiscoveryMarker : SCR_MapMarkerBase
|
|||||||
if (kind == ERR_LocationKind.RADIO) return "radio";
|
if (kind == ERR_LocationKind.RADIO) return "radio";
|
||||||
if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission";
|
if (kind == ERR_LocationKind.MORTAR) return "artilleryFireMission";
|
||||||
if (kind == ERR_LocationKind.OUTPOST) return "building";
|
if (kind == ERR_LocationKind.OUTPOST) return "building";
|
||||||
if (id.Contains("medical")) return "medical";
|
if (kind == ERR_LocationKind.HOSPITAL || id.Contains("medical")) return "medical";
|
||||||
return "supplies";
|
return "supplies";
|
||||||
}
|
}
|
||||||
void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2)
|
void SetLocationIcon(ERR_LocationKind kind, string id, int knowledge = 2)
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ modded class ERR_AdminMenu
|
|||||||
if (name == "FactionsRandomAll")
|
if (name == "FactionsRandomAll")
|
||||||
{
|
{
|
||||||
foreach (string role : ERR_FactionChoices.Roles()) SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget()).SetCurrentItem(0);
|
foreach (string role : ERR_FactionChoices.Roles()) SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget()).SetCurrentItem(0);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (name != "FactionsApply") return;
|
// Random all saves the mode immediately; resolving a round never replaces it.
|
||||||
|
if (name != "FactionsApply" && name != "FactionsRandomAll") return;
|
||||||
string command = "factions set";
|
string command = "factions set";
|
||||||
foreach (string role : ERR_FactionChoices.Roles())
|
foreach (string role : ERR_FactionChoices.Roles())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class ERR_FactionSelection
|
|||||||
map<string, string> loaded;
|
map<string, string> loaded;
|
||||||
if (Read(m_Path, loaded) || Read(m_Path + ".bak", loaded)) m_Saved = 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 (FileIO.FileExists(m_Path) || FileIO.FileExists(m_Path + ".bak"))
|
||||||
{ m_ReadOnly = true; Print("Escapists: invalid faction selections; defaults active and files preserved", LogLevel.ERROR); }
|
{ 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)
|
protected bool Read(string path, out map<string, string> values)
|
||||||
{
|
{
|
||||||
@@ -192,7 +192,7 @@ class ERR_FactionSelection
|
|||||||
if (!player.IsPlayable()) return false;
|
if (!player.IsPlayable()) return false;
|
||||||
m_Ready = true;
|
m_Ready = true;
|
||||||
mode.PublishFactionRoles(Choice("escapists").Key, Choice("enemy").Key);
|
mode.PublishFactionRoles(Choice("escapists").Key, Choice("enemy").Key);
|
||||||
PrintFormat("Escapists factions: players=%1 enemy=%2 independent=%3", Choice("escapists").Key, Choice("enemy").Key, Choice("independent").Key);
|
PrintFormat("Co10 Escape Reintroduced factions: players=%1 enemy=%2 independent=%3", Choice("escapists").Key, Choice("enemy").Key, Choice("independent").Key);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
void Snapshot(ERR_ContentSettings content, array<string> values)
|
void Snapshot(ERR_ContentSettings content, array<string> values)
|
||||||
|
|||||||
@@ -343,6 +343,8 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
{
|
{
|
||||||
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
|
if (!Replication.IsServer() || !actor || ERR_MissionRules.Terminal(m_Phase)) return;
|
||||||
m_Roster.Spawned(id, actor);
|
m_Roster.Spawned(id, actor);
|
||||||
|
SCR_CharacterDamageManagerComponent medical = SCR_CharacterDamageManagerComponent.Cast(actor.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
|
if (medical) medical.ERR_EnableCasualtyProtection();
|
||||||
m_StartedPlayers = true;
|
m_StartedPlayers = true;
|
||||||
m_NoSurvivorsSince = -1;
|
m_NoSurvivorsSince = -1;
|
||||||
}
|
}
|
||||||
@@ -493,6 +495,13 @@ class ERR_GameMode : SCR_BaseGameMode
|
|||||||
if (RefreshRadioFeedback()) Replication.BumpMe();
|
if (RefreshRadioFeedback()) Replication.BumpMe();
|
||||||
m_Recovery.Tick();
|
m_Recovery.Tick();
|
||||||
if (!Active()) return;
|
if (!Active()) return;
|
||||||
|
if (m_Recovery.DefeatDue(now))
|
||||||
|
{
|
||||||
|
m_EndReason = "All prisoners are unconscious and no treatment or recovery is progressing.";
|
||||||
|
ERR_HQ.Event("MISSION FAILED", m_EndReason);
|
||||||
|
Transition(ERR_Phase.DEFEAT);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (m_Command) m_Command.Maintain(now);
|
if (m_Command) m_Command.Maintain(now);
|
||||||
// Maintenance can finish or stop the round; do not resume tactical work.
|
// Maintenance can finish or stop the round; do not resume tactical work.
|
||||||
if (!Active()) return;
|
if (!Active()) return;
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class ERR_CampTerrain
|
|||||||
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
if (!Clear(position, heading, 10, 0.6, baseHeight)) return false;
|
||||||
float guardHeight;
|
float guardHeight;
|
||||||
if (site.Guarded && !Clear(position + Rotate(site.GuardOffset, heading), heading, 6, 0.6, guardHeight)) return false;
|
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;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,6 +94,8 @@ class ERR_GeneratedSites
|
|||||||
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
|
foreach (ERR_SiteDefinition prison : definition.Sites) if (prison.RandomClear && prison.Kind == ERR_LocationKind.PRISON) m_Pending.Insert(prison);
|
||||||
foreach (ERR_SiteDefinition radio : definition.Sites) if (radio.RandomClear && radio.Kind == ERR_LocationKind.RADIO) m_Pending.Insert(radio);
|
foreach (ERR_SiteDefinition 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 mortar : definition.Sites) if (mortar.RandomClear && mortar.Kind == ERR_LocationKind.MORTAR) m_Pending.Insert(mortar);
|
||||||
|
foreach (ERR_SiteDefinition optional : definition.Sites)
|
||||||
|
if (optional.RandomClear && (optional.Kind == ERR_LocationKind.HOSPITAL || optional.Kind == ERR_LocationKind.OUTPOST || optional.Kind == ERR_LocationKind.DEPOT)) m_Pending.Insert(optional);
|
||||||
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
GetGame().GetWorldEntity().GetWorldBounds(m_Low, m_High);
|
||||||
if (!m_Pending.IsEmpty()) m_Terrain.Collect(m_Low, m_High);
|
if (!m_Pending.IsEmpty()) m_Terrain.Collect(m_Low, m_High);
|
||||||
m_Began = System.GetTickCount();
|
m_Began = System.GetTickCount();
|
||||||
@@ -103,8 +106,12 @@ class ERR_GeneratedSites
|
|||||||
foreach (ERR_SiteDefinition other : m_Definition.Sites)
|
foreach (ERR_SiteDefinition other : m_Definition.Sites)
|
||||||
{
|
{
|
||||||
if (other == candidate) continue;
|
if (other == candidate) continue;
|
||||||
|
// Unresolved random sites have no occupied footprint yet. Later placements
|
||||||
|
// compare against all committed positions, not their unused fallback seeds.
|
||||||
|
if (other.RandomClear && !other.PlacementResolved) continue;
|
||||||
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.RADIO && other.RandomClear) continue;
|
if (candidate.Kind == ERR_LocationKind.PRISON && other.Kind == ERR_LocationKind.RADIO && other.RandomClear) continue;
|
||||||
float minimum = 300;
|
float minimum = 300;
|
||||||
|
if (candidate.Kind == ERR_LocationKind.HOSPITAL || candidate.Kind == ERR_LocationKind.OUTPOST || candidate.Kind == ERR_LocationKind.DEPOT) minimum = 600;
|
||||||
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.MORTAR) minimum = 1000;
|
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.MORTAR) minimum = 1000;
|
||||||
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.PRISON) minimum = 500;
|
if (candidate.Kind == ERR_LocationKind.MORTAR && other.Kind == ERR_LocationKind.PRISON) minimum = 500;
|
||||||
if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000;
|
if (candidate.Kind == ERR_LocationKind.RADIO && other.Kind == ERR_LocationKind.PRISON) minimum = 1000;
|
||||||
@@ -133,8 +140,8 @@ class ERR_GeneratedSites
|
|||||||
{
|
{
|
||||||
position[1] = floor + 0.02;
|
position[1] = floor + 0.02;
|
||||||
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
|
site.Position = position; site.Heading = heading; site.PlacementResolved = true;
|
||||||
PrintFormat("Escapists [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
|
PrintFormat("Co10 Escape Reintroduced [generated site]: id=%1 position=%2 heading=%3 attempts=%4 fallback=%5", site.Id, position, heading, m_Attempts, fallback);
|
||||||
m_Index++; m_Attempts = 0;
|
m_Index++; m_Attempts = 0; m_Began = System.GetTickCount();
|
||||||
}
|
}
|
||||||
else if (fallback)
|
else if (fallback)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// Presentation only. Original records stay available in the menu and log file.
|
||||||
|
class ERR_JournalText
|
||||||
|
{
|
||||||
|
static string Field(string message, string key)
|
||||||
|
{
|
||||||
|
int start = message.IndexOf(key + "=");
|
||||||
|
if (start < 0) return "unknown";
|
||||||
|
start += key.Length() + 1;
|
||||||
|
int end = start;
|
||||||
|
if (start < message.Length() && message.Substring(start, 1) == "<")
|
||||||
|
{
|
||||||
|
while (end < message.Length() && message.Substring(end, 1) != ">") end++;
|
||||||
|
if (end < message.Length()) end++;
|
||||||
|
}
|
||||||
|
else while (end < message.Length() && message.Substring(end, 1) != " " && message.Substring(end, 1) != ";") end++;
|
||||||
|
if (end == start) return "unknown";
|
||||||
|
return message.Substring(start, end - start);
|
||||||
|
}
|
||||||
|
static string Short(string text)
|
||||||
|
{
|
||||||
|
int address = text.IndexOf("<0x");
|
||||||
|
if (address >= 0) text = text.Substring(0, address) + " [entity details available]";
|
||||||
|
int resource = text.IndexOf("{");
|
||||||
|
if (resource >= 0) text = text.Substring(0, resource);
|
||||||
|
if (text.Length() > 190) text = text.Substring(0, 187) + "...";
|
||||||
|
text.Replace("\n", " ");
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
static string Subject(string message)
|
||||||
|
{
|
||||||
|
int end = message.IndexOf(" ");
|
||||||
|
if (end < 0) return Short(message);
|
||||||
|
string result = message.Substring(0, end);
|
||||||
|
result.Replace("aggression-uaz-", "UAZ patrol ");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static string YesNo(string value)
|
||||||
|
{
|
||||||
|
if (value == "1" || value == "true") return "yes";
|
||||||
|
if (value == "0" || value == "false") return "no";
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
static string Description(string kind, string message)
|
||||||
|
{
|
||||||
|
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 == "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") + ".";
|
||||||
|
if (kind == "HUNTER OPENING") return "Three starting UAZ patrol positions found on clear roads, at least 90 degrees apart around the players.";
|
||||||
|
if (kind == "HUNTER OPENING FAILED") return "Could not find three safe, widely separated road positions. Starting UAZ spawns were withheld.";
|
||||||
|
if (kind == "HUNTER WAIT") return "UAZ deployment delayed: " + Short(message);
|
||||||
|
if (kind == "HUNTER FAILED") return "UAZ deployment failed. This deployment is used up; no replacement is spawned. " + Short(message);
|
||||||
|
if (kind == "HUNTER RELEASE") return "UAZ driving task ended: " + Short(message);
|
||||||
|
if (kind == "REPORT") return "Sighting report queued for radio transmission: " + Short(message);
|
||||||
|
if (kind == "BODY REPORT") return "Body report queued for radio transmission: " + Short(message);
|
||||||
|
if (kind == "STATUS REPORT") return "Squad status report queued: " + Short(message);
|
||||||
|
if (kind == "CALL") return "A soldier is making a radio report; transmission starts after three uninterrupted seconds.";
|
||||||
|
if (kind == "CALL COMPLETE") return "The soldier finished making the report. Radio delivery can now proceed.";
|
||||||
|
if (kind == "TRANSMIT") return "Radio message in transit: " + Short(message);
|
||||||
|
if (kind == "RECEIVE") return "Radio message received: " + Short(message);
|
||||||
|
if (kind == "LOST") return "Radio delivery failed: " + Short(message);
|
||||||
|
if (kind == "BELIEF") return "HQ received and retained a report. Orders can use this reported information.";
|
||||||
|
if (kind == "ORDER QUEUED") return "HQ order queued; units have not received it yet. " + Short(message);
|
||||||
|
if (kind == "ORDER DELIVERED") return "HQ order delivered: " + Short(message);
|
||||||
|
if (kind == "ORDER FAILED") return "HQ order could not be carried out: " + Short(message);
|
||||||
|
if (kind == "CHECK-IN OVERDUE") return "A squad missed its expected check-in. This does not confirm that its soldiers are dead. " + Short(message);
|
||||||
|
if (kind == "AA PROFILE") return "Anti-air equipment selected for faction " + Field(message, "faction") + "; FIM-92 is the fallback.";
|
||||||
|
if (kind == "AA FALLBACK") return "The faction anti-air launcher could not be used. Trying FIM-92 instead.";
|
||||||
|
if (kind == "AA EQUIPMENT")
|
||||||
|
{
|
||||||
|
if (message.Contains("physically equipped")) return "A soldier has a loaded anti-air launcher. Dropping or firing it will not grant a replacement.";
|
||||||
|
if (message.Contains("quarantined")) return "Anti-air preparation timed out. An unresolved grant is held to prevent duplicate weapons.";
|
||||||
|
if (message.Contains("looted")) return "The issued anti-air launcher is no longer with its soldier; no replacement was granted.";
|
||||||
|
if (message.Contains("FIM-92")) return "The FIM-92 fallback could not be confirmed usable. Check technical details for the cause.";
|
||||||
|
return "Anti-air preparation stopped before a usable weapon could be confirmed. Check technical details.";
|
||||||
|
}
|
||||||
|
if (kind == "MINE EQUIPMENT")
|
||||||
|
{
|
||||||
|
if (message.Contains("two-mine allowance")) return "A soldier received carried AP mines (up to two). Used or looted mines are not replaced.";
|
||||||
|
if (message.Contains("capacity")) return "No inventory space for AP mines; no loose mines were spawned.";
|
||||||
|
if (message.Contains("deadline")) return "AP-mine preparation timed out; any pending grant will not be duplicated.";
|
||||||
|
return "AP mines could not be added to a soldier's inventory. No loose mines were spawned.";
|
||||||
|
}
|
||||||
|
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 == "INTEL COMPROMISE") return "HQ learned that intel is missing: " + Short(message);
|
||||||
|
if (kind == "OBJECTIVE DEFENCE") return "Objective security: " + Short(message);
|
||||||
|
if (kind == "HELICOPTER LAUNCH") return "Search helicopter launched: " + Short(message);
|
||||||
|
if (kind == "HELICOPTER RETURN") return "Helicopter returning to base: " + Short(message);
|
||||||
|
if (kind == "AIR DROP AUTHORIZED") return "HQ approved a paratrooper drop: " + Short(message);
|
||||||
|
if (kind == "AIR DROP WITHHELD") return "HQ withheld the paratrooper drop: " + Short(message);
|
||||||
|
kind.ToLower(); kind.Replace("_", " ");
|
||||||
|
return kind + ": " + Short(message);
|
||||||
|
}
|
||||||
|
static string Entry(int sequence, string clock, string kind, string message, int messageId)
|
||||||
|
{
|
||||||
|
string line = "[" + clock + "] " + Description(kind, message);
|
||||||
|
line += " (event " + sequence.ToString();
|
||||||
|
if (messageId > 0) line += ", message " + messageId.ToString();
|
||||||
|
return line + ")";
|
||||||
|
}
|
||||||
|
static string Menu(string line, bool details)
|
||||||
|
{
|
||||||
|
int split = line.IndexOf("\n#");
|
||||||
|
if (split >= 0 && !details) line = line.Substring(0, split);
|
||||||
|
line.Replace("#", "No. ");
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR }
|
enum ERR_LocationKind { PRISON, RADIO, EXTRACTION, OUTPOST, DEPOT, MORTAR, HOSPITAL }
|
||||||
|
|
||||||
[ComponentEditorProps(category: "Reintroduced", description: "Server-owned location with replicated discovery state")]
|
[ComponentEditorProps(category: "Reintroduced", description: "Server-owned location with replicated discovery state")]
|
||||||
class ERR_LocationComponentClass : ScriptComponentClass {}
|
class ERR_LocationComponentClass : ScriptComponentClass {}
|
||||||
|
|||||||
@@ -88,6 +88,16 @@ modded class ERR_AdminMenu
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
bool ERR_ProbeFactionSavedRandom()
|
||||||
|
{
|
||||||
|
if (!m_FactionFieldsInitialized) return false;
|
||||||
|
foreach (string role : ERR_FactionChoices.Roles())
|
||||||
|
{
|
||||||
|
SCR_ComboBoxComponent combo = SCR_ComboBoxComponent.GetComboBoxComponent("FactionSelect_" + role, GetRootWidget());
|
||||||
|
if (!combo || combo.GetCurrentIndex() != 0 || m_FactionSaved.Get(role) != "random") return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
void ERR_ProbeFactionApply() { OnFactionButton(SCR_ButtonTextComponent.GetButtonText("FactionsApply", GetRootWidget())); }
|
void ERR_ProbeFactionApply() { OnFactionButton(SCR_ButtonTextComponent.GetButtonText("FactionsApply", GetRootWidget())); }
|
||||||
bool ERR_ProbeHQ() { return m_HQ; }
|
bool ERR_ProbeHQ() { return m_HQ; }
|
||||||
bool ERR_ProbeIntel() { return m_Intel; }
|
bool ERR_ProbeIntel() { return m_Intel; }
|
||||||
@@ -115,6 +125,35 @@ modded class ERR_AdminMenu
|
|||||||
OnModButton(SCR_ButtonTextComponent.GetButtonText(name, row));
|
OnModButton(SCR_ButtonTextComponent.GetButtonText(name, row));
|
||||||
}
|
}
|
||||||
int ERR_ProbeLogCursor() { return m_LogCursor; }
|
int ERR_ProbeLogCursor() { return m_LogCursor; }
|
||||||
|
bool ERR_ProbeJournalDetails()
|
||||||
|
{
|
||||||
|
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
||||||
|
SCR_ButtonTextComponent button = SCR_ButtonTextComponent.GetButtonText("JournalDetails", GetRootWidget());
|
||||||
|
if (!text || !button || m_LogDetails || text.GetText().Contains("[session=")) return false;
|
||||||
|
string summary = text.GetText(); int cursor = m_LogCursor; int entries = m_LogLines.Count();
|
||||||
|
OnButton(button);
|
||||||
|
if (!m_LogDetails || !text.GetText().Contains("[session=") || !text.GetText().Contains("No. ")) return false;
|
||||||
|
OnButton(button);
|
||||||
|
return !m_LogDetails && text.GetText() == summary && cursor == m_LogCursor && entries == m_LogLines.Count();
|
||||||
|
}
|
||||||
|
bool ERR_ProbeDescriptions()
|
||||||
|
{
|
||||||
|
foreach (string key : ERR_AdminMenuFields.Keys())
|
||||||
|
{
|
||||||
|
ERR_ProbeFieldPage(key);
|
||||||
|
TextWidget help = TextWidget.Cast(GetRootWidget().FindAnyWidget("Help_" + key));
|
||||||
|
if (!help || help.GetText().IsEmpty()) return false;
|
||||||
|
GetRootWidget().Update();
|
||||||
|
float width, height, textWidth, textHeight;
|
||||||
|
help.GetScreenSize(width, height); help.GetTextSize(textWidth, textHeight);
|
||||||
|
// GetTextSize uses layout units; screen bounds include the workspace DPI scale.
|
||||||
|
width = GetGame().GetWorkspace().DPIUnscale(width);
|
||||||
|
height = GetGame().GetWorkspace().DPIUnscale(height);
|
||||||
|
PrintFormat("ERR ADMIN HELP BOUNDS: key=%1 widget=%2,%3 text=%4,%5", key, width, height, textWidth, textHeight);
|
||||||
|
if (textWidth > width + 2 || textHeight > height + 2) { Print("ERR ADMIN HELP OVERFLOW: " + key); return false; }
|
||||||
|
}
|
||||||
|
ERR_ProbePage(0); return true;
|
||||||
|
}
|
||||||
bool ERR_ProbeJournalVisible()
|
bool ERR_ProbeJournalVisible()
|
||||||
{
|
{
|
||||||
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
TextWidget text = TextWidget.Cast(GetRootWidget().FindAnyWidget("JournalText"));
|
||||||
@@ -129,7 +168,7 @@ modded class ERR_AdminMenu
|
|||||||
scroll.GetSliderPos(sliderX, sliderY);
|
scroll.GetSliderPos(sliderX, sliderY);
|
||||||
Print(string.Format("ERR ADMIN JOURNAL FOLLOW: slider=%1,%2 overflow=%3", sliderX, sliderY, height - viewHeight));
|
Print(string.Format("ERR ADMIN JOURNAL FOLLOW: slider=%1,%2 overflow=%3", sliderX, sliderY, height - viewHeight));
|
||||||
if (height > viewHeight && sliderY < 0.98) return false;
|
if (height > viewHeight && sliderY < 0.98) return false;
|
||||||
return m_Page == 2 && text.GetText().Contains("No. ") && width > viewWidth * 0.5 && height > 2 && x < viewX + viewWidth && x + width > viewX && y < viewY + viewHeight && y + height > viewY;
|
return m_Page == 2 && text.GetText().Contains("(event ") && width > viewWidth * 0.5 && height > 2 && x < viewX + viewWidth && x + width > viewX && y < viewY + viewHeight && y + height > viewY;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -175,7 +214,7 @@ class ERR_NativeAdminMenuProbe
|
|||||||
m_Deadline = now + 90000;
|
m_Deadline = now + 90000;
|
||||||
m_RhsSaved = m_Mode.ContentSettings().Enabled("rhs");
|
m_RhsSaved = m_Mode.ContentSettings().Enabled("rhs");
|
||||||
GetGame().OpenPauseMenu(false, true);
|
GetGame().OpenPauseMenu(false, true);
|
||||||
if (!PauseMenuUI.ERR_ProbeModsEntry()) return Fail("Esc menu missing Escapists Reintroduced entry");
|
if (!PauseMenuUI.ERR_ProbeModsEntry()) return Fail("Esc menu missing Co10 Escape Reintroduced entry");
|
||||||
PauseMenuUI.ERR_ProbeOpenMods();
|
PauseMenuUI.ERR_ProbeOpenMods();
|
||||||
Pass("native Esc menu entry opens mission administration");
|
Pass("native Esc menu entry opens mission administration");
|
||||||
m_Stage = 1; return 0;
|
m_Stage = 1; return 0;
|
||||||
@@ -187,6 +226,8 @@ class ERR_NativeAdminMenuProbe
|
|||||||
array<string> capabilities = {}; m_Mode.Admin().Capabilities(capabilities);
|
array<string> capabilities = {}; m_Mode.Admin().Capabilities(capabilities);
|
||||||
if (menu.ERR_ProbeHQ() != capabilities.Contains("supports=hq") || menu.ERR_ProbeIntel() != capabilities.Contains("supports=intel")) return Fail("capability snapshot mismatch");
|
if (menu.ERR_ProbeHQ() != capabilities.Contains("supports=hq") || menu.ERR_ProbeIntel() != capabilities.Contains("supports=intel")) return Fail("capability snapshot mismatch");
|
||||||
Pass("real owner RPC opens menu and returns current settings/capabilities");
|
Pass("real owner RPC opens menu and returns current settings/capabilities");
|
||||||
|
if (!menu.ERR_ProbeDescriptions()) return Fail("setting descriptions missing or clipped");
|
||||||
|
Pass("all thirteen setting descriptions fit their native text widgets");
|
||||||
System.MakeScreenshot("$logs:err-admin-open");
|
System.MakeScreenshot("$logs:err-admin-open");
|
||||||
if (!menu.ERR_ProbeNavigation(0)) return Fail("Esc entry did not open Mission page with shared navigation");
|
if (!menu.ERR_ProbeNavigation(0)) return Fail("Esc entry did not open Mission page with shared navigation");
|
||||||
menu.ERR_ProbePage(3);
|
menu.ERR_ProbePage(3);
|
||||||
@@ -259,21 +300,35 @@ class ERR_NativeAdminMenuProbe
|
|||||||
m_FactionActive.Insert(m_Mode.FactionSelection().Active(role));
|
m_FactionActive.Insert(m_Mode.FactionSelection().Active(role));
|
||||||
}
|
}
|
||||||
if (!menu.ERR_ProbeFactionRandomAll()) return Fail("faction selectors or Random all failed");
|
if (!menu.ERR_ProbeFactionRandomAll()) return Fail("faction selectors or Random all failed");
|
||||||
Pass("three faction selectors and Random all create distinct-role drafts");
|
Pass("Random all submits persistent random mode for all three roles");
|
||||||
m_Wait = now + 3000; m_Stage = 20; return 0;
|
m_Wait = now + 3000; m_Stage = 20; return 0;
|
||||||
}
|
}
|
||||||
if (m_Stage == 20)
|
if (m_Stage == 20)
|
||||||
{
|
{
|
||||||
if (!menu || !menu.ERR_ProbeFactionRandomAll()) return Fail("snapshot lost faction drafts");
|
if (!menu || !menu.ERR_ProbeFactionSavedRandom()) return Fail("Random all did not save without Apply");
|
||||||
menu.ERR_ProbeFactionApply();
|
ERR_FactionSelection reloaded = new ERR_FactionSelection();
|
||||||
m_Wait = now + 1500; m_Stage = 21; return 0;
|
foreach (string savedRole : ERR_FactionChoices.Roles())
|
||||||
|
if (reloaded.Saved(savedRole) != "random") return Fail("Random mode did not survive settings reload");
|
||||||
|
menu.Close(); m_Wait = now + 500; m_Stage = 23; return 0;
|
||||||
|
}
|
||||||
|
if (m_Stage == 23)
|
||||||
|
{
|
||||||
|
ERR_AdminMenu.Open(); m_Wait = now + 1500; m_Stage = 24; return 0;
|
||||||
|
}
|
||||||
|
if (m_Stage == 24)
|
||||||
|
{
|
||||||
|
if (!menu || !menu.ERR_ProbeReady()) return 0;
|
||||||
|
menu.ERR_ProbePage(4);
|
||||||
|
if (!menu.ERR_ProbeFactionSavedRandom()) return Fail("Random mode lost after reopening menu");
|
||||||
|
Pass("Random all survives settings reload, fresh snapshots and menu reopen without Apply");
|
||||||
|
m_Stage = 21; return 0;
|
||||||
}
|
}
|
||||||
if (m_Stage == 21)
|
if (m_Stage == 21)
|
||||||
{
|
{
|
||||||
array<string> roles = ERR_FactionChoices.Roles();
|
array<string> roles = ERR_FactionChoices.Roles();
|
||||||
for (int i = 0; i < roles.Count(); i++)
|
for (int i = 0; i < roles.Count(); i++)
|
||||||
if (m_Mode.FactionSelection().Saved(roles[i]) != "random" || m_Mode.FactionSelection().Active(roles[i]) != m_FactionActive[i]) return Fail("faction Apply failed or changed current round");
|
if (m_Mode.FactionSelection().Saved(roles[i]) != "random" || m_Mode.FactionSelection().Active(roles[i]) != m_FactionActive[i]) return Fail("Random all save failed or changed current round");
|
||||||
Pass("faction Apply persists all three through authenticated owner RPC without changing active round");
|
Pass("Random all persists through authenticated owner RPC without changing active round");
|
||||||
m_Controller.ERR_AdminSubmit("factions set " + m_FactionSaved[0] + " " + m_FactionSaved[1] + " " + m_FactionSaved[2], true);
|
m_Controller.ERR_AdminSubmit("factions set " + m_FactionSaved[0] + " " + m_FactionSaved[1] + " " + m_FactionSaved[2], true);
|
||||||
m_Wait = now + 1500; m_Stage = 22; return 0;
|
m_Wait = now + 1500; m_Stage = 22; return 0;
|
||||||
}
|
}
|
||||||
@@ -290,6 +345,8 @@ class ERR_NativeAdminMenuProbe
|
|||||||
{
|
{
|
||||||
if (menu.ERR_ProbeHQ() && menu.ERR_ProbeLogCursor() < 0) return Fail("journal owner RPC did not return");
|
if (menu.ERR_ProbeHQ() && menu.ERR_ProbeLogCursor() < 0) return Fail("journal owner RPC did not return");
|
||||||
if (menu.ERR_ProbeHQ() && !menu.ERR_ProbeJournalVisible()) return Fail("received journal text is empty, collapsed or outside its viewport");
|
if (menu.ERR_ProbeHQ() && !menu.ERR_ProbeJournalVisible()) return Fail("received journal text is empty, collapsed or outside its viewport");
|
||||||
|
if (menu.ERR_ProbeHQ() && !menu.ERR_ProbeJournalDetails()) return Fail("readable/technical journal toggle lost entries or text");
|
||||||
|
Pass("journal switches between readable summaries and complete technical records without losing cursor/history");
|
||||||
System.MakeScreenshot("$logs:err-admin-journal");
|
System.MakeScreenshot("$logs:err-admin-journal");
|
||||||
Pass("HQ capability and journal page remain within current mission services");
|
Pass("HQ capability and journal page remain within current mission services");
|
||||||
menu.Close();
|
menu.Close();
|
||||||
|
|||||||
@@ -16,6 +16,20 @@ modded class ERR_ObjectiveDefence
|
|||||||
}
|
}
|
||||||
modded class ERR_HunterService
|
modded class ERR_HunterService
|
||||||
{
|
{
|
||||||
|
bool ERR_ProbeOpening(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
if (!m_Opening || !m_Opening.Ready() || m_Hunters.Count() < 3) return false;
|
||||||
|
for (int i = 0; i < 3; i++)
|
||||||
|
{
|
||||||
|
ERR_HunterVehicle unit = m_Hunters[i];
|
||||||
|
if (!unit.Registered || !unit.CrewValid(mode) || unit.Members.Count() != 2) return false;
|
||||||
|
bool manpack;
|
||||||
|
foreach (IEntity actor : unit.Members) if (ERR_EnemyRadio.Capability(actor) == 2) manpack = true;
|
||||||
|
if (!manpack) return false;
|
||||||
|
for (int j = i + 1; j < 3; j++) if (!ERR_HunterOpeningRules.Spread(m_Opening.Center, m_Opening.Positions[i], m_Opening.Positions[j])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
ERR_HunterVehicle ERR_ProbeFirst()
|
ERR_HunterVehicle ERR_ProbeFirst()
|
||||||
{
|
{
|
||||||
if (m_Hunters.IsEmpty()) return null;
|
if (m_Hunters.IsEmpty()) return null;
|
||||||
@@ -46,6 +60,7 @@ modded class ERR_GameMode
|
|||||||
protected int m_ERRAggressionStage;
|
protected int m_ERRAggressionStage;
|
||||||
protected int m_ERRAggressionDeadline;
|
protected int m_ERRAggressionDeadline;
|
||||||
protected vector m_ERRHunterStart;
|
protected vector m_ERRHunterStart;
|
||||||
|
protected float m_ERRHunterWait;
|
||||||
protected bool m_ERRHunterOrder;
|
protected bool m_ERRHunterOrder;
|
||||||
protected override void EOnInit(IEntity owner)
|
protected override void EOnInit(IEntity owner)
|
||||||
{
|
{
|
||||||
@@ -74,9 +89,7 @@ modded class ERR_GameMode
|
|||||||
{
|
{
|
||||||
if (!ERR_AggressionCheck(UnlockPrison(player), "native prison release")) return;
|
if (!ERR_AggressionCheck(UnlockPrison(player), "native prison release")) return;
|
||||||
player.SetOrigin(ERR_Actors.Ground(Sites().Find("everon-medical-1").Location.Position() + "300 0 0"));
|
player.SetOrigin(ERR_Actors.Ground(Sites().Find("everon-medical-1").Location.Position() + "300 0 0"));
|
||||||
for (int i = 0; i < 3; i++) hq.ERR_ProbeBody(i, now);
|
if (!ERR_AggressionCheck(hq.Watch().HunterTarget() == 3, "opening allowance is three without identified bodies")) return;
|
||||||
hq.ERR_ProbeBody(2, now);
|
|
||||||
if (!ERR_AggressionCheck(hq.Watch().IdentifiedBodies() == 3 && hq.Watch().Aggression() == 1 && hq.Watch().HunterTarget() == 1, "three distinct delivered bodies unlock one UAZ; duplicate ignored")) return;
|
|
||||||
if (!ERR_AggressionCheck(ERR_HQWatchRules.Aggression(90) == 30 && ERR_HQWatchRules.Hunters(30) == 15 && ERR_HQWatchRules.Hunters(1000) == 15, "maximum 30 aggression / 15 UAZ allowance")) return;
|
if (!ERR_AggressionCheck(ERR_HQWatchRules.Aggression(90) == 30 && ERR_HQWatchRules.Hunters(30) == 15 && ERR_HQWatchRules.Hunters(1000) == 15, "maximum 30 aggression / 15 UAZ allowance")) return;
|
||||||
for (int j = 0; j < Sites().Count(); j++)
|
for (int j = 0; j < Sites().Count(); j++)
|
||||||
{
|
{
|
||||||
@@ -90,6 +103,11 @@ modded class ERR_GameMode
|
|||||||
if (!hunter || !hunter.Registered) return;
|
if (!hunter || !hunter.Registered) return;
|
||||||
if (m_ERRAggressionStage == 1)
|
if (m_ERRAggressionStage == 1)
|
||||||
{
|
{
|
||||||
|
if (!hq.Hunters().ERR_ProbeOpening(this)) return;
|
||||||
|
if (!ERR_AggressionCheck(hq.Hunters().Deployed() == 3, "three native seated UAZs on widely separated roads, each carrying a manpack")) return;
|
||||||
|
for (int bodySlot = 0; bodySlot < 3; bodySlot++) hq.ERR_ProbeBody(bodySlot, now);
|
||||||
|
hq.ERR_ProbeBody(2, now);
|
||||||
|
if (!ERR_AggressionCheck(hq.Watch().IdentifiedBodies() == 3 && hq.Watch().Aggression() == 1 && hq.Watch().HunterTarget() == 4, "three identified bodies add one UAZ beyond opening three; duplicate ignored")) return;
|
||||||
if (!ERR_AggressionCheck(hunter.CrewValid(this) && hunter.Members.Count() == 2, "physical UAZ with native seated driver and gunner")) return;
|
if (!ERR_AggressionCheck(hunter.CrewValid(this) && hunter.Members.Count() == 2, "physical UAZ with native seated driver and gunner")) return;
|
||||||
m_ERRHunterStart = hunter.Entity.GetOrigin(); m_ERRAggressionStage = 2;
|
m_ERRHunterStart = hunter.Entity.GetOrigin(); m_ERRAggressionStage = 2;
|
||||||
}
|
}
|
||||||
@@ -105,11 +123,40 @@ modded class ERR_GameMode
|
|||||||
if (!hq.ERR_ProbeMedicalDefence()) return;
|
if (!hq.ERR_ProbeMedicalDefence()) return;
|
||||||
if (!ERR_AggressionCheck(true, "nearby medical stash gained complete dedicated squad and defend waypoint at terrain height")) return;
|
if (!ERR_AggressionCheck(true, "nearby medical stash gained complete dedicated squad and defend waypoint at terrain height")) return;
|
||||||
if (!ERR_AggressionCheck(true, "HQ radio delivered road order; UAZ drove over 30m; maximum scoped crew skill active")) return;
|
if (!ERR_AggressionCheck(true, "HQ radio delivered road order; UAZ drove over 30m; maximum scoped crew skill active")) return;
|
||||||
if (!ERR_AggressionCheck(hq.Hunters().Deployed() == 1, "bounded allowance does not spawn duplicate UAZs")) return;
|
if (!ERR_AggressionCheck(hq.Hunters().Deployed() >= 3 && hq.Hunters().Deployed() <= 4, "opening and aggression respect cumulative allowance")) return;
|
||||||
|
SCR_CharacterDamageManagerComponent.Cast(hunter.Members[1].FindComponent(SCR_CharacterDamageManagerComponent)).Kill(Instigator.CreateInstigator(player));
|
||||||
|
m_ERRHunterWait = now + 3000; m_ERRAggressionStage = 3; return;
|
||||||
|
}
|
||||||
|
if (m_ERRAggressionStage == 3)
|
||||||
|
{
|
||||||
|
if (now < m_ERRHunterWait) return;
|
||||||
|
if (!ERR_AggressionCheck(!ERR_Actors.Alive(hunter.Members[1]) && !hunter.Released && hunter.CrewValid(this), "gunner casualty preserves the original seated driver's patrol")) return;
|
||||||
|
CharacterControllerComponent.Cast(hunter.Driver.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||||
|
m_ERRHunterWait = now + 3000; m_ERRAggressionStage = 4; return;
|
||||||
|
}
|
||||||
|
if (m_ERRAggressionStage == 4)
|
||||||
|
{
|
||||||
|
if (now < m_ERRHunterWait) return;
|
||||||
|
if (!ERR_AggressionCheck(!ERR_Actors.CanFight(hunter.Driver) && !hunter.Released, "temporarily unconscious driver does not discard the vehicle")) return;
|
||||||
|
CharacterControllerComponent.Cast(hunter.Driver.FindComponent(CharacterControllerComponent)).SetUnconscious(false);
|
||||||
|
m_ERRAggressionStage = 5; return;
|
||||||
|
}
|
||||||
|
if (m_ERRAggressionStage == 5)
|
||||||
|
{
|
||||||
|
if (!hunter.CrewValid(this)) return;
|
||||||
|
CompartmentAccessComponent access = CompartmentAccessComponent.Cast(hunter.Driver.FindComponent(CompartmentAccessComponent));
|
||||||
|
if (!ERR_AggressionCheck(access && access.GetOutVehicle(EGetOutType.TELEPORT, 0, ECloseDoorAfterActions.LEAVE_OPEN, true), "fixture dismounts the original driver through native compartment API")) return;
|
||||||
|
m_ERRHunterWait = now + 2000; m_ERRAggressionStage = 6; return;
|
||||||
|
}
|
||||||
|
if (m_ERRAggressionStage == 6)
|
||||||
|
{
|
||||||
|
if (now < m_ERRHunterWait || !hunter.CrewValid(this)) return;
|
||||||
|
if (!ERR_AggressionCheck(!hunter.Released && hunter.Members.Count() == 2 && !ERR_Actors.Alive(hunter.Members[1]), "native AI reboards original driver without replacing the lost gunner")) return;
|
||||||
hq.Hunters().Stop();
|
hq.Hunters().Stop();
|
||||||
if (!ERR_AggressionCheck(hunter.Released && !hunter.Waypoint && hunter.Entity && hunter.Members.Count() == 2, "stop removes orders and preserves physical vehicle/crew")) return;
|
if (!ERR_AggressionCheck(hunter.Released && !hunter.Waypoint && hunter.Entity && hunter.Members.Count() == 2, "stop removes orders and preserves physical vehicle/crew")) return;
|
||||||
foreach (IEntity survivor : hunter.Members)
|
foreach (IEntity survivor : hunter.Members)
|
||||||
{
|
{
|
||||||
|
if (!ERR_Actors.Alive(survivor)) continue;
|
||||||
SCR_AICombatComponent survivingCombat = SCR_AICombatComponent.Cast(survivor.FindComponent(SCR_AICombatComponent));
|
SCR_AICombatComponent survivingCombat = SCR_AICombatComponent.Cast(survivor.FindComponent(SCR_AICombatComponent));
|
||||||
if (!ERR_AggressionCheck(survivingCombat && survivingCombat.ERR_EnhancedEnemy(), "released driving task preserves surviving crew AI profile")) return;
|
if (!ERR_AggressionCheck(survivingCombat && survivingCombat.ERR_EnhancedEnemy(), "released driving task preserves surviving crew AI profile")) return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ modded class ERR_GameMode
|
|||||||
string enabled;
|
string enabled;
|
||||||
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errBaseSmoke", enabled) || enabled != "1") return;
|
||||||
string nativeEndCase;
|
string nativeEndCase;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "aggression" || nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing" || nativeEndCase == "generated-sites" || nativeEndCase == "mortar-station" || nativeEndCase == "opening-tools" || nativeEndCase == "airfield-markers" || nativeEndCase == "air-logistics" || nativeEndCase == "air-assault" || nativeEndCase == "air-recon")) return;
|
if (System.GetCLIParam("errSmokeOutcome", nativeEndCase) && (nativeEndCase == "unconscious-camera" || nativeEndCase == "town-garrisons" || nativeEndCase == "site-garrisons" || nativeEndCase == "radio-interaction" || nativeEndCase == "enemy-loadout" || nativeEndCase == "aggression" || nativeEndCase == "native-end-active" || nativeEndCase == "native-end-preparing" || nativeEndCase == "generated-sites" || nativeEndCase == "mortar-station" || nativeEndCase == "opening-tools" || nativeEndCase == "airfield-markers" || nativeEndCase == "air-logistics" || nativeEndCase == "air-assault" || nativeEndCase == "air-recon")) return;
|
||||||
ERR_NativeObjectiveProbe.BeginRound();
|
ERR_NativeObjectiveProbe.BeginRound();
|
||||||
ERR_NativeProgressionIdentityProbe.BeginRound();
|
ERR_NativeProgressionIdentityProbe.BeginRound();
|
||||||
string outcome;
|
string outcome;
|
||||||
@@ -267,9 +267,9 @@ modded class ERR_GameMode
|
|||||||
}
|
}
|
||||||
if (m_ERRExtractionCase == "recovery")
|
if (m_ERRExtractionCase == "recovery")
|
||||||
{
|
{
|
||||||
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("administrator abandoned"), "explicit authorized abandonment uses normal defeat result")) return;
|
if (!ERR_BaseCheck(current == ERR_Phase.DEFEAT && m_ERRBaseStep == 502 && EndReason().Contains("All prisoners are unconscious"), "stalled all-unconscious team reaches automatic mission defeat")) return;
|
||||||
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
|
if (!ERR_BaseCheck(!RecoveryStatus().IsEmpty() && !AbandonRound(), "terminal event observes active warning and rejects repeated abandonment")) return;
|
||||||
Print("ERR SMOKE COMPLETE: recovery status and explicit administrator abandonment");
|
Print("ERR SMOKE COMPLETE: player casualty protection, native healing and automatic all-unconscious defeat");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (m_ERRExtractionCase == "objective-ui")
|
if (m_ERRExtractionCase == "objective-ui")
|
||||||
@@ -445,9 +445,7 @@ modded class ERR_GameMode
|
|||||||
if (result == 1)
|
if (result == 1)
|
||||||
{
|
{
|
||||||
m_ERRBaseStep = 502;
|
m_ERRBaseStep = 502;
|
||||||
array<string> replies = {};
|
ERR_BaseCheck(Active() && ERR_Actors.Alive(m_ERRBasePlayer), "fixture leaves a living casualty for the production failure timer");
|
||||||
Admin().Execute(this, true, "mission abandon", replies);
|
|
||||||
ERR_BaseCheck(Phase() == ERR_Phase.DEFEAT && replies.Count() == 1 && replies[0].Contains("Round abandoned") && RecoveryStatus().IsEmpty() && Recovery().Conscious() == 0 && Recovery().Incapacitated() == 0, "administrator command returns accepted outcome after recovery cleanup");
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -811,12 +809,12 @@ modded class ERR_GameMode
|
|||||||
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
if (!ERR_NativeObjectiveProbe.Observe(this)) return;
|
||||||
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent)).SetUnconscious(true);
|
||||||
m_ERRBaseStep = 310;
|
m_ERRBaseStep = 310;
|
||||||
m_ERRBaseWait = now + 35000;
|
m_ERRBaseWait = now + 5000;
|
||||||
}
|
}
|
||||||
else if (m_ERRBaseStep == 310)
|
else if (m_ERRBaseStep == 310)
|
||||||
{
|
{
|
||||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent));
|
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_ERRBasePlayer.FindComponent(CharacterControllerComponent));
|
||||||
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor does not trigger defeat after the full grace period")) return;
|
if (!ERR_BaseCheck(controller.IsUnconscious() && ERR_Actors.Alive(m_ERRBasePlayer) && Players().HasSurvivor() && Phase() == ERR_Phase.RADIO_DEFENCE, "unconscious survivor remains revivable during the failure grace period")) return;
|
||||||
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30, "unconscious radio defender cannot advance transmission")) return;
|
if (!ERR_BaseCheck(!DefenceOccupied() && DefenceSecondsRemaining() == 30, "unconscious radio defender cannot advance transmission")) return;
|
||||||
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Progress has reset"), "radio guide reports interruption after unconsciousness")) return;
|
if (!ERR_BaseCheck(RadioFeedback() == ERR_RadioFeedback.INTERRUPTED && ERR_ObjectiveText.Description(this, false).Contains("Progress has reset"), "radio guide reports interruption after unconsciousness")) return;
|
||||||
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
SCR_PlayerController playerController = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
// Real native inventory fixture; deliberately not registered with the ordinary equipment service.
|
||||||
|
class ERR_ProbeBrokenLauncherGrant : ERR_EnemyLauncherGrant
|
||||||
|
{
|
||||||
|
override protected ResourceName Preferred(ERR_GameMode mode, SCR_AIGroup group)
|
||||||
|
{
|
||||||
|
return "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRLoadoutDeadline;
|
||||||
|
protected int m_ERRLoadoutStage;
|
||||||
|
protected SCR_AIGroup m_ERRLoadoutGroup;
|
||||||
|
protected ref ERR_EnemyLauncherGrant m_ERRNativeAA = new ERR_EnemyLauncherGrant();
|
||||||
|
protected ref ERR_ProbeBrokenLauncherGrant m_ERRFallbackAA = new ERR_ProbeBrokenLauncherGrant();
|
||||||
|
protected ref ERR_EnemyMineGrant m_ERRNativeMines = new ERR_EnemyMineGrant();
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "enemy-loadout") return;
|
||||||
|
m_ERRLoadoutDeadline = System.GetTickCount() + 240000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_LoadoutTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_LoadoutCheck(bool ok, string text)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: enemy-loadout " + text); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: enemy-loadout " + text, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_LoadoutTick); return false;
|
||||||
|
}
|
||||||
|
protected bool ERR_LoadoutWeapon(IEntity actor, ResourceName prefab)
|
||||||
|
{
|
||||||
|
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
|
||||||
|
array<IEntity> weapons = {}; if (manager) manager.GetWeaponsList(weapons);
|
||||||
|
IEntity weapon = ERR_EnemyLoadoutCatalog.Find(prefab, weapons, null);
|
||||||
|
return weapon && ERR_EnemyLoadoutCatalog.Loaded(weapon);
|
||||||
|
}
|
||||||
|
protected void ERR_LoadoutTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRLoadoutDeadline || Phase() == ERR_Phase.DEFEAT) { ERR_LoadoutCheck(false, "native equipment deadline"); return; }
|
||||||
|
if (Phase() < ERR_Phase.PRISON || !GetGame().GetPlayerController() || !GetGame().GetPlayerController().GetControlledEntity()) return;
|
||||||
|
if (m_ERRLoadoutStage == 0)
|
||||||
|
{
|
||||||
|
if (!ERR_LoadoutCheck(Settings().Set("antiAirChancePercent", 100) && Settings().Set("antiPersonnelMineChancePercent", 100), "percentage settings saved")) return;
|
||||||
|
m_ERRLoadoutGroup = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", ERR_Actors.Ground(Sites().Find("everon-prison-1").Group.GetOrigin() + "10 0 10")));
|
||||||
|
if (!ERR_LoadoutCheck(m_ERRLoadoutGroup != null, "native test group")) return;
|
||||||
|
Own(m_ERRLoadoutGroup); m_ERRLoadoutGroup.SetFaction(EnemyFaction());
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
array<AIAgent> agents = {}; m_ERRLoadoutGroup.GetAgents(agents);
|
||||||
|
if (agents.Count() != 2 || !agents[0].GetControlledEntity() || !agents[1].GetControlledEntity()) return;
|
||||||
|
IEntity first = agents[0].GetControlledEntity(); IEntity second = agents[1].GetControlledEntity();
|
||||||
|
foreach (AIAgent agent : agents) agent.DeactivateAI();
|
||||||
|
if (m_ERRLoadoutStage == 1)
|
||||||
|
{
|
||||||
|
m_ERRNativeAA.Check(this, m_ERRLoadoutGroup, first);
|
||||||
|
m_ERRFallbackAA.Check(this, m_ERRLoadoutGroup, second);
|
||||||
|
m_ERRNativeMines.Check(this, m_ERRLoadoutGroup, first);
|
||||||
|
ResourceName preferred = ERR_EnemyLoadoutCatalog.Launcher(this, m_ERRLoadoutGroup.GetFaction());
|
||||||
|
if (!ERR_LoadoutWeapon(first, preferred) || !ERR_LoadoutWeapon(second, ERR_EnemyLoadoutCatalog.FIM)) return;
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(first.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
array<IEntity> items = {}; inventory.GetItems(items); int mines;
|
||||||
|
foreach (IEntity item : items) if (item && item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)) mines++;
|
||||||
|
if (mines != 2) return;
|
||||||
|
ERR_LoadoutCheck(true, "preferred=" + preferred + "; fallback FIM physically equipped and loaded; two carried AP mines");
|
||||||
|
ERR_AdminMenu.Open(); m_ERRLoadoutStage = 2; return;
|
||||||
|
}
|
||||||
|
ERR_AdminMenu menu = ERR_AdminMenu.GetOpenMenu(); if (!menu || !menu.ERR_ProbeReady()) return;
|
||||||
|
if (m_ERRLoadoutStage == 2)
|
||||||
|
{
|
||||||
|
menu.ERR_ProbePage(1);
|
||||||
|
if (!ERR_LoadoutCheck(menu.ERR_ProbeNavigation(1), "HQ menu navigation")) return;
|
||||||
|
Print("ERR ADMIN CAPTURE PAGE: 1"); m_ERRLoadoutStage = 3; return;
|
||||||
|
}
|
||||||
|
if (m_ERRLoadoutStage++ < 8) 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -178,6 +178,17 @@ class ERR_NativeRadioDefenceProbe
|
|||||||
if (m_Step == 0)
|
if (m_Step == 0)
|
||||||
{
|
{
|
||||||
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Radio.Knowledge() == 2 && ERR_Actors.Player(m_Player), "identified radio and real controlled prisoner available")) return -1;
|
if (!Check(m_Mode.Phase() == ERR_Phase.RADIO_SEARCH && m_Radio.Knowledge() == 2 && ERR_Actors.Player(m_Player), "identified radio and real controlled prisoner available")) return -1;
|
||||||
|
ActionsManagerComponent manager = ActionsManagerComponent.Cast(m_Radio.GetOwner().FindComponent(ActionsManagerComponent));
|
||||||
|
if (!Check(manager != null, "radio has native action manager")) return -1;
|
||||||
|
UserActionContext context = manager.GetContext("ERR_Radio");
|
||||||
|
vector contextTransform[4];
|
||||||
|
if (!Check(context && context.GetTransformationWorld(contextTransform), "radio context has a valid world transform")) return -1;
|
||||||
|
PrintFormat("ERR RADIO CONTEXT: radio=%1 context=%2 radius=%3", m_Radio.Position(), contextTransform[3], context.GetRadius());
|
||||||
|
if (!Check(vector.Distance(contextTransform[3], m_Radio.Position()) < 0.6, "radio action context follows the generated radio rather than world origin")) return -1;
|
||||||
|
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
|
||||||
|
ERR_TuneRadioAction tune;
|
||||||
|
foreach (BaseUserAction action : actions) if (ERR_TuneRadioAction.Cast(action)) tune = ERR_TuneRadioAction.Cast(action);
|
||||||
|
if (!Check(tune && tune.CanBeShownScript(m_Player) && tune.CanBePerformedScript(m_Player), "native radio context contains the visible permitted extraction action")) return -1;
|
||||||
m_OriginalDuration = m_Mode.Settings().Get("radioDefenceSeconds");
|
m_OriginalDuration = m_Mode.Settings().Get("radioDefenceSeconds");
|
||||||
m_Player.GetWorldTransform(m_Original);
|
m_Player.GetWorldTransform(m_Original);
|
||||||
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
CharacterControllerComponent controller = CharacterControllerComponent.Cast(m_Player.FindComponent(CharacterControllerComponent));
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
// Uses the local native client's nearby-context collection. It does not inject
|
||||||
|
// a context or an action into the interaction handler.
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRRadioInteractionStage;
|
||||||
|
protected int m_ERRRadioInteractionDeadline;
|
||||||
|
protected float m_ERRRadioInteractionWait;
|
||||||
|
protected ERR_LocationComponent m_ERRInteractionRadio;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "radio-interaction") return;
|
||||||
|
m_ERRRadioInteractionDeadline = System.GetTickCount() + 150000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_RadioInteractionTick, 500, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_RadioInteractionCheck(bool ok, string detail)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: radio interaction " + detail); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: radio interaction " + detail, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_RadioInteractionTick); return false;
|
||||||
|
}
|
||||||
|
protected void ERR_RadioInteractionTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() > m_ERRRadioInteractionDeadline) { ERR_RadioInteractionCheck(false, "native nearby-context deadline"); return; }
|
||||||
|
if (!Active() || GetWorld().GetWorldTime() < m_ERRRadioInteractionWait) return;
|
||||||
|
PlayerController controller = GetGame().GetPlayerController();
|
||||||
|
if (!controller || !controller.GetControlledEntity()) return;
|
||||||
|
IEntity player = controller.GetControlledEntity();
|
||||||
|
if (m_ERRRadioInteractionStage == 0)
|
||||||
|
{
|
||||||
|
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
|
if (!ERR_RadioInteractionCheck(UnlockPrison(player), "normal prison escape")) return;
|
||||||
|
m_ERRInteractionRadio = Sites().Find("everon-radio-1").Location;
|
||||||
|
m_ERRInteractionRadio.Discover(0, 100);
|
||||||
|
vector transform[4]; player.GetWorldTransform(transform);
|
||||||
|
transform[3] = ERR_Actors.Ground(m_ERRInteractionRadio.Position() + ERR_CampTerrain.Rotate("0 0 -1.5", Sites().Find("everon-radio-1").Definition.Heading));
|
||||||
|
ChimeraCharacter.Cast(player).Teleport(transform);
|
||||||
|
CharacterControllerComponent character = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
|
||||||
|
character.SetHeadingAngle(Sites().Find("everon-radio-1").Definition.Heading * Math.DEG2RAD, false);
|
||||||
|
character.SetFreeLook(true, false, false);
|
||||||
|
character.GetAimingComponent().SetAimingRotation("0 -0.3 0");
|
||||||
|
m_ERRRadioInteractionWait = GetWorld().GetWorldTime() + 3000;
|
||||||
|
m_ERRRadioInteractionStage = 1; return;
|
||||||
|
}
|
||||||
|
ActionsManagerComponent manager = ActionsManagerComponent.Cast(m_ERRInteractionRadio.GetOwner().FindComponent(ActionsManagerComponent));
|
||||||
|
UserActionContext context;
|
||||||
|
if (manager) context = manager.GetContext("ERR_Radio");
|
||||||
|
vector world[4];
|
||||||
|
if (!ERR_RadioInteractionCheck(context && context.GetTransformationWorld(world), "native context has authored position")) return;
|
||||||
|
if (!ERR_RadioInteractionCheck(vector.Distance(world[3], m_ERRInteractionRadio.Position()) < 0.6, "context follows randomly placed radio")) return;
|
||||||
|
array<BaseUserAction> actions = {}; context.GetActionsList(actions);
|
||||||
|
ERR_TuneRadioAction tune;
|
||||||
|
foreach (BaseUserAction action : actions) if (ERR_TuneRadioAction.Cast(action)) tune = ERR_TuneRadioAction.Cast(action);
|
||||||
|
if (!ERR_RadioInteractionCheck(tune && tune.CanBeShown(player) && tune.CanBePerformed(player), "native action is showable and permitted")) return;
|
||||||
|
SCR_InteractionHandlerComponent handler = SCR_InteractionHandlerComponent.Cast(controller.FindComponent(SCR_InteractionHandlerComponent));
|
||||||
|
if (!ERR_RadioInteractionCheck(handler != null, "real client interaction handler exists")) return;
|
||||||
|
array<UserActionContext> nearby = {}; handler.GetNearbyAvailableContextList(nearby);
|
||||||
|
PrintFormat("ERR RADIO INTERACTION: player=%1 context=%2 radius=%3 nearby=%4 selected=%5 nearbyEnabled=%6 camera=%7", player.GetOrigin(), world[3], context.GetRadius(), nearby.Count(), handler.GetCurrentContext() == context, handler.GetNearbyCollectionEnabled(), GetGame().GetCameraManager().CurrentCamera().Type().ToString());
|
||||||
|
if (!ERR_RadioInteractionCheck(nearby.Contains(context) || handler.GetCurrentContext() == context, "native client collection discovers the radio in its furnished tent")) return;
|
||||||
|
tune.PerformAction(m_ERRInteractionRadio.GetOwner(), player);
|
||||||
|
if (!ERR_RadioInteractionCheck(Phase() == ERR_Phase.RADIO_DEFENCE, "discovered native action starts transmission")) return;
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_RadioInteractionTick);
|
||||||
|
Print("ERR SMOKE COMPLETE: native radio context collection and extraction action");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -1,39 +1,38 @@
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
|
// Explicit disposable-player fixture. Real damage, healing and the live defeat timer.
|
||||||
class ERR_NativeRecoveryProbe
|
class ERR_NativeRecoveryProbe
|
||||||
{
|
{
|
||||||
protected ERR_GameMode m_Mode;
|
protected ERR_GameMode m_Mode;
|
||||||
protected IEntity m_Player;
|
protected IEntity m_Player;
|
||||||
|
protected SCR_CharacterDamageManagerComponent m_Damage;
|
||||||
protected CharacterControllerComponent m_Controller;
|
protected CharacterControllerComponent m_Controller;
|
||||||
protected float m_Wait;
|
protected float m_Wait;
|
||||||
protected float m_RecoveryDeadline;
|
protected float m_HealingDeadline;
|
||||||
protected bool m_Stopped;
|
|
||||||
protected bool m_ChangedConsciousness;
|
|
||||||
protected bool m_InitiallyUnconscious;
|
|
||||||
protected int m_Step;
|
protected int m_Step;
|
||||||
protected int m_Changes;
|
protected bool m_Stopped;
|
||||||
void ERR_NativeRecoveryProbe(ERR_GameMode mode, IEntity player)
|
void ERR_NativeRecoveryProbe(ERR_GameMode mode, IEntity player)
|
||||||
{
|
{
|
||||||
m_Mode = mode; m_Player = player;
|
m_Mode = mode; m_Player = player;
|
||||||
|
m_Damage = SCR_CharacterDamageManagerComponent.Cast(player.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
m_Controller = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
|
m_Controller = CharacterControllerComponent.Cast(player.FindComponent(CharacterControllerComponent));
|
||||||
if (m_Controller) m_InitiallyUnconscious = m_Controller.IsUnconscious();
|
|
||||||
mode.Recovery().OnChanged().Insert(Changed);
|
|
||||||
mode.OnStop().Insert(Stop);
|
mode.OnStop().Insert(Stop);
|
||||||
}
|
}
|
||||||
protected void Changed(string message) { m_Changes++; }
|
|
||||||
void Stop()
|
void Stop()
|
||||||
{
|
{
|
||||||
if (m_Stopped) return;
|
if (m_Stopped) return;
|
||||||
m_Stopped = true;
|
m_Stopped = true;
|
||||||
if (m_Mode) m_Mode.OnStop().Remove(Stop);
|
if (m_Mode) m_Mode.OnStop().Remove(Stop);
|
||||||
if (m_Mode && m_Mode.Recovery()) m_Mode.Recovery().OnChanged().Remove(Changed);
|
if (m_Damage) m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||||
if (m_ChangedConsciousness && m_Controller && ERR_Actors.Alive(m_Player)) m_Controller.SetUnconscious(m_InitiallyUnconscious);
|
|
||||||
}
|
}
|
||||||
protected bool Check(bool ok, string detail)
|
protected bool Check(bool ok, string detail)
|
||||||
{
|
{
|
||||||
if (ok) { Print("ERR SMOKE PASS: recovery " + detail); return true; }
|
if (ok) { Print("ERR SMOKE PASS: recovery " + detail); return true; }
|
||||||
Stop();
|
Print("ERR SMOKE FAIL: recovery " + detail, LogLevel.ERROR); Stop(); return false;
|
||||||
Print("ERR SMOKE FAIL: recovery " + detail, LogLevel.ERROR);
|
}
|
||||||
return false;
|
protected void HaltPassiveRecovery()
|
||||||
|
{
|
||||||
|
array<ref SCR_PersistentDamageEffect> effects = m_Damage.GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect);
|
||||||
|
foreach (SCR_PersistentDamageEffect effect : effects) m_Damage.TerminateDamageEffect(effect);
|
||||||
}
|
}
|
||||||
int Tick(float now)
|
int Tick(float now)
|
||||||
{
|
{
|
||||||
@@ -41,54 +40,50 @@ class ERR_NativeRecoveryProbe
|
|||||||
if (now < m_Wait) return 0;
|
if (now < m_Wait) return 0;
|
||||||
if (m_Step == 0)
|
if (m_Step == 0)
|
||||||
{
|
{
|
||||||
m_Mode.Recovery().Tick();
|
if (!Check(m_Damage && m_Controller && m_Damage.ERR_IsRevivablePlayer(), "real admitted prisoner has player-only casualty protection")) return -1;
|
||||||
if (!Check(m_Controller && m_Mode.Recovery().Conscious() == 1 && m_Mode.RecoveryStatus().IsEmpty(), "real conscious prisoner is available without recovery warning")) return -1;
|
m_Damage.EnableDamageHandling(true);
|
||||||
array<string> replies = {};
|
// Each exceeds full native health, including blood loss and a head injury.
|
||||||
m_Mode.Admin().Execute(m_Mode, false, "mission abandon", replies);
|
m_Damage.GetDefaultHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||||
if (!Check(replies.Count() == 1 && replies[0].Contains("Administrator") && m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "unauthorized abandonment leaves round and result intact")) return -1;
|
m_Damage.GetBloodHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||||
m_Mode.Admin().Execute(m_Mode, true, "mission abandon now", replies);
|
m_Damage.GetHeadHitZone().HandleDamage(100000, EDamageType.TRUE, null);
|
||||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && m_Mode.EndReason().IsEmpty(), "malformed abandonment does not end a round")) return -1;
|
m_Wait = now + 1000; m_Step = 1; return 0;
|
||||||
m_ChangedConsciousness = true;
|
|
||||||
m_Controller.SetUnconscious(true);
|
|
||||||
m_Step = 1; m_Wait = now + 1000;
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
if (m_Step == 1)
|
if (m_Step == 1)
|
||||||
{
|
{
|
||||||
m_Mode.Recovery().Tick();
|
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Damage.GetState() != EDamageState.DESTROYED && m_Damage.GetBloodHitZone().GetDamageState() != EDamageState.DESTROYED, "otherwise lethal health, head and blood damage leave the same living unconscious body")) return -1;
|
||||||
if (!Check(ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Recovery().Incapacitated() == 1 && m_Mode.Recovery().Conscious() == 0, "native unconscious prisoner is counted as incapacitated, not dead")) return -1;
|
// Reset fixture injuries, then exercise treatment of an isolated knockout.
|
||||||
if (!Check(m_Mode.RecoveryStatus().Contains("No conscious prisoner") && m_Mode.Status().Contains("mission abandon") && ERR_ObjectiveText.Description(m_Mode).Contains(m_Mode.RecoveryStatus()), "server status and native objective copy explain the recovery state")) return -1;
|
m_Damage.FullHeal(); m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||||
if (!Check(m_Changes == 1, "recovery state publishes exactly one change")) return -1;
|
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||||
m_Mode.Recovery().Tick();
|
m_Wait = now + 35000; m_Step = 2; return 0;
|
||||||
if (!Check(m_Changes == 1, "unchanged sampling emits no duplicate recovery event")) return -1;
|
|
||||||
m_Step = 2; m_Wait = now + 35000;
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
if (m_Step == 2)
|
if (m_Step == 2)
|
||||||
{
|
{
|
||||||
if (!Check(m_Mode.Phase() == ERR_Phase.PRISON && ERR_Actors.Alive(m_Player) && !ERR_Actors.CanFight(m_Player) && m_Mode.Players().HasSurvivor(), "all-incapacitated player survives beyond ordinary defeat grace")) return -1;
|
if (!Check(m_Mode.Active() && m_Controller.IsUnconscious() && m_Damage.ERR_TreatmentInProgress(), "active native treatment keeps an all-unconscious round alive past 30 seconds")) return -1;
|
||||||
m_Controller.SetUnconscious(false);
|
m_Damage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||||
m_RecoveryDeadline = now + 12000;
|
SCR_PassiveHitZoneRegenDamageEffect healing = new SCR_PassiveHitZoneRegenDamageEffect();
|
||||||
m_Step = 3; m_Wait = now + 1000;
|
healing.SetAffectedHitZone(m_Damage.GetResilienceHitZone());
|
||||||
return 0;
|
healing.SetDamageType(EDamageType.HEALING); healing.SetDPS(-100);
|
||||||
|
healing.SetMaxDuration(10); healing.SkipRegenDelay(true);
|
||||||
|
m_Damage.AddDamageEffect(healing);
|
||||||
|
if (!Check(m_Damage.ERR_TreatmentInProgress(), "native resilience-healing effect is recognized as ongoing treatment")) return -1;
|
||||||
|
m_HealingDeadline = now + 12000;
|
||||||
|
m_Wait = now + 1000; m_Step = 3; return 0;
|
||||||
}
|
}
|
||||||
if (m_Step == 3)
|
if (m_Step == 3)
|
||||||
{
|
{
|
||||||
if (!ERR_Actors.CanFight(m_Player) && now < m_RecoveryDeadline) return 0;
|
PrintFormat("ERR RECOVERY HEAL: blood=%1 resilience=%2 state=%3 unconscious=%4", m_Damage.GetBloodHitZone().GetHealthScaled(), m_Damage.GetResilienceHitZone().GetHealthScaled(), m_Controller.GetLifeState(), m_Controller.IsUnconscious());
|
||||||
|
if (!ERR_Actors.CanFight(m_Player) && now < m_HealingDeadline) return 0;
|
||||||
|
if (!Check(ERR_Actors.CanFight(m_Player) && m_Mode.Active(), "native resilience healing restores consciousness on the original body")) return -1;
|
||||||
m_Mode.Recovery().Tick();
|
m_Mode.Recovery().Tick();
|
||||||
if (!Check(ERR_Actors.CanFight(m_Player) && m_Mode.RecoveryStatus().IsEmpty() && m_Mode.Recovery().Conscious() == 1 && m_Changes == 2, "native recovery clears the warning once")) return -1;
|
if (!Check(!m_Mode.Recovery().DefeatDue(now + 31000), "successful recovery resets the defeat timer")) return -1;
|
||||||
int id = GetGame().GetPlayerManager().GetPlayerIdFromControlledEntity(m_Player);
|
m_Damage.ForceUnconsciousness(); HaltPassiveRecovery();
|
||||||
ERR_PlayerLife life = m_Mode.Players().Get(id);
|
m_Wait = now + 1000; m_Step = 4; return 0;
|
||||||
if (!Check(life && life.UsedLife && life.Actor == m_Player, "recovery preserves original actor and spent life")) return -1;
|
|
||||||
m_Controller.SetUnconscious(true);
|
|
||||||
m_Step = 4; m_Wait = now + 1000;
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
if (m_Step == 4)
|
if (m_Step == 4)
|
||||||
{
|
{
|
||||||
m_Mode.Recovery().Tick();
|
m_Mode.Recovery().Tick();
|
||||||
if (!Check(!m_Mode.RecoveryStatus().IsEmpty() && m_Changes == 3, "second incapacitation restores warning before abandonment")) return -1;
|
if (!Check(ERR_Actors.Alive(m_Player) && m_Controller.IsUnconscious() && m_Mode.RecoveryStatus().Contains("30 uninterrupted seconds"), "stalled all-unconscious team starts a clear failure grace period")) return -1;
|
||||||
return 1;
|
return 1; // Parent now waits for the production timer, never forces defeat.
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
modded class ERR_HQ
|
||||||
|
{
|
||||||
|
bool ERR_ProbeSiteRoles(ERR_SiteRuntime site)
|
||||||
|
{
|
||||||
|
bool fixedGuard, mobile;
|
||||||
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
{
|
||||||
|
if (unit.Group == site.Group) fixedGuard = unit.ReservedForSupport && unit.HomeGuard && !unit.Eligible(m_Mode) && !unit.PendingOrder && !unit.Assignment;
|
||||||
|
if (site.Responders && unit.Group == site.Responders.Group) mobile = !unit.ReservedForSupport && !unit.HomeGuard && unit.Eligible(m_Mode) && unit.HomeId == site.Definition.Id;
|
||||||
|
}
|
||||||
|
return fixedGuard && mobile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRSiteStage, m_ERRSiteDeadline;
|
||||||
|
protected float m_ERRSiteWait;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "site-garrisons") return;
|
||||||
|
m_ERRSiteDeadline = System.GetTickCount() + 300000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_SiteGarrisonTick, 1000, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_SiteCheck(bool ok, string message)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: sites " + message); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: sites " + message, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SiteGarrisonTick); return false;
|
||||||
|
}
|
||||||
|
protected void ERR_SiteGarrisonTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRSiteDeadline || !PreparationError().IsEmpty()) { ERR_SiteCheck(false, "startup deadline/preparation: " + PreparationError()); return; }
|
||||||
|
if (!Active() || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||||
|
if (!player || GetWorld().GetWorldTime() < m_ERRSiteWait) return;
|
||||||
|
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
|
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||||
|
if (m_ERRSiteStage == 0)
|
||||||
|
{
|
||||||
|
int hospitals, camps, defenders, responders;
|
||||||
|
for (int i = 0; i < Sites().Count(); i++)
|
||||||
|
{
|
||||||
|
ERR_SiteRuntime site = Sites().At(i);
|
||||||
|
if (site.Definition.ResponseCount == 0) continue;
|
||||||
|
if (!ERR_SiteCheck(site.Definition.PlacementResolved && site.Props.Count() >= 4, site.Definition.Id + " randomized composition and scenery created")) return;
|
||||||
|
if (!ERR_SiteCheck(site.Group && site.Responders && site.Responders.Group && site.Group != site.Responders.Group && ForceLiving(site.Group) == 4 && ForceLiving(site.Responders.Group) == 2, site.Definition.Id + " distinct native 4 + 2 rosters")) return;
|
||||||
|
if (!ERR_SiteCheck(hq.ERR_ProbeSiteRoles(site), site.Definition.Id + " permanent defenders reserved, responders HQ eligible")) return;
|
||||||
|
if (!ERR_SiteCheck(Forces().FindGroup(site.Group) && Forces().FindGroup(site.Responders.Group), site.Definition.Id + " both finite forces registered")) return;
|
||||||
|
InventoryStorageManagerComponent storage = InventoryStorageManagerComponent.Cast(site.Container.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
array<IEntity> items = {}; if (storage) storage.GetItems(items);
|
||||||
|
int expected; foreach (ERR_SiteStock stock : site.Definition.Stock) expected += stock.Count;
|
||||||
|
if (hq.Logistics().Cache() == site.Container)
|
||||||
|
{
|
||||||
|
array<IEntity> courierStock = {}; hq.Logistics().InitialStock(courierStock);
|
||||||
|
expected += courierStock.Count();
|
||||||
|
if (!ERR_SiteCheck(hq.Logistics().Group() == site.Responders.Group, "mortar courier uses mobile team, never the permanent garrison")) return;
|
||||||
|
}
|
||||||
|
int topLevel;
|
||||||
|
foreach (IEntity stored : items)
|
||||||
|
{
|
||||||
|
InventoryItemComponent item = InventoryItemComponent.Cast(stored.FindComponent(InventoryItemComponent));
|
||||||
|
if (item && item.GetParentSlot() && item.GetParentSlot().GetStorage() && item.GetParentSlot().GetStorage().GetOwner() == site.Container) topLevel++;
|
||||||
|
}
|
||||||
|
// GetItems also enumerates native magazines/attachments inside supplied
|
||||||
|
// rifles. Only items directly stored in this cache belong to its stock list.
|
||||||
|
if (!ERR_SiteCheck(topLevel == expected, string.Format("%1 finite supplies inserted before admission: expected=%2 direct=%3 including nested=%4", site.Definition.Id, expected, topLevel, items.Count()))) return;
|
||||||
|
if (!ERR_SiteCheck(site.Location.Knowledge() == 0 && site.Location.ERR_ProbeGeneratedMarker(false), site.Definition.Id + " undiscovered marker hidden")) return;
|
||||||
|
site.Location.Discover(99, 0);
|
||||||
|
if (!ERR_SiteCheck(site.Location.Knowledge() == 1 && site.Location.ERR_ProbeGeneratedMarker(true), site.Definition.Id + " partial clue displays approximate marker")) return;
|
||||||
|
site.Location.Discover(99, 0);
|
||||||
|
if (!ERR_SiteCheck(site.Location.Knowledge() == 2 && site.Location.ERR_ProbeGeneratedMarker(true), site.Definition.Id + " exact marker at generated position")) return;
|
||||||
|
if (site.Definition.Kind == ERR_LocationKind.HOSPITAL)
|
||||||
|
{
|
||||||
|
hospitals++;
|
||||||
|
if (!ERR_SiteCheck(ERR_DiscoveryMarker.Icon(site.Definition.Kind, site.Definition.Id) == "medical", "hospital uses medical map icon")) return;
|
||||||
|
}
|
||||||
|
else camps++;
|
||||||
|
defenders += ForceLiving(site.Group); responders += ForceLiving(site.Responders.Group);
|
||||||
|
for (int j = 0; j < i; j++)
|
||||||
|
{
|
||||||
|
ERR_SiteRuntime previous = Sites().At(j);
|
||||||
|
if (previous.Definition.ResponseCount > 0 && !ERR_SiteCheck(vector.DistanceXZ(previous.Location.Position(), site.Location.Position()) >= 600, "field sites separated by at least 600 metres")) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ERR_SiteCheck(hospitals == 3 && camps == 5 && defenders == 32 && responders == 16, "three hospitals, five camps, 32 fixed defenders and 16 mobile soldiers")) return;
|
||||||
|
if (!ERR_SiteCheck(UnlockPrison(player), "normal prison release")) return;
|
||||||
|
m_ERRSiteStage = 1; m_ERRSiteWait = GetWorld().GetWorldTime() + 10000; return;
|
||||||
|
}
|
||||||
|
for (int index = 0; index < Sites().Count(); index++)
|
||||||
|
{
|
||||||
|
ERR_SiteRuntime garrison = Sites().At(index);
|
||||||
|
if (garrison.Responders && !ERR_SiteCheck(hq.ERR_ProbeSiteRoles(garrison), garrison.Definition.Id + " role separation survives live HQ ticks")) return;
|
||||||
|
}
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_SiteGarrisonTick);
|
||||||
|
Print("ERR SMOKE COMPLETE: randomized hospitals/camps, native rosters, finite stock, discovery and HQ role separation");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -101,7 +101,7 @@ class ERR_NativeTimeProbe
|
|||||||
bool savedSpeed = mode.Settings().Get("timeMultiplier") == 4 && replies[0].Contains("next round");
|
bool savedSpeed = mode.Settings().Get("timeMultiplier") == 4 && replies[0].Contains("next round");
|
||||||
bool idempotent = mode.Time().Start(mode) && Math.AbsFloat(manager.GetTimeOfTheDay() - currentHours) < 0.01 && manager.GetDayDuration() == currentDuration && mode.Time().StartMinutes() == appliedStart && mode.Time().Multiplier() == appliedSpeed;
|
bool idempotent = mode.Time().Start(mode) && Math.AbsFloat(manager.GetTimeOfTheDay() - currentHours) < 0.01 && manager.GetDayDuration() == currentDuration && mode.Time().StartMinutes() == appliedStart && mode.Time().Multiplier() == appliedSpeed;
|
||||||
mode.Admin().Execute(mode, true, "mission time show", replies);
|
mode.Admin().Execute(mode, true, "mission time show", replies);
|
||||||
bool shown = replies.Count() == 2 && replies[0].Contains("Clock") && replies[1] == "Next round: 06:15, 4x progression.";
|
bool shown = replies.Count() == 2 && replies[0].Contains("Clock") && replies[1] == "Next round: 06:15, 4 progression.";
|
||||||
bool restoredTime = mode.Settings().Set("startTimeMinutes", originalTime);
|
bool restoredTime = mode.Settings().Set("startTimeMinutes", originalTime);
|
||||||
bool restoredSpeed = mode.Settings().Set("timeMultiplier", originalSpeed);
|
bool restoredSpeed = mode.Settings().Set("timeMultiplier", originalSpeed);
|
||||||
if (!Check(savedTime && savedSpeed && restoredTime && restoredSpeed, "authorized settings persist and test restores originals")) return false;
|
if (!Check(savedTime && savedSpeed && restoredTime && restoredSpeed, "authorized settings persist and test restores originals")) return false;
|
||||||
|
|||||||
@@ -3,7 +3,29 @@ enum ERR_ObserverReason
|
|||||||
NONE,
|
NONE,
|
||||||
SPENT_LIFE,
|
SPENT_LIFE,
|
||||||
LATE_EXTRACTION,
|
LATE_EXTRACTION,
|
||||||
SPAWN_UNAVAILABLE
|
SPAWN_UNAVAILABLE,
|
||||||
|
UNCONSCIOUS
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_ObserverRules
|
||||||
|
{
|
||||||
|
static int Cycle(bool enabled, bool left, bool right, bool previousLeft, bool previousRight)
|
||||||
|
{
|
||||||
|
if (!enabled || left == right) return 0;
|
||||||
|
if (left && !previousLeft) return -1;
|
||||||
|
if (right && !previousRight) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
static bool Casualty(bool active, bool currentBody, bool alive, bool conscious)
|
||||||
|
{
|
||||||
|
return active && currentBody && alive && !conscious;
|
||||||
|
}
|
||||||
|
static bool View(int reason, bool active, bool alive, bool conscious)
|
||||||
|
{
|
||||||
|
if (!active || reason == ERR_ObserverReason.NONE) return false;
|
||||||
|
if (reason == ERR_ObserverReason.UNCONSCIOUS) return alive && !conscious;
|
||||||
|
return !alive;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Presentation only. Existing server admission and observer selection own state.
|
// Presentation only. Existing server admission and observer selection own state.
|
||||||
@@ -19,9 +41,14 @@ class ERR_ObserverGuidance
|
|||||||
message = "Extraction has started. New prisoners can join next round.";
|
message = "Extraction has started. New prisoners can join next round.";
|
||||||
else if (reason == ERR_ObserverReason.SPAWN_UNAVAILABLE)
|
else if (reason == ERR_ObserverReason.SPAWN_UNAVAILABLE)
|
||||||
message = "Your prisoner could not be spawned. You are spectating until the next round.";
|
message = "Your prisoner could not be spawned. You are spectating until the next round.";
|
||||||
|
else if (reason == ERR_ObserverReason.UNCONSCIOUS)
|
||||||
|
{
|
||||||
|
message = "You are unconscious. Your body remains where you fell and teammates can still treat you. Your view returns automatically when you wake up.";
|
||||||
|
if (!survivorAvailable) return message + " No conscious teammate is currently available; the camera stays near your body.";
|
||||||
|
}
|
||||||
if (!survivorAvailable)
|
if (!survivorAvailable)
|
||||||
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
return message + " No survivor is currently available to follow. The view will follow a survivor when one becomes available.";
|
||||||
return message + " Use /esc spectate next or /esc spectate previous to change who you follow.";
|
return message + " Press Left / Right arrow to change teammate (or /esc spectate next / /esc spectate previous). Use camera look controls to orbit and zoom controls to change distance.";
|
||||||
}
|
}
|
||||||
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
static bool Refresh(bool shown, int previousReason, int reason, bool previousAvailable, bool survivorAvailable)
|
||||||
{
|
{
|
||||||
@@ -33,11 +60,22 @@ class ERR_ObserverGuidance
|
|||||||
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
[BaseContainerProps(), SCR_BaseManualCameraComponentTitle()]
|
||||||
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
||||||
{
|
{
|
||||||
|
protected IEntity m_Target;
|
||||||
|
protected IEntity m_Vehicle;
|
||||||
|
protected float m_Yaw;
|
||||||
|
protected float m_Pitch = 20;
|
||||||
|
protected float m_Distance = 5;
|
||||||
|
protected bool m_LeftDown, m_RightDown;
|
||||||
override bool EOnCameraInit() { return true; }
|
override bool EOnCameraInit() { return true; }
|
||||||
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
override void EOnCameraFrame(SCR_ManualCameraParam param)
|
||||||
{
|
{
|
||||||
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
SCR_PlayerController controller = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
if (!controller) return;
|
if (!controller) return;
|
||||||
|
bool left = (Debug.KeyState(KeyCode.KC_LEFT) & 32768) != 0;
|
||||||
|
bool right = (Debug.KeyState(KeyCode.KC_RIGHT) & 32768) != 0;
|
||||||
|
int cycle = ERR_ObserverRules.Cycle(param.isManualInputEnabled, left, right, m_LeftDown, m_RightDown);
|
||||||
|
m_LeftDown = left; m_RightDown = right;
|
||||||
|
if (cycle != 0) controller.ERR_CycleObserver(cycle);
|
||||||
IEntity target = controller.ERR_ObservedEntity();
|
IEntity target = controller.ERR_ObservedEntity();
|
||||||
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
vector aim = controller.ERR_ObserverAnchor() + "0 1.5 0";
|
||||||
vector position = aim + "0 4 -6";
|
vector position = aim + "0 4 -6";
|
||||||
@@ -52,15 +90,32 @@ class ERR_FollowSurvivorCameraComponent : SCR_BaseManualCameraComponent
|
|||||||
vector lower, upper;
|
vector lower, upper;
|
||||||
vehicle.GetWorldBounds(lower, upper);
|
vehicle.GetWorldBounds(lower, upper);
|
||||||
aim = (lower + upper) * 0.5;
|
aim = (lower + upper) * 0.5;
|
||||||
position = aim + "0 5 0" - transform[2] * 10;
|
|
||||||
exclude = vehicle;
|
exclude = vehicle;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
target.GetWorldTransform(transform);
|
target.GetWorldTransform(transform);
|
||||||
aim = transform[3] + "0 1.5 0";
|
aim = transform[3] + "0 1.5 0";
|
||||||
position = aim + "0 1.5 0" - transform[2] * 4;
|
|
||||||
}
|
}
|
||||||
|
if (target != m_Target || vehicle != m_Vehicle)
|
||||||
|
{
|
||||||
|
vector angles = transform[2].VectorToAngles();
|
||||||
|
m_Yaw = angles[0] + 180; m_Pitch = 20; m_Distance = 5;
|
||||||
|
if (vehicle) m_Distance = 12;
|
||||||
|
m_Target = target; m_Vehicle = vehicle;
|
||||||
|
}
|
||||||
|
if (param.isManualInputEnabled)
|
||||||
|
{
|
||||||
|
m_Yaw += GetInputManager().GetActionValue("ManualCameraRotateYaw") * 90 * param.timeSlice;
|
||||||
|
m_Pitch = Math.Clamp(m_Pitch - GetInputManager().GetActionValue("ManualCameraRotatePitch") * 90 * param.timeSlice, 5, 75);
|
||||||
|
float scale = Math.Clamp(1 + GetInputManager().GetActionValue("ManualCameraZoom") * 12 * param.timeSlice, 0.5, 1.5);
|
||||||
|
float minimum = 2, maximum = 12;
|
||||||
|
if (vehicle) { minimum = 6; maximum = 25; }
|
||||||
|
m_Distance = Math.Clamp(m_Distance * scale, minimum, maximum);
|
||||||
|
}
|
||||||
|
float yaw = m_Yaw * Math.DEG2RAD, pitch = m_Pitch * Math.DEG2RAD;
|
||||||
|
vector offset = Vector(Math.Sin(yaw) * Math.Cos(pitch), Math.Sin(pitch), Math.Cos(yaw) * Math.Cos(pitch));
|
||||||
|
position = aim + offset * m_Distance;
|
||||||
}
|
}
|
||||||
TraceParam trace = new TraceParam();
|
TraceParam trace = new TraceParam();
|
||||||
trace.Start = aim;
|
trace.Start = aim;
|
||||||
@@ -106,7 +161,7 @@ modded class SCR_PlayerController
|
|||||||
void ERR_SetObserver(ERR_ObserverReason reason)
|
void ERR_SetObserver(ERR_ObserverReason reason)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer()) return;
|
if (!Replication.IsServer()) return;
|
||||||
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity())) return;
|
if (reason != ERR_ObserverReason.NONE && ERR_Actors.Alive(GetControlledEntity()) && reason != ERR_ObserverReason.UNCONSCIOUS) return;
|
||||||
m_ERRObserverReason = reason;
|
m_ERRObserverReason = reason;
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
|
if (mode && reason != ERR_ObserverReason.NONE) m_ERRObserverAnchor = mode.PrisonPosition();
|
||||||
@@ -132,9 +187,17 @@ modded class SCR_PlayerController
|
|||||||
}
|
}
|
||||||
void ERR_UpdateObserverServer(int direction = 0)
|
void ERR_UpdateObserverServer(int direction = 0)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
if (!Replication.IsServer()) return;
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
if (!mode || !mode.Active() || ERR_Actors.Alive(GetControlledEntity()))
|
IEntity body = GetControlledEntity();
|
||||||
|
bool casualty = mode && ERR_ObserverRules.Casualty(mode.Active(), mode.Players() && mode.Players().CurrentActor(body) != null, ERR_Actors.Alive(body), ERR_Actors.CanFight(body));
|
||||||
|
if (casualty && m_ERRObserverReason != ERR_ObserverReason.UNCONSCIOUS)
|
||||||
|
{
|
||||||
|
m_ERRObserverReason = ERR_ObserverReason.UNCONSCIOUS;
|
||||||
|
m_ERRObserverAnchor = body.GetOrigin();
|
||||||
|
}
|
||||||
|
if (m_ERRObserverReason == ERR_ObserverReason.NONE) return;
|
||||||
|
if (!mode || !mode.Active() || (ERR_Actors.Alive(body) && !casualty) || (m_ERRObserverReason == ERR_ObserverReason.UNCONSCIOUS && !casualty))
|
||||||
{
|
{
|
||||||
m_ERRObserverReason = ERR_ObserverReason.NONE;
|
m_ERRObserverReason = ERR_ObserverReason.NONE;
|
||||||
ERR_ReleaseObserved();
|
ERR_ReleaseObserved();
|
||||||
@@ -145,7 +208,7 @@ modded class SCR_PlayerController
|
|||||||
ERR_Actors.Players(survivors);
|
ERR_Actors.Players(survivors);
|
||||||
Faction friendly = mode.PlayerFaction();
|
Faction friendly = mode.PlayerFaction();
|
||||||
for (int i = survivors.Count() - 1; i >= 0; i--)
|
for (int i = survivors.Count() - 1; i >= 0; i--)
|
||||||
if (!friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
if (survivors[i] == body || (casualty && !ERR_Actors.CanFight(survivors[i])) || !friendly || !mode.Players() || !mode.Players().CurrentActor(survivors[i]) || SCR_FactionManager.SGetFaction(survivors[i]) != friendly) survivors.RemoveOrdered(i);
|
||||||
int index = survivors.Find(m_ERRServerObserved);
|
int index = survivors.Find(m_ERRServerObserved);
|
||||||
if (index < 0) index = 0;
|
if (index < 0) index = 0;
|
||||||
else index += direction;
|
else index += direction;
|
||||||
@@ -175,6 +238,7 @@ modded class SCR_PlayerController
|
|||||||
ERR_StreamObserved(vehicle, false);
|
ERR_StreamObserved(vehicle, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (casualty) m_ERRObserverAnchor = body.GetOrigin();
|
||||||
Replication.BumpMe();
|
Replication.BumpMe();
|
||||||
}
|
}
|
||||||
void ERR_CycleObserver(int direction)
|
void ERR_CycleObserver(int direction)
|
||||||
@@ -202,7 +266,7 @@ modded class SCR_PlayerController
|
|||||||
protected void ERR_UpdateObserverClient()
|
protected void ERR_UpdateObserverClient()
|
||||||
{
|
{
|
||||||
ERR_GameMode mode = ERR_GameMode.Get();
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
bool observe = mode && mode.Active() && m_ERRObserverReason != ERR_ObserverReason.NONE && !ERR_Actors.Alive(GetControlledEntity());
|
bool observe = mode && ERR_ObserverRules.View(m_ERRObserverReason, mode.Active(), ERR_Actors.Alive(GetControlledEntity()), ERR_Actors.CanFight(GetControlledEntity()));
|
||||||
if (!observe)
|
if (!observe)
|
||||||
{
|
{
|
||||||
ERR_CloseObserverCamera();
|
ERR_CloseObserverCamera();
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Player-only casualty protection. Damage still wounds; healing remains native.
|
||||||
|
class ERR_PlayerMedicalRules
|
||||||
|
{
|
||||||
|
static float AllowedDamage(float damage, float health, float maximum, float destroyedThreshold)
|
||||||
|
{
|
||||||
|
if (damage <= 0) return damage;
|
||||||
|
float floor = maximum * Math.Clamp(destroyedThreshold + 0.01, 0.01, 1);
|
||||||
|
return Math.Min(damage, Math.Max(0, health - floor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_CharacterDamageManagerComponent
|
||||||
|
{
|
||||||
|
protected bool m_ERRRevivablePlayer;
|
||||||
|
protected bool m_ERRKnockoutPending;
|
||||||
|
|
||||||
|
void ERR_EnableCasualtyProtection()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer()) return;
|
||||||
|
m_ERRRevivablePlayer = true;
|
||||||
|
SetPermitUnconsciousness(true, true);
|
||||||
|
}
|
||||||
|
bool ERR_IsRevivablePlayer() { return m_ERRRevivablePlayer; }
|
||||||
|
|
||||||
|
// Queue outside hitzone callbacks: changing another hitzone in them can re-enter
|
||||||
|
// the native damage pipeline. The lethal damage itself is clamped synchronously.
|
||||||
|
void ERR_RequestKnockout()
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || !m_ERRRevivablePlayer || m_ERRKnockoutPending) return;
|
||||||
|
CharacterControllerComponent controller = CharacterControllerComponent.Cast(GetOwner().FindComponent(CharacterControllerComponent));
|
||||||
|
if (controller && controller.IsUnconscious()) return;
|
||||||
|
m_ERRKnockoutPending = true;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_ApplyKnockout, 0, false);
|
||||||
|
}
|
||||||
|
protected void ERR_ApplyKnockout()
|
||||||
|
{
|
||||||
|
m_ERRKnockoutPending = false;
|
||||||
|
if (!m_ERRRevivablePlayer || !ERR_Actors.Alive(GetOwner())) return;
|
||||||
|
ForceUnconsciousness();
|
||||||
|
ERR_HQ.Event("PLAYER CASUALTY", "Lethal injury prevented; prisoner remains alive and can receive medical treatment.");
|
||||||
|
}
|
||||||
|
override void Kill(notnull Instigator instigator)
|
||||||
|
{
|
||||||
|
if (!m_ERRRevivablePlayer) { super.Kill(instigator); return; }
|
||||||
|
ERR_RequestKnockout();
|
||||||
|
}
|
||||||
|
override protected bool IsIndefinitelyUnconscious(bool onlyPlayers = true)
|
||||||
|
{
|
||||||
|
if (m_ERRRevivablePlayer) return false;
|
||||||
|
return super.IsIndefinitelyUnconscious(onlyPlayers);
|
||||||
|
}
|
||||||
|
bool ERR_TreatmentInProgress()
|
||||||
|
{
|
||||||
|
if (m_aBeingHealedGroup && !m_aBeingHealedGroup.IsEmpty()) return true;
|
||||||
|
array<ref SCR_PersistentDamageEffect> effects = GetAllPersistentEffectsOfType(SCR_DotDamageEffect, true);
|
||||||
|
foreach (SCR_PersistentDamageEffect effect : effects)
|
||||||
|
{
|
||||||
|
DotDamageEffect dot = DotDamageEffect.Cast(effect);
|
||||||
|
// Includes saline and ACE epinephrine. Passive regeneration is checked by
|
||||||
|
// observed progress, so a stalled regeneration effect cannot block defeat.
|
||||||
|
if (dot && dot.GetDamageType() == EDamageType.HEALING && dot.GetDPS() < 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_HitZone
|
||||||
|
{
|
||||||
|
override float ComputeEffectiveDamage(notnull BaseDamageContext damageContext, bool isDOT)
|
||||||
|
{
|
||||||
|
float damage = super.ComputeEffectiveDamage(damageContext, isDOT);
|
||||||
|
if (!Replication.IsServer() || damage <= 0 || SCR_CharacterResilienceHitZone.Cast(this)) return damage;
|
||||||
|
SCR_CharacterDamageManagerComponent manager = SCR_CharacterDamageManagerComponent.Cast(GetHitZoneContainer());
|
||||||
|
if (!manager || !manager.ERR_IsRevivablePlayer()) return damage;
|
||||||
|
float allowed = ERR_PlayerMedicalRules.AllowedDamage(damage, GetHealth(), GetMaxHealth(), GetDamageStateThreshold(EDamageState.DESTROYED));
|
||||||
|
if (allowed < damage) manager.ERR_RequestKnockout();
|
||||||
|
return allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// Observation only: unconsciousness never consumes a life or ends a round here.
|
// Observe recovery, retaining native treatment and the same living casualty.
|
||||||
class ERR_RecoveryRules
|
class ERR_RecoveryRules
|
||||||
{
|
{
|
||||||
// 0 absent/dead, 1 reserved or awaiting handoff, 2 conscious, 3 incapacitated.
|
// 0 absent/dead, 1 reserved or awaiting handoff, 2 conscious, 3 incapacitated.
|
||||||
@@ -13,6 +13,20 @@ class ERR_RecoveryRules
|
|||||||
{
|
{
|
||||||
return conscious == 0 && incapacitated > 0;
|
return conscious == 0 && incapacitated > 0;
|
||||||
}
|
}
|
||||||
|
static bool Stalled(int conscious, int incapacitated, int reservedRescuers, int recovering, bool connected)
|
||||||
|
{
|
||||||
|
return connected && conscious == 0 && incapacitated > 0 && reservedRescuers == 0 && recovering == 0;
|
||||||
|
}
|
||||||
|
static bool DefeatDue(float since, float now)
|
||||||
|
{
|
||||||
|
return since >= 0 && now >= since && now - since >= 30000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_RecoverySample
|
||||||
|
{
|
||||||
|
float Blood;
|
||||||
|
float Resilience;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ERR_RecoveryService
|
class ERR_RecoveryService
|
||||||
@@ -22,6 +36,8 @@ class ERR_RecoveryService
|
|||||||
protected int m_Conscious;
|
protected int m_Conscious;
|
||||||
protected int m_Incapacitated;
|
protected int m_Incapacitated;
|
||||||
protected int m_Reserved;
|
protected int m_Reserved;
|
||||||
|
protected float m_StalledSince = -1;
|
||||||
|
protected ref map<IEntity, ref ERR_RecoverySample> m_Samples = new map<IEntity, ref ERR_RecoverySample>();
|
||||||
protected string m_Message;
|
protected string m_Message;
|
||||||
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
protected ref ScriptInvoker m_OnChanged = new ScriptInvoker();
|
||||||
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
void ERR_RecoveryService(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
@@ -30,11 +46,13 @@ class ERR_RecoveryService
|
|||||||
int Incapacitated() { return m_Incapacitated; }
|
int Incapacitated() { return m_Incapacitated; }
|
||||||
int Reserved() { return m_Reserved; }
|
int Reserved() { return m_Reserved; }
|
||||||
string Message() { return m_Message; }
|
string Message() { return m_Message; }
|
||||||
|
bool DefeatDue(float now) { return ERR_RecoveryRules.DefeatDue(m_StalledSince, now); }
|
||||||
|
|
||||||
void Tick()
|
void Tick()
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
if (!Replication.IsServer() || m_Stopped || !m_Mode || !m_Mode.Active()) return;
|
||||||
int conscious, incapacitated, reserved;
|
int conscious, incapacitated, reserved, reservedRescuers, recovering;
|
||||||
|
map<IEntity, ref ERR_RecoverySample> nextSamples = new map<IEntity, ref ERR_RecoverySample>();
|
||||||
array<ref ERR_PlayerLife> lives = {};
|
array<ref ERR_PlayerLife> lives = {};
|
||||||
m_Mode.Players().Participants(lives);
|
m_Mode.Players().Participants(lives);
|
||||||
foreach (ERR_PlayerLife life : lives)
|
foreach (ERR_PlayerLife life : lives)
|
||||||
@@ -42,19 +60,40 @@ class ERR_RecoveryService
|
|||||||
IEntity controlled = null;
|
IEntity controlled = null;
|
||||||
if (life.Connected) controlled = GetGame().GetPlayerManager().GetPlayerControlledEntity(life.ConnectionId);
|
if (life.Connected) controlled = GetGame().GetPlayerManager().GetPlayerControlledEntity(life.ConnectionId);
|
||||||
int kind = ERR_RecoveryRules.SurvivorKind(ERR_Actors.Alive(life.Actor), life.Connected, controlled == life.Actor, ERR_Actors.CanFight(life.Actor));
|
int kind = ERR_RecoveryRules.SurvivorKind(ERR_Actors.Alive(life.Actor), life.Connected, controlled == life.Actor, ERR_Actors.CanFight(life.Actor));
|
||||||
if (kind == 1) reserved++;
|
if (kind == 1)
|
||||||
|
{
|
||||||
|
reserved++;
|
||||||
|
if (ERR_Actors.CanFight(life.Actor)) reservedRescuers++;
|
||||||
|
else incapacitated++;
|
||||||
|
}
|
||||||
else if (kind == 2) conscious++;
|
else if (kind == 2) conscious++;
|
||||||
else if (kind == 3) incapacitated++;
|
else if (kind == 3) incapacitated++;
|
||||||
|
if (kind == 0 || kind == 2) continue;
|
||||||
|
SCR_CharacterDamageManagerComponent medical = SCR_CharacterDamageManagerComponent.Cast(life.Actor.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
|
if (!medical) continue;
|
||||||
|
ERR_RecoverySample sample = new ERR_RecoverySample();
|
||||||
|
if (medical.GetBloodHitZone()) sample.Blood = medical.GetBloodHitZone().GetHealth();
|
||||||
|
if (medical.GetResilienceHitZone()) sample.Resilience = medical.GetResilienceHitZone().GetHealth();
|
||||||
|
ERR_RecoverySample previous = m_Samples.Get(life.Actor);
|
||||||
|
bool improving = previous && (sample.Blood > previous.Blood + 0.001 || sample.Resilience > previous.Resilience + 0.001);
|
||||||
|
if (improving || medical.ERR_TreatmentInProgress()) recovering++;
|
||||||
|
nextSamples.Insert(life.Actor, sample);
|
||||||
}
|
}
|
||||||
|
m_Samples = nextSamples;
|
||||||
m_Conscious = conscious;
|
m_Conscious = conscious;
|
||||||
m_Incapacitated = incapacitated;
|
m_Incapacitated = incapacitated;
|
||||||
m_Reserved = reserved;
|
m_Reserved = reserved;
|
||||||
|
float now = GetGame().GetWorld().GetWorldTime();
|
||||||
|
bool stalled = ERR_RecoveryRules.Stalled(conscious, incapacitated, reservedRescuers, recovering, GetGame().GetPlayerManager().GetPlayerCount() > 0);
|
||||||
|
if (!stalled) m_StalledSince = -1;
|
||||||
|
else if (m_StalledSince < 0 || now < m_StalledSince) m_StalledSince = now;
|
||||||
string message;
|
string message;
|
||||||
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
|
if (ERR_RecoveryRules.NeedsRescuer(conscious, incapacitated))
|
||||||
{
|
{
|
||||||
message = "No conscious prisoner is currently available to help. Natural recovery and treatment can still continue.";
|
message = "No conscious prisoner is currently available to help.";
|
||||||
if (reserved > 0) message += " A surviving prisoner's reconnection may provide help.";
|
if (recovering > 0) message += " Treatment or recovery is progressing; the round continues.";
|
||||||
message += " An administrator can abandon this round with /esc mission abandon.";
|
else if (reservedRescuers > 0) message += " A conscious prisoner's reconnection may provide help.";
|
||||||
|
else message += " Everyone is unconscious with no treatment or recovery progressing. The mission fails after 30 uninterrupted seconds in this state.";
|
||||||
}
|
}
|
||||||
Publish(message);
|
Publish(message);
|
||||||
}
|
}
|
||||||
@@ -69,6 +108,7 @@ class ERR_RecoveryService
|
|||||||
if (!Replication.IsServer() || m_Stopped) return;
|
if (!Replication.IsServer() || m_Stopped) return;
|
||||||
m_Stopped = true;
|
m_Stopped = true;
|
||||||
m_Conscious = 0; m_Incapacitated = 0; m_Reserved = 0;
|
m_Conscious = 0; m_Incapacitated = 0; m_Reserved = 0;
|
||||||
|
m_StalledSince = -1; m_Samples.Clear();
|
||||||
Publish("");
|
Publish("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
m_Started = true;
|
m_Started = true;
|
||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
string historicalFixture;
|
string historicalFixture;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
if (System.GetCLIParam("errSmokeOutcome", historicalFixture) && historicalFixture != "site-garrisons" && historicalFixture != "generated-sites" && historicalFixture != "opening-tools" && historicalFixture != "airfield-markers" && historicalFixture != "air-recon" && historicalFixture != "air-logistics" && historicalFixture != "air-assault")
|
||||||
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
m_Definition = "{ACA98B44E2173605}Configs/ERR/Tests/LegacySites.conf";
|
||||||
#endif
|
#endif
|
||||||
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
m_RuntimeDefinition = ERR_ScenarioDefinition.Cast(SCR_BaseContainerTools.CreateInstanceFromPrefab(m_Definition));
|
||||||
@@ -38,7 +38,7 @@ class ERR_ScenarioComponent : ScriptComponent
|
|||||||
#ifdef WORKBENCH
|
#ifdef WORKBENCH
|
||||||
// Existing deterministic probes intentionally use authored coordinates.
|
// Existing deterministic probes intentionally use authored coordinates.
|
||||||
string fixture;
|
string fixture;
|
||||||
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
if (System.GetCLIParam("errSmokeOutcome", fixture) && fixture != "site-garrisons" && fixture != "generated-sites" && fixture != "airfield-markers" && fixture != "air-recon" && fixture != "air-logistics" && fixture != "air-assault")
|
||||||
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
foreach (ERR_SiteDefinition fixedSite : m_RuntimeDefinition.Sites) fixedSite.RandomClear = false;
|
||||||
#endif
|
#endif
|
||||||
m_Generated = new ERR_GeneratedSites();
|
m_Generated = new ERR_GeneratedSites();
|
||||||
|
|||||||
@@ -9,15 +9,16 @@ class ERR_Settings
|
|||||||
|
|
||||||
static array<string> Keys()
|
static array<string> Keys()
|
||||||
{
|
{
|
||||||
return {"reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "stingerChanceOneIn", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
return {"keepWeaponOnUnconscious", "reportIntervalSeconds", "supportSquadLimit", "responseRadiusMetres", "aiIntelChancePercent", "intelRevealChancePercent", "radioDefenceSeconds", "startTimeMinutes", "timeMultiplier", "startDateCode", "weatherProfile", "antiAirChancePercent", "antiPersonnelMineChancePercent", "automaticConvoyEnabled", "convoyCount", "heliTimerMinutes"};
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Valid(string key, int value)
|
static bool Valid(string key, int value)
|
||||||
{
|
{
|
||||||
|
if (key == "keepWeaponOnUnconscious") return value == 0 || value == 1;
|
||||||
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
if (key == "heliTimerMinutes") return value >= 1 && value <= 1440;
|
||||||
if (key == "convoyCount") return value >= 0 && value <= 20;
|
if (key == "convoyCount") return value >= 0 && value <= 20;
|
||||||
if (key == "automaticConvoyEnabled") return value == 0 || value == 1;
|
if (key == "automaticConvoyEnabled") return value == 0 || value == 1;
|
||||||
if (key == "stingerChanceOneIn") return value >= 0 && value <= 10000;
|
if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent") return value >= 0 && value <= 100;
|
||||||
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
|
if (key == "reportIntervalSeconds") return value >= 5 && value <= 60;
|
||||||
if (key == "supportSquadLimit") return value >= 0 && value <= 8;
|
if (key == "supportSquadLimit") return value >= 0 && value <= 8;
|
||||||
if (key == "responseRadiusMetres") return value >= 500 && value <= 5000;
|
if (key == "responseRadiusMetres") return value >= 500 && value <= 5000;
|
||||||
@@ -50,7 +51,7 @@ class ERR_Settings
|
|||||||
// Saved modes are distinct from validated concrete gameplay values.
|
// Saved modes are distinct from validated concrete gameplay values.
|
||||||
static bool Randomizable(string key)
|
static bool Randomizable(string key)
|
||||||
{
|
{
|
||||||
return key == "heliTimerMinutes" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent" || key == "radioDefenceSeconds" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile";
|
return key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "heliTimerMinutes" || key == "convoyCount" || key == "reportIntervalSeconds" || key == "supportSquadLimit" || key == "responseRadiusMetres" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent" || key == "radioDefenceSeconds" || key == "startTimeMinutes" || key == "timeMultiplier" || key == "startDateCode" || key == "weatherProfile";
|
||||||
}
|
}
|
||||||
static bool ValidSelection(string key, int value)
|
static bool ValidSelection(string key, int value)
|
||||||
{
|
{
|
||||||
@@ -72,7 +73,7 @@ class ERR_Settings
|
|||||||
else if (key == "reportIntervalSeconds") { minimum = 5; maximum = 60; }
|
else if (key == "reportIntervalSeconds") { minimum = 5; maximum = 60; }
|
||||||
else if (key == "supportSquadLimit") maximum = 8;
|
else if (key == "supportSquadLimit") maximum = 8;
|
||||||
else if (key == "responseRadiusMetres") { minimum = 500; maximum = 5000; }
|
else if (key == "responseRadiusMetres") { minimum = 500; maximum = 5000; }
|
||||||
else if (key == "aiIntelChancePercent" || key == "intelRevealChancePercent") maximum = 100;
|
else if (key == "antiAirChancePercent" || key == "antiPersonnelMineChancePercent" || key == "aiIntelChancePercent" || key == "intelRevealChancePercent") maximum = 100;
|
||||||
else if (key == "radioDefenceSeconds") { minimum = 30; maximum = 1800; }
|
else if (key == "radioDefenceSeconds") { minimum = 30; maximum = 1800; }
|
||||||
else if (key == "startTimeMinutes") maximum = 1439;
|
else if (key == "startTimeMinutes") maximum = 1439;
|
||||||
else if (key == "timeMultiplier") { minimum = 1; maximum = 24; }
|
else if (key == "timeMultiplier") { minimum = 1; maximum = 24; }
|
||||||
@@ -96,10 +97,12 @@ class ERR_Settings
|
|||||||
|
|
||||||
static int AddedDefault(string key)
|
static int AddedDefault(string key)
|
||||||
{
|
{
|
||||||
|
if (key == "keepWeaponOnUnconscious") return 1;
|
||||||
if (key == "heliTimerMinutes") return 15;
|
if (key == "heliTimerMinutes") return 15;
|
||||||
if (key == "convoyCount") return 3;
|
if (key == "convoyCount") return 3;
|
||||||
if (key == "automaticConvoyEnabled") return 1;
|
if (key == "automaticConvoyEnabled") return 1;
|
||||||
if (key == "stingerChanceOneIn") return 15;
|
if (key == "antiAirChancePercent") return 7;
|
||||||
|
if (key == "antiPersonnelMineChancePercent") return 60;
|
||||||
if (key == "startTimeMinutes") return 840;
|
if (key == "startTimeMinutes") return 840;
|
||||||
if (key == "timeMultiplier") return 1;
|
if (key == "timeMultiplier") return 1;
|
||||||
if (key == "startDateCode" || key == "weatherProfile") return 0;
|
if (key == "startDateCode" || key == "weatherProfile") return 0;
|
||||||
@@ -109,6 +112,7 @@ class ERR_Settings
|
|||||||
void ERR_Settings(string path = PATH)
|
void ERR_Settings(string path = PATH)
|
||||||
{
|
{
|
||||||
m_Path = path;
|
m_Path = path;
|
||||||
|
m_Values.Set("keepWeaponOnUnconscious", AddedDefault("keepWeaponOnUnconscious"));
|
||||||
m_Values.Set("reportIntervalSeconds", 10);
|
m_Values.Set("reportIntervalSeconds", 10);
|
||||||
m_Values.Set("supportSquadLimit", 4);
|
m_Values.Set("supportSquadLimit", 4);
|
||||||
m_Values.Set("responseRadiusMetres", 2000);
|
m_Values.Set("responseRadiusMetres", 2000);
|
||||||
@@ -119,7 +123,8 @@ class ERR_Settings
|
|||||||
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
m_Values.Set("timeMultiplier", AddedDefault("timeMultiplier"));
|
||||||
m_Values.Set("startDateCode", AddedDefault("startDateCode"));
|
m_Values.Set("startDateCode", AddedDefault("startDateCode"));
|
||||||
m_Values.Set("weatherProfile", AddedDefault("weatherProfile"));
|
m_Values.Set("weatherProfile", AddedDefault("weatherProfile"));
|
||||||
m_Values.Set("stingerChanceOneIn", AddedDefault("stingerChanceOneIn"));
|
m_Values.Set("antiAirChancePercent", AddedDefault("antiAirChancePercent"));
|
||||||
|
m_Values.Set("antiPersonnelMineChancePercent", AddedDefault("antiPersonnelMineChancePercent"));
|
||||||
m_Values.Set("automaticConvoyEnabled", AddedDefault("automaticConvoyEnabled"));
|
m_Values.Set("automaticConvoyEnabled", AddedDefault("automaticConvoyEnabled"));
|
||||||
m_Values.Set("convoyCount", AddedDefault("convoyCount"));
|
m_Values.Set("convoyCount", AddedDefault("convoyCount"));
|
||||||
m_Values.Set("heliTimerMinutes", AddedDefault("heliTimerMinutes"));
|
m_Values.Set("heliTimerMinutes", AddedDefault("heliTimerMinutes"));
|
||||||
@@ -142,6 +147,17 @@ class ERR_Settings
|
|||||||
values = new map<string, int>();
|
values = new map<string, int>();
|
||||||
foreach (string key : Keys())
|
foreach (string key : Keys())
|
||||||
{
|
{
|
||||||
|
// Migrate the former one-in-N setting to the nearest integer percent.
|
||||||
|
if (key == "antiAirChancePercent" && !json.DoesKeyExist(key) && json.DoesKeyExist("stingerChanceOneIn"))
|
||||||
|
{
|
||||||
|
float oldNumber;
|
||||||
|
if (!json.ReadValue("stingerChanceOneIn", oldNumber)) return false;
|
||||||
|
int oldChance = Math.Round(oldNumber);
|
||||||
|
if (oldNumber != oldChance || oldChance < 0 || oldChance > 10000) return false;
|
||||||
|
int percent = 0;
|
||||||
|
if (oldChance > 0) percent = Math.Round(100.0 / oldChance);
|
||||||
|
values.Set(key, percent); continue;
|
||||||
|
}
|
||||||
// Only newly introduced keys may be absent in an older valid file.
|
// Only newly introduced keys may be absent in an older valid file.
|
||||||
if (!json.DoesKeyExist(key) && AddedDefault(key) >= 0)
|
if (!json.DoesKeyExist(key) && AddedDefault(key) >= 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class ERR_SiteDefinition
|
|||||||
ResourceName Prefab;
|
ResourceName Prefab;
|
||||||
[Attribute("0 0 0", UIWidgets.Coords)]
|
[Attribute("0 0 0", UIWidgets.Coords)]
|
||||||
vector Position;
|
vector Position;
|
||||||
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, radio or mortar)")]
|
[Attribute("0", UIWidgets.CheckBox, "Choose dry clear terrain once per round (prison, radio, mortar, hospital, depot or camp)")]
|
||||||
bool RandomClear;
|
bool RandomClear;
|
||||||
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
[Attribute("0", UIWidgets.EditBox, "Composition heading in degrees")]
|
||||||
float Heading;
|
float Heading;
|
||||||
@@ -75,6 +75,12 @@ class ERR_SiteDefinition
|
|||||||
[Attribute()] ref array<ref ERR_SitePatrolPoint> Patrol;
|
[Attribute()] ref array<ref ERR_SitePatrolPoint> Patrol;
|
||||||
[Attribute("0", UIWidgets.CheckBox, "Populate a finite squad from the faction catalog") ]
|
[Attribute("0", UIWidgets.CheckBox, "Populate a finite squad from the faction catalog") ]
|
||||||
bool Guarded;
|
bool Guarded;
|
||||||
|
[Attribute("0", UIWidgets.EditBox, "Permanent defenders (0 uses the full faction group; otherwise 1-8)")]
|
||||||
|
int GuardCount;
|
||||||
|
[Attribute("0", UIWidgets.EditBox, "Separate mobile soldiers HQ may send hunting (0-8); defenders remain at home")]
|
||||||
|
int ResponseCount;
|
||||||
|
[Attribute("-12 0 0", UIWidgets.Coords, "Mobile patrol assembly offset")]
|
||||||
|
vector ResponseOffset;
|
||||||
[Attribute("20 0 0", UIWidgets.Coords)]
|
[Attribute("20 0 0", UIWidgets.Coords)]
|
||||||
vector GuardOffset;
|
vector GuardOffset;
|
||||||
[Attribute("", UIWidgets.ResourceNamePicker, "Finite inventory container", "et")]
|
[Attribute("", UIWidgets.ResourceNamePicker, "Finite inventory container", "et")]
|
||||||
@@ -117,10 +123,14 @@ class ERR_ScenarioDefinition
|
|||||||
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
|
if (!site || site.Id.IsEmpty() || ids.Contains(site.Id) || site.Prefab.IsEmpty()) return false;
|
||||||
if (site.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) return false;
|
if (site.RequireAirfield && site.Kind != ERR_LocationKind.EXTRACTION) return false;
|
||||||
if (site.Kind == ERR_LocationKind.RADIO && !site.Guarded && site.Guards.IsEmpty()) return false;
|
if (site.Kind == ERR_LocationKind.RADIO && !site.Guarded && site.Guards.IsEmpty()) return false;
|
||||||
if (site.RandomClear && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR) return false;
|
if (site.RandomClear && site.Kind != ERR_LocationKind.PRISON && site.Kind != ERR_LocationKind.RADIO && site.Kind != ERR_LocationKind.MORTAR && site.Kind != ERR_LocationKind.HOSPITAL && site.Kind != ERR_LocationKind.OUTPOST && site.Kind != ERR_LocationKind.DEPOT) return false;
|
||||||
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
|
if (site.Label.IsEmpty() || site.Label.Length() > 64) return false;
|
||||||
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
|
if (!ERR_SiteDiscoveryRules.ValidRadius(site.Kind, site.DiscoverRadius)) return false;
|
||||||
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST;
|
bool optionalSite = site.Kind == ERR_LocationKind.DEPOT || site.Kind == ERR_LocationKind.OUTPOST || site.Kind == ERR_LocationKind.HOSPITAL;
|
||||||
|
if (site.GuardCount < 0 || site.GuardCount > 8 || site.ResponseCount < 0 || site.ResponseCount > 8) return false;
|
||||||
|
if ((site.GuardCount > 0 || site.ResponseCount > 0) && (!optionalSite || !site.Guarded)) return false;
|
||||||
|
if (site.ResponseOffset.Length() > 40 || Math.AbsFloat(site.ResponseOffset[1]) > 0.01) return false;
|
||||||
|
if (site.ResponseCount > 0) groups++;
|
||||||
if (!optionalSite && site.RevealOnEscape) return false;
|
if (!optionalSite && site.RevealOnEscape) return false;
|
||||||
if (site.Transport)
|
if (site.Transport)
|
||||||
{
|
{
|
||||||
@@ -137,7 +147,7 @@ class ERR_ScenarioDefinition
|
|||||||
foreach (ERR_SiteProp prop : site.Props)
|
foreach (ERR_SiteProp prop : site.Props)
|
||||||
if (!prop || prop.Prefab.IsEmpty() || prop.Offset.Length() > 40) return false;
|
if (!prop || prop.Prefab.IsEmpty() || prop.Offset.Length() > 40) return false;
|
||||||
}
|
}
|
||||||
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.MORTAR) return false;
|
if (site.Kind < ERR_LocationKind.PRISON || site.Kind > ERR_LocationKind.HOSPITAL) return false;
|
||||||
ids.Insert(site.Id);
|
ids.Insert(site.Id);
|
||||||
if (site.Kind == ERR_LocationKind.PRISON) prisons++;
|
if (site.Kind == ERR_LocationKind.PRISON) prisons++;
|
||||||
if (site.Kind == ERR_LocationKind.RADIO) radios++;
|
if (site.Kind == ERR_LocationKind.RADIO) radios++;
|
||||||
@@ -181,6 +191,6 @@ class ERR_ScenarioDefinition
|
|||||||
sources++;
|
sources++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prisons == 1 && radios == 1 && extractions == 1 && groups <= 8 && items <= 256 && props <= 64 && transports <= 4 && sources >= MinimumRadioSources && maps >= 1;
|
return prisons == 1 && radios == 1 && extractions == 1 && groups <= 24 && items <= 512 && props <= 128 && transports <= 4 && sources >= MinimumRadioSources && maps >= 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class ERR_SiteDiscoveryRules
|
|||||||
{
|
{
|
||||||
if (radius < 0 || radius > 60) return false;
|
if (radius < 0 || radius > 60) return false;
|
||||||
if (kind == ERR_LocationKind.RADIO) return radius <= RadioRadiusLimit();
|
if (kind == ERR_LocationKind.RADIO) return radius <= RadioRadiusLimit();
|
||||||
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.MORTAR) return true;
|
if (kind == ERR_LocationKind.DEPOT || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.MORTAR || kind == ERR_LocationKind.HOSPITAL) return true;
|
||||||
return (kind == ERR_LocationKind.PRISON || kind == ERR_LocationKind.EXTRACTION) && radius == 0;
|
return (kind == ERR_LocationKind.PRISON || kind == ERR_LocationKind.EXTRACTION) && radius == 0;
|
||||||
}
|
}
|
||||||
static bool InRange(float radius, float horizontalDistance, float verticalDistance)
|
static bool InRange(float radius, float horizontalDistance, float verticalDistance)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class ERR_SiteRuntime
|
|||||||
ERR_LocationComponent Location;
|
ERR_LocationComponent Location;
|
||||||
IEntity Container;
|
IEntity Container;
|
||||||
SCR_AIGroup Group;
|
SCR_AIGroup Group;
|
||||||
|
ref ERR_SiteRuntime Responders;
|
||||||
string ExpectedGuardFaction;
|
string ExpectedGuardFaction;
|
||||||
int InitialGuardCount;
|
int InitialGuardCount;
|
||||||
ref ERR_PrisonPatrol PrisonPatrol;
|
ref ERR_PrisonPatrol PrisonPatrol;
|
||||||
@@ -87,7 +88,7 @@ class ERR_SiteService
|
|||||||
{
|
{
|
||||||
if (!group) return null;
|
if (!group) return null;
|
||||||
foreach (ERR_SiteRuntime site : m_Sites)
|
foreach (ERR_SiteRuntime site : m_Sites)
|
||||||
if (site.Group == group) return site;
|
if (site.Group == group || (site.Responders && site.Responders.Group == group)) return site;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
ERR_SiteRuntime Find(string id)
|
ERR_SiteRuntime Find(string id)
|
||||||
@@ -109,6 +110,17 @@ class ERR_SiteService
|
|||||||
PrintFormat("Reintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
|
PrintFormat("Reintroduced [sites]: cache clearance position=%1 clear=%2", ground, !m_Obstructed);
|
||||||
return !m_Obstructed;
|
return !m_Obstructed;
|
||||||
}
|
}
|
||||||
|
protected bool GeneratedCacheClearance(vector position)
|
||||||
|
{
|
||||||
|
// Match the physical terrain survey. An entity's broad bounding box may
|
||||||
|
// overlap an empty volume near a tree or ruin without blocking the cache.
|
||||||
|
vector ground = ERR_Actors.Ground(position);
|
||||||
|
TraceBox trace = new TraceBox();
|
||||||
|
trace.Start = ground + "0 0.12 0"; trace.End = ground + "0 2 0";
|
||||||
|
trace.Mins = "-1.3 0 -1.3"; trace.Maxs = "1.3 0.1 1.3";
|
||||||
|
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||||
|
return GetGame().GetWorld().TraceMove(trace, null) >= 1;
|
||||||
|
}
|
||||||
protected bool BackpackClearance(vector position)
|
protected bool BackpackClearance(vector position)
|
||||||
{
|
{
|
||||||
vector floor = ERR_Actors.Ground(position);
|
vector floor = ERR_Actors.Ground(position);
|
||||||
@@ -293,6 +305,7 @@ class ERR_SiteService
|
|||||||
vector containerPosition = position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading);
|
vector containerPosition = position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading);
|
||||||
bool clearContainer;
|
bool clearContainer;
|
||||||
if (spec.Kind == ERR_LocationKind.PRISON) clearContainer = BackpackClearance(containerPosition);
|
if (spec.Kind == ERR_LocationKind.PRISON) clearContainer = BackpackClearance(containerPosition);
|
||||||
|
else if (spec.PlacementResolved) clearContainer = GeneratedCacheClearance(containerPosition);
|
||||||
else clearContainer = CacheClearance(containerPosition);
|
else clearContainer = CacheClearance(containerPosition);
|
||||||
if (!clearContainer) return Fail("obstructed cache position: " + spec.Id);
|
if (!clearContainer) return Fail("obstructed cache position: " + spec.Id);
|
||||||
site.Container = Spawn(spec.Container, position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading));
|
site.Container = Spawn(spec.Container, position + ERR_CampTerrain.Rotate(spec.ContainerOffset, spec.Heading));
|
||||||
@@ -334,11 +347,13 @@ class ERR_SiteService
|
|||||||
vector propPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(prop.Offset, spec.Heading));
|
vector propPosition = ERR_Actors.Ground(position + ERR_CampTerrain.Rotate(prop.Offset, spec.Heading));
|
||||||
// Radio furniture intentionally surrounds/supports its own elevated radio.
|
// Radio furniture intentionally surrounds/supports its own elevated radio.
|
||||||
// Generated camp terrain was checked before any of these objects existed.
|
// Generated camp terrain was checked before any of these objects existed.
|
||||||
if (spec.Kind != ERR_LocationKind.RADIO && !CacheClearance(propPosition)) return Fail("obstructed scenery position: " + spec.Id);
|
if (!spec.PlacementResolved && spec.Kind != ERR_LocationKind.RADIO && !CacheClearance(propPosition)) return Fail("obstructed scenery position: " + spec.Id);
|
||||||
propPosition[1] = propPosition[1] + prop.Offset[1];
|
propPosition[1] = propPosition[1] + prop.Offset[1];
|
||||||
IEntity scenery = ERR_Actors.Spawn(prop.Prefab, propPosition, prop.Angles + Vector(spec.Heading, 0, 0));
|
IEntity scenery = ERR_Actors.Spawn(prop.Prefab, propPosition, prop.Angles + Vector(spec.Heading, 0, 0));
|
||||||
if (!scenery) return Fail("scenery prefab: " + spec.Id);
|
if (!scenery) return Fail("scenery prefab: " + spec.Id);
|
||||||
m_Entities.Insert(scenery); site.Props.Insert(scenery);
|
m_Entities.Insert(scenery); site.Props.Insert(scenery);
|
||||||
|
SCR_AIWorld aiWorld = SCR_AIWorld.Cast(GetGame().GetAIWorld());
|
||||||
|
if (aiWorld) aiWorld.RequestNavmeshRebuildEntity(scenery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!CreateTransport(site, position)) return false;
|
if (!CreateTransport(site, position)) return false;
|
||||||
@@ -363,6 +378,12 @@ class ERR_SiteService
|
|||||||
if (!ERR_PrisonPatrol.Configure(site.Group, groupPrefab, m_Mode.EnemyFaction())) return Fail("prison patrol roster: " + spec.Id);
|
if (!ERR_PrisonPatrol.Configure(site.Group, groupPrefab, m_Mode.EnemyFaction())) return Fail("prison patrol roster: " + spec.Id);
|
||||||
site.PrisonPatrol = new ERR_PrisonPatrol();
|
site.PrisonPatrol = new ERR_PrisonPatrol();
|
||||||
}
|
}
|
||||||
|
else if (spec.GuardCount > 0)
|
||||||
|
{
|
||||||
|
site.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", guardPosition));
|
||||||
|
Faction faction = GetGame().GetFactionManager().GetFactionByKey(site.ExpectedGuardFaction);
|
||||||
|
if (!ERR_SiteSquads.Configure(site.Group, groupPrefab, faction, spec.GuardCount)) return Fail("site defender roster: " + spec.Id);
|
||||||
|
}
|
||||||
else site.Group = SCR_AIGroup.Cast(Spawn(groupPrefab, guardPosition));
|
else site.Group = SCR_AIGroup.Cast(Spawn(groupPrefab, guardPosition));
|
||||||
if (!site.Group) return Fail("guard group resource: " + spec.Id);
|
if (!site.Group) return Fail("guard group resource: " + spec.Id);
|
||||||
if (!site.Group.m_aUnitPrefabSlots || site.Group.m_aUnitPrefabSlots.IsEmpty() || !site.Group.GetSpawnImmediately()) return Fail("guard profile must request an initial physical squad: " + spec.Id);
|
if (!site.Group.m_aUnitPrefabSlots || site.Group.m_aUnitPrefabSlots.IsEmpty() || !site.Group.GetSpawnImmediately()) return Fail("guard profile must request an initial physical squad: " + spec.Id);
|
||||||
@@ -370,6 +391,7 @@ class ERR_SiteService
|
|||||||
foreach (ResourceName member : site.Group.m_aUnitPrefabSlots)
|
foreach (ResourceName member : site.Group.m_aUnitPrefabSlots)
|
||||||
if (member.IsEmpty()) return Fail("guard profile has an empty member resource: " + spec.Id);
|
if (member.IsEmpty()) return Fail("guard profile has an empty member resource: " + spec.Id);
|
||||||
if (!CreateGuardOrders(site, guardPosition)) return Fail("guard initial orders: " + spec.Id);
|
if (!CreateGuardOrders(site, guardPosition)) return Fail("guard initial orders: " + spec.Id);
|
||||||
|
if (!CreateResponders(site, groupPrefab)) return Fail("site mobile roster: " + spec.Id);
|
||||||
}
|
}
|
||||||
PrintFormat("Reintroduced [sites]: authored %1 at %2", spec.Id, position);
|
PrintFormat("Reintroduced [sites]: authored %1 at %2", spec.Id, position);
|
||||||
}
|
}
|
||||||
@@ -452,7 +474,9 @@ class ERR_SiteService
|
|||||||
if (!GetGame()) return;
|
if (!GetGame()) return;
|
||||||
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
|
ChimeraAIWorld world = ChimeraAIWorld.Cast(GetGame().GetAIWorld());
|
||||||
if (!world) return;
|
if (!world) return;
|
||||||
foreach (ERR_SiteRuntime site : m_Sites)
|
array<ERR_SiteRuntime> pendingGroups = {};
|
||||||
|
foreach (ERR_SiteRuntime owner : m_Sites) { pendingGroups.Insert(owner); if (owner.Responders) pendingGroups.Insert(owner.Responders); }
|
||||||
|
foreach (ERR_SiteRuntime site : pendingGroups)
|
||||||
{
|
{
|
||||||
if (!site.Group || site.InitialGuardCount <= 0) continue;
|
if (!site.Group || site.InitialGuardCount <= 0) continue;
|
||||||
// Retire this instance's initial spawn intent as well as its current queue
|
// Retire this instance's initial spawn intent as well as its current queue
|
||||||
@@ -482,6 +506,12 @@ class ERR_SiteService
|
|||||||
ERR_PrepareResult guards = InitialGuards(site);
|
ERR_PrepareResult guards = InitialGuards(site);
|
||||||
if (guards == ERR_PrepareResult.FAILED) return m_State;
|
if (guards == ERR_PrepareResult.FAILED) return m_State;
|
||||||
if (guards == ERR_PrepareResult.PENDING) pendingGuards++;
|
if (guards == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||||
|
if (site.Responders)
|
||||||
|
{
|
||||||
|
ERR_PrepareResult mobile = InitialGuards(site.Responders);
|
||||||
|
if (mobile == ERR_PrepareResult.FAILED) return m_State;
|
||||||
|
if (mobile == ERR_PrepareResult.PENDING) pendingGuards++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
string firstRejected;
|
string firstRejected;
|
||||||
foreach (ERR_StockPlacement placement : m_Placements)
|
foreach (ERR_StockPlacement placement : m_Placements)
|
||||||
@@ -528,6 +558,7 @@ class ERR_SiteService
|
|||||||
}
|
}
|
||||||
m_Mode.Forces().Register(site.Definition.Id, kind, site.Group.GetOrigin(), m_Mode.ForceLiving(site.Group), route, site.Group, GetGame().GetWorld().GetWorldTime());
|
m_Mode.Forces().Register(site.Definition.Id, kind, site.Group.GetOrigin(), m_Mode.ForceLiving(site.Group), route, site.Group, GetGame().GetWorld().GetWorldTime());
|
||||||
m_Mode.RegisterGroup(site.Group);
|
m_Mode.RegisterGroup(site.Group);
|
||||||
|
ActivateResponders(site);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -588,4 +619,32 @@ class ERR_SiteService
|
|||||||
if (m_State == ERR_PrepareResult.PENDING) CancelInitialGuardRequests();
|
if (m_State == ERR_PrepareResult.PENDING) CancelInitialGuardRequests();
|
||||||
StopPatrols();
|
StopPatrols();
|
||||||
}
|
}
|
||||||
|
protected bool CreateResponders(ERR_SiteRuntime site, ResourceName profile)
|
||||||
|
{
|
||||||
|
ERR_SiteDefinition spec = site.Definition;
|
||||||
|
if (spec.ResponseCount == 0) return true;
|
||||||
|
ERR_SiteRuntime mobile = new ERR_SiteRuntime();
|
||||||
|
site.Responders = mobile;
|
||||||
|
mobile.Definition = spec; mobile.Location = site.Location;
|
||||||
|
mobile.ExpectedGuardFaction = site.ExpectedGuardFaction;
|
||||||
|
vector position = ERR_Actors.Ground(spec.Position + ERR_CampTerrain.Rotate(spec.ResponseOffset, spec.Heading));
|
||||||
|
mobile.Group = SCR_AIGroup.Cast(Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", position));
|
||||||
|
mobile.InitialGuardCount = spec.ResponseCount;
|
||||||
|
if (!ERR_SiteSquads.Configure(mobile.Group, profile, site.Group.GetFaction(), spec.ResponseCount)) return false;
|
||||||
|
// Local guard duty resumes when an HQ search expires. HQ inserts its delivered
|
||||||
|
// search ahead of this waypoint; only this mobile group is eligible to leave.
|
||||||
|
AIWaypoint home = AIWaypoint.Cast(Spawn(ERR_Resources.DefendWaypoint(), position));
|
||||||
|
if (!home) return false;
|
||||||
|
home.SetCompletionRadius(25); mobile.Group.AddWaypoint(home);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected void ActivateResponders(ERR_SiteRuntime site)
|
||||||
|
{
|
||||||
|
if (!site.Responders || !site.Responders.Group) return;
|
||||||
|
SCR_AIGroup group = site.Responders.Group;
|
||||||
|
m_Mode.Forces().Register(site.Definition.Id + "-responders", ERR_ForceKind.PATROL, group.GetOrigin(), m_Mode.ForceLiving(group), null, group, GetGame().GetWorld().GetWorldTime());
|
||||||
|
m_Mode.RegisterGroup(group);
|
||||||
|
ERR_HQ.Event("SITE GARRISON", string.Format("%1 (%2) at %3: %4 permanent defenders stay at the site; %5 mobile soldiers may receive HQ hunting orders; faction=%6", site.Definition.Label, site.Definition.Id, site.Location.Position(), m_Mode.ForceLiving(site.Group), m_Mode.ForceLiving(group), site.ExpectedGuardFaction));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// Finite role-separated site rosters. A mobile team never borrows the permanent
|
||||||
|
// defenders, and a defeated team is not recreated by this owner.
|
||||||
|
class ERR_SiteSquads
|
||||||
|
{
|
||||||
|
static array<ResourceName> Roster(array<ResourceName> source, int count)
|
||||||
|
{
|
||||||
|
array<ResourceName> result = {};
|
||||||
|
if (!source || source.IsEmpty() || count < 1 || count > 8) return result;
|
||||||
|
foreach (ResourceName member : source) if (member.IsEmpty()) return result;
|
||||||
|
for (int index = 0; index < count; index++) result.Insert(source[index % source.Count()]);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
static bool Configure(SCR_AIGroup group, ResourceName profile, Faction faction, int count)
|
||||||
|
{
|
||||||
|
if (!Replication.IsServer() || !group || !faction || group.GetAgentsCount() != 0) return false;
|
||||||
|
Resource resource = Resource.Load(profile);
|
||||||
|
if (!resource || !resource.IsValid()) return false;
|
||||||
|
IEntitySource source = resource.GetResource().ToEntitySource();
|
||||||
|
array<ResourceName> roster = {};
|
||||||
|
if (!source || !source.Get("m_aUnitPrefabSlots", roster)) return false;
|
||||||
|
group.m_aUnitPrefabSlots = Roster(roster, count);
|
||||||
|
if (group.m_aUnitPrefabSlots.Count() != count) return false;
|
||||||
|
group.SetFaction(faction);
|
||||||
|
group.SetSpawnImmediately(true);
|
||||||
|
group.SetPermanentLOD(0); group.ActivateAI();
|
||||||
|
group.RequestSpawn(count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
// The local native runner has one real player. A physical stand-in tests camera
|
||||||
|
// tracking; canonical multiplayer target selection is tested separately.
|
||||||
|
modded class SCR_PlayerController
|
||||||
|
{
|
||||||
|
protected IEntity m_ERRCameraProbeTarget;
|
||||||
|
void ERR_ProbeCameraTarget(IEntity target) { m_ERRCameraProbeTarget = target; }
|
||||||
|
override IEntity ERR_ObservedEntity()
|
||||||
|
{
|
||||||
|
string outcome;
|
||||||
|
if (m_ERRCameraProbeTarget && System.GetCLIParam("errSmokeOutcome", outcome) && outcome == "unconscious-camera") return m_ERRCameraProbeTarget;
|
||||||
|
return super.ERR_ObservedEntity();
|
||||||
|
}
|
||||||
|
bool ERR_ProbeCameraActive()
|
||||||
|
{
|
||||||
|
return m_ERRObserverCamera && GetGame().GetCameraManager().CurrentCamera() == m_ERRObserverCamera;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected int m_ERRCameraStage = -2;
|
||||||
|
protected int m_ERRCameraDeadline;
|
||||||
|
protected IEntity m_ERRCameraWeapon;
|
||||||
|
protected float m_ERRCameraWait;
|
||||||
|
protected IEntity m_ERRCameraBody, m_ERRCameraTarget;
|
||||||
|
protected SCR_CharacterDamageManagerComponent m_ERRCameraDamage;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "unconscious-camera") return;
|
||||||
|
m_ERRCameraDeadline = System.GetTickCount() + 180000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_CameraProbeTick, 500, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_CameraCheck(bool ok, string message)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: unconscious camera " + message); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: unconscious camera " + message, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_CameraProbeTick); return false;
|
||||||
|
}
|
||||||
|
protected void ERR_CameraKnockout()
|
||||||
|
{
|
||||||
|
m_ERRCameraDamage.ForceUnconsciousness();
|
||||||
|
array<ref SCR_PersistentDamageEffect> effects = m_ERRCameraDamage.GetAllPersistentEffectsOfType(SCR_PassiveHitZoneRegenDamageEffect);
|
||||||
|
foreach (SCR_PersistentDamageEffect effect : effects) m_ERRCameraDamage.TerminateDamageEffect(effect);
|
||||||
|
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, true);
|
||||||
|
}
|
||||||
|
protected void ERR_CameraProbeTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRCameraDeadline || !PreparationError().IsEmpty()) { ERR_CameraCheck(false, "deadline/preparation " + PreparationError()); return; }
|
||||||
|
if (!Active()) return;
|
||||||
|
SCR_PlayerController pc = SCR_PlayerController.Cast(GetGame().GetPlayerController());
|
||||||
|
if (!pc || !pc.GetControlledEntity()) return;
|
||||||
|
float now = GetWorld().GetWorldTime();
|
||||||
|
if (now < m_ERRCameraWait) return;
|
||||||
|
if (m_ERRCameraStage == -2)
|
||||||
|
{
|
||||||
|
if (!ERR_CameraCheck(Settings().Get("keepWeaponOnUnconscious") == 1, "weapon retention defaults to enabled")) return;
|
||||||
|
m_ERRCameraBody = pc.GetControlledEntity();
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(m_ERRCameraBody.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
m_ERRCameraWeapon = ERR_Actors.Spawn("{FA5C25BF66A53DCF}Prefabs/Weapons/Rifles/AK74/Rifle_AK74.et", m_ERRCameraBody.GetOrigin()); Own(m_ERRCameraWeapon);
|
||||||
|
if (!ERR_CameraCheck(m_ERRCameraWeapon && inventory && inventory.TryInsertItem(m_ERRCameraWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY), "one physical rifle granted to the player")) return;
|
||||||
|
m_ERRCameraStage = -1; m_ERRCameraWait = now + 1500; return;
|
||||||
|
}
|
||||||
|
SCR_CharacterControllerComponent character = SCR_CharacterControllerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterControllerComponent));
|
||||||
|
InventoryStorageManagerComponent playerInventory = character.GetInventoryStorageManager();
|
||||||
|
if (m_ERRCameraStage == -1)
|
||||||
|
{
|
||||||
|
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(m_ERRCameraWeapon.FindComponent(BaseWeaponComponent));
|
||||||
|
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon) && character.SelectWeapon(weapon), "rifle selected with native equip animation")) return;
|
||||||
|
m_ERRCameraStage = 0; m_ERRCameraWait = now + 2000; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 0)
|
||||||
|
{
|
||||||
|
if (!ERR_CameraCheck(character.GetWeaponManagerComponent().GetCurrentWeapon() && character.GetWeaponManagerComponent().GetCurrentWeapon().GetOwner() == m_ERRCameraWeapon, "original rifle actually in hand before knockout")) return;
|
||||||
|
m_ERRCameraBody = pc.GetControlledEntity();
|
||||||
|
m_ERRCameraDamage = SCR_CharacterDamageManagerComponent.Cast(m_ERRCameraBody.FindComponent(SCR_CharacterDamageManagerComponent));
|
||||||
|
if (!ERR_CameraCheck(m_ERRCameraDamage && m_ERRCameraDamage.ERR_IsRevivablePlayer(), "admitted player retains treatable body")) return;
|
||||||
|
ERR_CameraKnockout(); m_ERRCameraStage = 1; m_ERRCameraWait = now + 3000; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 1)
|
||||||
|
{
|
||||||
|
if (!ERR_CameraCheck(pc.ERR_ObserverState() == ERR_ObserverReason.UNCONSCIOUS && pc.ERR_ProbeCameraActive(), "native knockout automatically opens the manual third-person camera")) return;
|
||||||
|
if (!ERR_CameraCheck(pc.GetControlledEntity() == m_ERRCameraBody && ERR_Actors.Alive(m_ERRCameraBody) && !ERR_Actors.CanFight(m_ERRCameraBody), "possession and living unconscious body unchanged")) return;
|
||||||
|
if (!ERR_CameraCheck(!pc.ERR_ObservedEntity() && vector.DistanceXZ(pc.ERR_ObserverAnchor(), m_ERRCameraBody.GetOrigin()) < 1, "no teammate falls back to the casualty body")) return;
|
||||||
|
if (!ERR_CameraCheck(playerInventory.Contains(m_ERRCameraWeapon), "enabled retention keeps the exact rifle with the unconscious player")) return;
|
||||||
|
vector point = ERR_Actors.Ground(m_ERRCameraBody.GetOrigin() + "25 0 25");
|
||||||
|
m_ERRCameraTarget = ERR_Actors.Spawn(Factions().PrisonerPrefab, point); Own(m_ERRCameraTarget);
|
||||||
|
if (!ERR_CameraCheck(m_ERRCameraTarget != null, "physical follow-target stand-in created")) return;
|
||||||
|
pc.ERR_ProbeCameraTarget(m_ERRCameraTarget);
|
||||||
|
m_ERRCameraStage = 2; m_ERRCameraWait = now + 1500; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 2)
|
||||||
|
{
|
||||||
|
vector camera = GetGame().GetCameraManager().CurrentCamera().GetOrigin();
|
||||||
|
if (!ERR_CameraCheck(vector.Distance(camera, m_ERRCameraTarget.GetOrigin()) < 13 && vector.Distance(camera, m_ERRCameraBody.GetOrigin()) > 15, "camera follows target in 3D without moving the casualty")) return;
|
||||||
|
m_ERRCameraTarget.SetOrigin(ERR_Actors.Ground(m_ERRCameraTarget.GetOrigin() + "15 0 0"));
|
||||||
|
m_ERRCameraStage = 3; m_ERRCameraWait = now + 1500; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 3)
|
||||||
|
{
|
||||||
|
if (!ERR_CameraCheck(vector.Distance(GetGame().GetCameraManager().CurrentCamera().GetOrigin(), m_ERRCameraTarget.GetOrigin()) < 13, "camera tracks a moving target")) return;
|
||||||
|
pc.ERR_ProbeCameraTarget(null); ERR_Actors.DeleteSpawned(m_ERRCameraTarget); m_ERRCameraTarget = null;
|
||||||
|
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||||
|
m_ERRCameraDamage.FullHeal(); m_ERRCameraStage = 4; m_ERRCameraWait = now + 4000; return;
|
||||||
|
}
|
||||||
|
if (m_ERRCameraStage == 4)
|
||||||
|
{
|
||||||
|
if (!ERR_CameraCheck(ERR_Actors.CanFight(m_ERRCameraBody) && pc.GetControlledEntity() == m_ERRCameraBody && pc.ERR_ObserverState() == ERR_ObserverReason.NONE && !pc.ERR_HasObserverCamera(), "native healing closes spectator and retains the original conscious player")) return;
|
||||||
|
if (!ERR_CameraCheck(GetGame().GetCameraManager().CurrentCamera() == pc.GetPlayerCamera(), "normal player camera restored")) return;
|
||||||
|
if (!ERR_CameraCheck(character.GetWeaponManagerComponent().GetCurrentWeapon() && character.GetWeaponManagerComponent().GetCurrentWeapon().GetOwner() == m_ERRCameraWeapon, "revival equips the same rifle without ground pickup")) return;
|
||||||
|
if (!ERR_CameraCheck(Settings().Set("keepWeaponOnUnconscious", 0), "retention disabled through persistent setting")) return;
|
||||||
|
ERR_CameraKnockout(); m_ERRCameraStage = 5; m_ERRCameraWait = now + 3000; return;
|
||||||
|
}
|
||||||
|
if (!ERR_CameraCheck(pc.ERR_ProbeCameraActive(), "second knockout reopens camera without stale target")) return;
|
||||||
|
if (!ERR_CameraCheck(!playerInventory.Contains(m_ERRCameraWeapon), "disabled retention leaves normal native weapon drop on ground")) return;
|
||||||
|
m_ERRCameraDamage.SetGroupIsBeingHealed(ECharacterHitZoneGroup.RIGHTARM, false);
|
||||||
|
m_ERRCameraDamage.FullHeal();
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_CameraProbeTick);
|
||||||
|
Print("ERR SMOKE COMPLETE: unconscious spectator camera, physical tracking, retained treatment body, recovery and repeated knockout; target stand-in, not multiplayer proof");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
class ERR_UnconsciousWeaponRules
|
||||||
|
{
|
||||||
|
static bool Firearm(int kind)
|
||||||
|
{
|
||||||
|
return kind == EWeaponType.WT_RIFLE || kind == EWeaponType.WT_SNIPERRIFLE || kind == EWeaponType.WT_MACHINEGUN
|
||||||
|
|| kind == EWeaponType.WT_HANDGUN || kind == EWeaponType.WT_GRENADELAUNCHER || kind == EWeaponType.WT_ROCKETLAUNCHER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retain the actual item, including its magazine/attachments. Never spawn a
|
||||||
|
// replacement and never take an item that another inventory has acquired.
|
||||||
|
modded class SCR_CharacterControllerComponent
|
||||||
|
{
|
||||||
|
protected IEntity m_ERRRetainedWeapon;
|
||||||
|
protected ref ERR_EnemyEquipmentCallback m_ERRWeaponInsertion;
|
||||||
|
protected int m_ERRWeaponInsertAttempts, m_ERRWeaponEquipAttempts;
|
||||||
|
protected bool m_ERRWeaponWasDropped;
|
||||||
|
|
||||||
|
protected bool ERR_WeaponOwnerCurrent()
|
||||||
|
{
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
return Replication.IsServer() && mode && mode.Active() && mode.Players() && mode.Players().CurrentActor(GetOwner()) && ERR_Actors.Alive(GetOwner());
|
||||||
|
}
|
||||||
|
protected void ERR_RememberWeapon(IEntity weapon, bool dropped = false)
|
||||||
|
{
|
||||||
|
if (m_ERRRetainedWeapon || !weapon || !ERR_WeaponOwnerCurrent() || ERR_GameMode.Get().Settings().Get("keepWeaponOnUnconscious") != 1) return;
|
||||||
|
BaseWeaponComponent component = BaseWeaponComponent.Cast(weapon.FindComponent(BaseWeaponComponent));
|
||||||
|
if (!component || !ERR_UnconsciousWeaponRules.Firearm(component.GetWeaponType())) return;
|
||||||
|
m_ERRRetainedWeapon = weapon; m_ERRWeaponWasDropped = dropped;
|
||||||
|
m_ERRWeaponInsertAttempts = 0; m_ERRWeaponEquipAttempts = 0; m_ERRWeaponInsertion = null;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_KeepWeaponTick, 100, true);
|
||||||
|
}
|
||||||
|
bool ERR_RetainsWeapon(IEntity weapon)
|
||||||
|
{
|
||||||
|
return weapon && weapon == m_ERRRetainedWeapon && ERR_WeaponOwnerCurrent() && IsUnconscious();
|
||||||
|
}
|
||||||
|
override void OnLifeStateChanged(ECharacterLifeState previousLifeState, ECharacterLifeState newLifeState, bool isJIP)
|
||||||
|
{
|
||||||
|
if (newLifeState == ECharacterLifeState.INCAPACITATED && GetWeaponManagerComponent())
|
||||||
|
{
|
||||||
|
BaseWeaponComponent weapon = GetWeaponManagerComponent().GetCurrentWeapon();
|
||||||
|
if (weapon) ERR_RememberWeapon(weapon.GetOwner());
|
||||||
|
}
|
||||||
|
super.OnLifeStateChanged(previousLifeState, newLifeState, isJIP);
|
||||||
|
}
|
||||||
|
override protected void OnWeaponDropped(IEntity pWeaponEntity, WeaponSlotComponent pWeaponSlot)
|
||||||
|
{
|
||||||
|
super.OnWeaponDropped(pWeaponEntity, pWeaponSlot);
|
||||||
|
if (!IsUnconscious() && GetLifeState() != ECharacterLifeState.INCAPACITATED) return;
|
||||||
|
ERR_RememberWeapon(pWeaponEntity, true);
|
||||||
|
if (pWeaponEntity == m_ERRRetainedWeapon) m_ERRWeaponWasDropped = true;
|
||||||
|
}
|
||||||
|
protected void ERR_ForgetWeapon()
|
||||||
|
{
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_KeepWeaponTick);
|
||||||
|
m_ERRRetainedWeapon = null; m_ERRWeaponInsertion = null;
|
||||||
|
}
|
||||||
|
protected void ERR_KeepWeaponTick()
|
||||||
|
{
|
||||||
|
if (!m_ERRRetainedWeapon || !ERR_WeaponOwnerCurrent()) { ERR_ForgetWeapon(); return; }
|
||||||
|
InventoryStorageManagerComponent inventory = GetInventoryStorageManager();
|
||||||
|
if (!inventory) { ERR_ForgetWeapon(); return; }
|
||||||
|
if (!inventory.Contains(m_ERRRetainedWeapon))
|
||||||
|
{
|
||||||
|
if (m_ERRWeaponInsertion)
|
||||||
|
{
|
||||||
|
if (!m_ERRWeaponInsertion.Finished) return;
|
||||||
|
// One accepted asynchronous insertion is never submitted a second time.
|
||||||
|
if (!m_ERRWeaponInsertion.Succeeded) ERR_HQ.Event("PLAYER WEAPON", "Weapon retention insertion failed; original gun remains available on the ground.");
|
||||||
|
ERR_ForgetWeapon(); return;
|
||||||
|
}
|
||||||
|
InventoryItemComponent item = InventoryItemComponent.Cast(m_ERRRetainedWeapon.FindComponent(InventoryItemComponent));
|
||||||
|
if (!m_ERRWeaponWasDropped || !item || item.GetParentSlot() || vector.Distance(GetOwner().GetOrigin(), m_ERRRetainedWeapon.GetOrigin()) > 8 || ++m_ERRWeaponInsertAttempts > 10)
|
||||||
|
{ ERR_ForgetWeapon(); return; }
|
||||||
|
ref ERR_EnemyEquipmentCallback operation = new ERR_EnemyEquipmentCallback();
|
||||||
|
if (inventory.TryInsertItem(m_ERRRetainedWeapon, EStoragePurpose.PURPOSE_WEAPON_PROXY, operation)) m_ERRWeaponInsertion = operation;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (IsUnconscious()) return;
|
||||||
|
if (++m_ERRWeaponEquipAttempts > 30 || CompartmentAccessComponent.GetVehicleIn(GetOwner())) { ERR_ForgetWeapon(); return; }
|
||||||
|
BaseWeaponComponent current = GetWeaponManagerComponent().GetCurrentWeapon();
|
||||||
|
if (current && current.GetOwner() == m_ERRRetainedWeapon) { ERR_ForgetWeapon(); return; }
|
||||||
|
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(m_ERRRetainedWeapon.FindComponent(BaseWeaponComponent));
|
||||||
|
if (!weapon || !SelectWeapon(weapon)) return;
|
||||||
|
RplComponent rpl = RplComponent.Cast(m_ERRRetainedWeapon.FindComponent(RplComponent));
|
||||||
|
if (rpl) Rpc(ERR_EquipRetainedWeapon, rpl.Id());
|
||||||
|
}
|
||||||
|
[RplRpc(RplChannel.Reliable, RplRcver.Owner)]
|
||||||
|
protected void ERR_EquipRetainedWeapon(RplId id)
|
||||||
|
{
|
||||||
|
if (GetOwner() != SCR_PlayerController.GetLocalControlledEntity() || !ERR_Actors.CanFight(GetOwner())) return;
|
||||||
|
RplComponent rpl = RplComponent.Cast(Replication.FindItem(id));
|
||||||
|
if (!rpl || !GetInventoryStorageManager() || !GetInventoryStorageManager().Contains(rpl.GetEntity())) return;
|
||||||
|
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(rpl.GetEntity().FindComponent(BaseWeaponComponent));
|
||||||
|
if (weapon) SelectWeapon(weapon);
|
||||||
|
}
|
||||||
|
void ~SCR_CharacterControllerComponent()
|
||||||
|
{
|
||||||
|
if (GetGame()) GetGame().GetCallqueue().Remove(ERR_KeepWeaponTick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modded class SCR_CharacterInventoryStorageComponent
|
||||||
|
{
|
||||||
|
override bool OnOverrideCanRemoveItem() { return true; }
|
||||||
|
override bool CanRemoveItem(IEntity item)
|
||||||
|
{
|
||||||
|
SCR_CharacterControllerComponent controller = SCR_CharacterControllerComponent.Cast(GetOwner().FindComponent(SCR_CharacterControllerComponent));
|
||||||
|
if (controller && controller.ERR_RetainsWeapon(item)) return false;
|
||||||
|
return super.CanRemoveItem(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ class ERR_AirAssaultTeam
|
|||||||
}
|
}
|
||||||
class ERR_AirDropGeometry
|
class ERR_AirDropGeometry
|
||||||
{
|
{
|
||||||
|
static string LastFailure;
|
||||||
static float Along(vector position, vector center, vector direction)
|
static float Along(vector position, vector center, vector direction)
|
||||||
{ vector offset = position - center;
|
{ vector offset = position - center;
|
||||||
offset[1] = 0;
|
offset[1] = 0;
|
||||||
@@ -47,17 +48,19 @@ class ERR_AirDropGeometry
|
|||||||
}
|
}
|
||||||
static bool LandingLine(vector center, vector direction)
|
static bool LandingLine(vector center, vector direction)
|
||||||
{
|
{
|
||||||
|
LastFailure = "clear";
|
||||||
for (int step = -4; step <= 5; step++)
|
for (int step = -4; step <= 5; step++)
|
||||||
{
|
{
|
||||||
vector point = ERR_Actors.Ground(center + direction * (step * 50));
|
vector point = ERR_Actors.Ground(center + direction * (step * 50));
|
||||||
if (point[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point) || !ERR_HQRoadIntercept.Standable(point)) return false;
|
if (point[1] < 2 || ChimeraWorldUtils.TryGetWaterSurfaceSimple(GetGame().GetWorld(), point)) { LastFailure = "water/shore"; return false; }
|
||||||
|
if (!ERR_HQRoadIntercept.Standable(point)) { LastFailure = "landing ground not standable"; return false; }
|
||||||
TraceBox trace = new TraceBox();
|
TraceBox trace = new TraceBox();
|
||||||
trace.Start = point + "0 3 0";
|
trace.Start = point + "0 3 0";
|
||||||
trace.End = point + "0 190 0";
|
trace.End = point + "0 190 0";
|
||||||
trace.Mins = "-4 -1 -4";
|
trace.Mins = "-4 -1 -4";
|
||||||
trace.Maxs = "4 1 4";
|
trace.Maxs = "4 1 4";
|
||||||
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
trace.Flags = TraceFlags.ENTS | TraceFlags.WORLD;
|
||||||
if (GetGame().GetWorld().TraceMove(trace, null) < 1) return false;
|
if (GetGame().GetWorld().TraceMove(trace, null) < 1) { LastFailure = "obstructed descent column"; return false; }
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -254,16 +257,28 @@ class ERR_AirAssaultLoad
|
|||||||
radial[1] = 0;
|
radial[1] = 0;
|
||||||
float heading = Math.Atan2(radial[0], radial[2]);
|
float heading = Math.Atan2(radial[0], radial[2]);
|
||||||
bool found;
|
bool found;
|
||||||
for (int attempt = 0; attempt < 12; attempt++)
|
int surveyed;
|
||||||
|
map<string, int> rejected = new map<string, int>();
|
||||||
|
// Several distances and both runway directions provide alternatives around
|
||||||
|
// woodland while retaining every landing/descent safety check.
|
||||||
|
for (int attempt = 0; attempt < 72; attempt++)
|
||||||
{
|
{
|
||||||
float angle = heading + attempt * Math.PI / 6;
|
int bearing = attempt % 12;
|
||||||
|
int ring = Math.Floor(attempt / 24.0);
|
||||||
|
float radius = 300 + ring * 200;
|
||||||
|
float angle = heading + bearing * Math.PI / 6;
|
||||||
radial = Vector(Math.Sin(angle), 0, Math.Cos(angle));
|
radial = Vector(Math.Sin(angle), 0, Math.Cos(angle));
|
||||||
m_Center = ERR_Actors.Ground(order.Target + radial * 450);
|
m_Center = ERR_Actors.Ground(order.Target + radial * radius);
|
||||||
m_Direction = Vector(-radial[2], 0, radial[0]);
|
m_Direction = Vector(-radial[2], 0, radial[0]);
|
||||||
if (ERR_AirDropGeometry.LandingLine(m_Center, m_Direction)) { found = true;
|
if ((attempt % 24) >= 12) m_Direction = radial;
|
||||||
break;
|
surveyed++;
|
||||||
}
|
if (ERR_AirDropGeometry.LandingLine(m_Center, m_Direction)) { found = true; break; }
|
||||||
|
int count; rejected.Find(ERR_AirDropGeometry.LastFailure, count);
|
||||||
|
rejected.Set(ERR_AirDropGeometry.LastFailure, count + 1);
|
||||||
}
|
}
|
||||||
|
string summary;
|
||||||
|
foreach (string reason, int failures : rejected) summary += reason + "=" + failures.ToString() + "; ";
|
||||||
|
ERR_HQ.Event("AIR DROP SURVEY", string.Format("target=%1 checked=%2 accepted=%3 center=%4; %5", order.Target, surveyed, found, m_Center, summary));
|
||||||
if (!found) return false;
|
if (!found) return false;
|
||||||
m_RunStart = m_Center - m_Direction * 350;
|
m_RunStart = m_Center - m_Direction * 350;
|
||||||
m_RunEnd = m_Center + m_Direction * 450;
|
m_RunEnd = m_Center + m_Direction * 450;
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
class ERR_BodyRecoveryJob
|
class ERR_BodyRecoveryJob
|
||||||
{
|
{
|
||||||
|
int Id;
|
||||||
|
int Repaths;
|
||||||
|
int Reassignments;
|
||||||
|
bool Approaching;
|
||||||
|
float ProgressAt;
|
||||||
|
float BestDistance = 1e20;
|
||||||
|
ref array<SCR_AIGroup> BlockedTeams = {};
|
||||||
IEntity Body;
|
IEntity Body;
|
||||||
vector Position;
|
vector Position;
|
||||||
SCR_AIGroup Team;
|
SCR_AIGroup Team;
|
||||||
@@ -38,6 +45,7 @@ class ERR_BodyRecovery
|
|||||||
{
|
{
|
||||||
if (m_Stopped || !report || !report.Target || m_ReportedBodies.Contains(report.Target) || m_Jobs.Count() >= 128 || !m_Mode.Evidence().Find(report.Target)) return;
|
if (m_Stopped || !report || !report.Target || m_ReportedBodies.Contains(report.Target) || m_Jobs.Count() >= 128 || !m_Mode.Evidence().Find(report.Target)) return;
|
||||||
ERR_BodyRecoveryJob job = new ERR_BodyRecoveryJob();
|
ERR_BodyRecoveryJob job = new ERR_BodyRecoveryJob();
|
||||||
|
job.Id = m_Jobs.Count() + 1;
|
||||||
job.Body = report.Target;
|
job.Body = report.Target;
|
||||||
job.Position = report.Position;
|
job.Position = report.Position;
|
||||||
m_ReportedBodies.Insert(report.Target);
|
m_ReportedBodies.Insert(report.Target);
|
||||||
@@ -49,6 +57,7 @@ class ERR_BodyRecovery
|
|||||||
foreach (ERR_BodyRecoveryJob previous : m_Jobs)
|
foreach (ERR_BodyRecoveryJob previous : m_Jobs)
|
||||||
{
|
{
|
||||||
if (previous == job || previous.State != 4 || !previous.Team || previous.Team.GetPlayerCount(true) > 0 || previous.Team.GetFaction() != m_Mode.EnemyFaction() || m_Mode.ForceLiving(previous.Team) == 0) continue;
|
if (previous == job || previous.State != 4 || !previous.Team || previous.Team.GetPlayerCount(true) > 0 || previous.Team.GetFaction() != m_Mode.EnemyFaction() || m_Mode.ForceLiving(previous.Team) == 0) continue;
|
||||||
|
if (job.BlockedTeams.Contains(previous.Team)) continue;
|
||||||
job.Team = previous.Team;
|
job.Team = previous.Team;
|
||||||
previous.Team = null;
|
previous.Team = null;
|
||||||
job.State = 1;
|
job.State = 1;
|
||||||
@@ -178,6 +187,7 @@ class ERR_BodyRecovery
|
|||||||
}
|
}
|
||||||
if (job.State == 2)
|
if (job.State == 2)
|
||||||
{
|
{
|
||||||
|
if (!TrackProgress(job, now)) continue;
|
||||||
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
|
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
|
||||||
if (utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty()) { job.SearchAt = -1;
|
if (utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty()) { job.SearchAt = -1;
|
||||||
continue;
|
continue;
|
||||||
@@ -193,7 +203,7 @@ class ERR_BodyRecovery
|
|||||||
float nearest = 1e20;
|
float nearest = 1e20;
|
||||||
foreach (AIAgent nearbyAgent : agents)
|
foreach (AIAgent nearbyAgent : agents)
|
||||||
if (nearbyAgent && nearbyAgent.GetControlledEntity()) nearest = Math.Min(nearest, vector.Distance(nearbyAgent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
|
if (nearbyAgent && nearbyAgent.GetControlledEntity()) nearest = Math.Min(nearest, vector.Distance(nearbyAgent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
|
||||||
ERR_HQ.Event("BODY SEARCH PROGRESS", "nearest searcher=" + nearest.ToString() + "m; physical inspection required");
|
ERR_HQ.Event("BODY SEARCH PROGRESS", string.Format("job=%1 team=%2 body=%3 nearest searcher=%4m; replans=%5; physical inspection required", job.Id, job.Team.GetOrigin(), job.Body.GetOrigin(), nearest, job.Repaths));
|
||||||
}
|
}
|
||||||
if (!searcher) { job.SearchAt = -1;
|
if (!searcher) { job.SearchAt = -1;
|
||||||
continue;
|
continue;
|
||||||
@@ -218,6 +228,51 @@ class ERR_BodyRecovery
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
protected bool TrackProgress(ERR_BodyRecoveryJob job, float now)
|
||||||
|
{
|
||||||
|
array<AIAgent> agents = {}; job.Team.GetAgents(agents);
|
||||||
|
float nearest = 1e20;
|
||||||
|
foreach (AIAgent agent : agents)
|
||||||
|
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) nearest = Math.Min(nearest, vector.Distance(agent.GetControlledEntity().GetOrigin(), job.Body.GetOrigin()));
|
||||||
|
SCR_AIGroupUtilityComponent utility = job.Team.GetGroupUtilityComponent();
|
||||||
|
bool combat = utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty();
|
||||||
|
if (job.ProgressAt == 0 || nearest < job.BestDistance - 10 || combat || nearest <= 3)
|
||||||
|
{ job.ProgressAt = now; job.BestDistance = nearest; }
|
||||||
|
if (job.Approaching && job.Waypoint && job.Waypoint.IsWithinCompletionRadius(job.Team))
|
||||||
|
{
|
||||||
|
RemoveWaypoint(job); job.Approaching = false;
|
||||||
|
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), job.Position));
|
||||||
|
if (job.Waypoint) { job.Waypoint.SetCompletionRadius(1.5); job.Team.AddWaypointAt(job.Waypoint, 0); }
|
||||||
|
}
|
||||||
|
if (now - job.ProgressAt < 120000) return true;
|
||||||
|
job.ProgressAt = now;
|
||||||
|
if (job.Repaths >= 3)
|
||||||
|
{
|
||||||
|
ERR_HQ.Event("BODY SEARCH STALLED", string.Format("job=%1 team=%2 body=%3 distance=%4m; three native replans exhausted", job.Id, job.Team.GetOrigin(), job.Body.GetOrigin(), nearest));
|
||||||
|
if (job.Reassignments >= 2) { Finish(job, false); return false; }
|
||||||
|
RemoveWaypoint(job); m_HQ.ReleaseRecovery(job.Team);
|
||||||
|
job.BlockedTeams.Insert(job.Team); job.Team = null;
|
||||||
|
job.Reassignments++; job.Repaths = 0; job.State = 0; job.Approaching = false;
|
||||||
|
job.BestDistance = 1e20; job.RetryAt = now + 30000; job.SearchAt = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
job.Repaths++;
|
||||||
|
vector target = job.Position;
|
||||||
|
// Vary the final approach instead of teleporting a team or pretending the
|
||||||
|
// body was searched. The original body remains the only inspection target.
|
||||||
|
for (int attempt = 0; attempt < 8; attempt++)
|
||||||
|
{
|
||||||
|
float angle = (job.Repaths * 90 + attempt * 45) * Math.DEG2RAD;
|
||||||
|
vector approach = ERR_Actors.Ground(target + Vector(Math.Sin(angle), 0, Math.Cos(angle)) * 60);
|
||||||
|
if (!ERR_HQRoadIntercept.Standable(approach)) continue;
|
||||||
|
target = approach; job.Approaching = true; break;
|
||||||
|
}
|
||||||
|
RemoveWaypoint(job);
|
||||||
|
job.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), target));
|
||||||
|
if (job.Waypoint) { job.Waypoint.SetCompletionRadius(8); job.Team.AddWaypointAt(job.Waypoint, 0); }
|
||||||
|
ERR_HQ.Event("BODY SEARCH REPATH", string.Format("job=%1 attempt=%2 body=%3 approach=%4 distance=%5m; no progress for 120 seconds", job.Id, job.Repaths, job.Body.GetOrigin(), target, nearest));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
void Stop() { m_Stopped = true;
|
void Stop() { m_Stopped = true;
|
||||||
foreach (ERR_BodyRecoveryJob job : m_Jobs) { RemoveWaypoint(job);
|
foreach (ERR_BodyRecoveryJob job : m_Jobs) { RemoveWaypoint(job);
|
||||||
job.Order = null;
|
job.Order = null;
|
||||||
@@ -267,7 +322,6 @@ modded class ERR_HQ
|
|||||||
if (assignedTask && assignedTask.SecuritySite == id && GetGame().GetWorld().GetWorldTime() < assignedTask.ExpiresAt) secured = true;
|
if (assignedTask && assignedTask.SecuritySite == id && GetGame().GetWorld().GetWorldTime() < assignedTask.ExpiresAt) secured = true;
|
||||||
}
|
}
|
||||||
if (secured) continue;
|
if (secured) continue;
|
||||||
ERR_HQ.Event("INTEL COMPROMISE", "missing issued document concerns " + id + "; defensive reinforcement requested");
|
|
||||||
ERR_HQUnit best;
|
ERR_HQUnit best;
|
||||||
float distance = 1e20;
|
float distance = 1e20;
|
||||||
foreach (ERR_HQUnit unit : m_Units)
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
@@ -279,9 +333,10 @@ modded class ERR_HQ
|
|||||||
distance = candidate;
|
distance = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!best) continue;
|
if (!best) { ERR_HQ.Event("SITE SECURITY WAIT", "site=" + id + ": no eligible idle infantry with a fresh status report within 2500m; no dispatch"); continue; }
|
||||||
ERR_HQRoute route = Downlink(best.Group, "SITE SECURITY", "secure potentially compromised site " + id, 0);
|
ERR_HQRoute route = Downlink(best.Group, "SITE SECURITY", "secure potentially compromised site " + id, 0);
|
||||||
if (!route) continue;
|
if (!route) { ERR_HQ.Event("SITE SECURITY WAIT", "site=" + id + ": selected group has no radio route; no dispatch"); continue; }
|
||||||
|
ERR_HQ.Event("SITE SECURITY QUEUED", "site=" + id + "; group=" + best.Id.ToString() + "; distance=" + distance.ToString() + "m; awaiting radio delivery");
|
||||||
ERR_HQTask task = new ERR_HQTask();
|
ERR_HQTask task = new ERR_HQTask();
|
||||||
task.SecuritySite = id;
|
task.SecuritySite = id;
|
||||||
task.Role = ERR_HQPursuitRole.SEARCH;
|
task.Role = ERR_HQPursuitRole.SEARCH;
|
||||||
|
|||||||
@@ -2,13 +2,17 @@
|
|||||||
// HQ supplies the physical groups. No independent spawn/order timers exist here.
|
// HQ supplies the physical groups. No independent spawn/order timers exist here.
|
||||||
class ERR_EnemyEquipmentRules
|
class ERR_EnemyEquipmentRules
|
||||||
{
|
{
|
||||||
static bool Eligible(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
static bool Owned(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return false;
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !mode || mode != ERR_GameMode.Get() || !mode.Active()) return false;
|
||||||
if (!group || !actor || !ERR_Actors.CanFight(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group) return false;
|
if (!group || !actor || !ERR_Actors.Alive(actor) || ERR_Actors.Player(actor) || ERR_Actors.Group(actor) != group) return false;
|
||||||
SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent();
|
SCR_AIGroupUtilityComponent utility = group.GetGroupUtilityComponent();
|
||||||
return utility && utility.IsMilitary() && group.GetPlayerCount(true) == 0 && mode.IsMissionEnemy(group.GetFaction()) && !ERR_EnemyGunnerSkill.CapturedGroup(mode, group);
|
return utility && utility.IsMilitary() && group.GetPlayerCount(true) == 0 && mode.IsMissionEnemy(group.GetFaction()) && !ERR_EnemyGunnerSkill.CapturedGroup(mode, group);
|
||||||
}
|
}
|
||||||
|
static bool Eligible(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||||
|
{
|
||||||
|
return Owned(mode, group, actor) && ERR_Actors.CanFight(actor);
|
||||||
|
}
|
||||||
static EAISkill Floor(EAISkill skill)
|
static EAISkill Floor(EAISkill skill)
|
||||||
{
|
{
|
||||||
return EAISkill.CYLON;
|
return EAISkill.CYLON;
|
||||||
@@ -70,66 +74,23 @@ class ERR_EnemyEquipmentCallback : ScriptedInventoryOperationCallback
|
|||||||
|
|
||||||
class ERR_EnemyEquipmentEntry
|
class ERR_EnemyEquipmentEntry
|
||||||
{
|
{
|
||||||
static const ResourceName LAUNCHER = "{3DF204EEE0534EF2}Prefabs/Weapons/Launchers/FIM92_Stinger/Launcher_FIM92_Stinger.et";
|
|
||||||
static const int MAX_CHECKS = 5;
|
|
||||||
IEntity Actor;
|
IEntity Actor;
|
||||||
SCR_AIGroup Group;
|
SCR_AIGroup Group;
|
||||||
SCR_AICombatComponent Combat;
|
SCR_AICombatComponent Combat;
|
||||||
ref ERR_EnemyOptics Optics;
|
ref ERR_EnemyOptics Optics;
|
||||||
int Checks;
|
ref ERR_EnemyLauncherGrant Launcher = new ERR_EnemyLauncherGrant();
|
||||||
|
ref ERR_EnemyMineGrant Mines = new ERR_EnemyMineGrant();
|
||||||
int SkillChecks;
|
int SkillChecks;
|
||||||
bool Rolled;
|
|
||||||
bool Selected;
|
|
||||||
bool Submitted;
|
|
||||||
bool Complete;
|
|
||||||
bool Released;
|
bool Released;
|
||||||
ref ERR_EnemyEquipmentCallback Operation;
|
void Check(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
Launcher.Check(mode, Group, Actor);
|
||||||
|
Mines.Check(mode, Group, Actor);
|
||||||
|
}
|
||||||
|
|
||||||
protected bool Finish(string result)
|
|
||||||
{
|
|
||||||
Complete = true;
|
|
||||||
if (Selected) ERR_HQ.Event("STINGER", result);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool Check(ERR_GameMode mode)
|
|
||||||
{
|
|
||||||
if (Complete) return true;
|
|
||||||
if (!ERR_EnemyEquipmentRules.Eligible(mode, Group, Actor)) return Finish("startup equipment cancelled: actor no longer eligible");
|
|
||||||
Checks++;
|
|
||||||
if (!Rolled)
|
|
||||||
{
|
|
||||||
int oneIn = mode.Settings().Get("stingerChanceOneIn");
|
|
||||||
Rolled = true;
|
|
||||||
if (oneIn > 0) Selected = Math.RandomInt(0, oneIn) == 0;
|
|
||||||
}
|
|
||||||
if (!Selected) return Finish("not selected");
|
|
||||||
BaseWeaponManagerComponent weapons = BaseWeaponManagerComponent.Cast(Actor.FindComponent(BaseWeaponManagerComponent));
|
|
||||||
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(Actor.FindComponent(InventoryStorageManagerComponent));
|
|
||||||
if (!weapons || !inventory)
|
|
||||||
{
|
|
||||||
if (Checks >= MAX_CHECKS) return Finish("inventory unavailable after bounded startup checks");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
array<IEntity> equipped = {};
|
|
||||||
weapons.GetWeaponsList(equipped);
|
|
||||||
foreach (IEntity item : equipped)
|
|
||||||
{
|
|
||||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == LAUNCHER)
|
|
||||||
return Finish("physical FIM-92 observed in weapon slot; no replacement will be issued");
|
|
||||||
}
|
|
||||||
// A successful callback may precede a changed slot; dropping/looting that item
|
|
||||||
// before observation must not cause a second grant. Failure permits a bounded retry.
|
|
||||||
if (Submitted && Operation && Operation.Finished && Operation.Succeeded) return Finish("native insertion completed; equipment will not be replenished");
|
|
||||||
if (Checks >= MAX_CHECKS) return Finish("startup deadline reached; unresolved insertion quarantined for this actor");
|
|
||||||
if (Submitted && Operation && !Operation.Finished) return false;
|
|
||||||
Operation = new ERR_EnemyEquipmentCallback();
|
|
||||||
Submitted = true;
|
|
||||||
if (!inventory.TrySpawnPrefabToStorage(LAUNCHER, null, -1, EStoragePurpose.PURPOSE_WEAPON_PROXY, Operation)) Submitted = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
void Stop(ERR_GameMode mode)
|
void Stop(ERR_GameMode mode)
|
||||||
{
|
{
|
||||||
Complete = true;
|
Launcher.Stop(); Mines.Stop();
|
||||||
Released = true;
|
Released = true;
|
||||||
if (Optics) Optics.Stop();
|
if (Optics) Optics.Stop();
|
||||||
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
if (Combat) Combat.ERR_ReleaseEnemySkill(mode);
|
||||||
@@ -205,7 +166,10 @@ class ERR_EnemyEquipment
|
|||||||
foreach (ERR_EnemyEquipmentEntry entry : m_Entries)
|
foreach (ERR_EnemyEquipmentEntry entry : m_Entries)
|
||||||
{
|
{
|
||||||
if (entry.Released) continue;
|
if (entry.Released) continue;
|
||||||
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; }
|
if (!ERR_EnemyEquipmentRules.Owned(m_Mode, entry.Group, entry.Actor)) { entry.Stop(m_Mode); continue; }
|
||||||
|
// Incapacitation suspends work on the existing entry; it must not permanently
|
||||||
|
// remove the skill binding or recreate completed equipment grants on recovery.
|
||||||
|
if (!ERR_EnemyEquipmentRules.Eligible(m_Mode, entry.Group, entry.Actor)) continue;
|
||||||
// Combat may arrive after the group's initial membership notification.
|
// Combat may arrive after the group's initial membership notification.
|
||||||
if (!entry.Combat && entry.SkillChecks < 5)
|
if (!entry.Combat && entry.SkillChecks < 5)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
// Finite mission-owned inventory grants. No client spawns, rerolls or replenishment.
|
||||||
|
class ERR_EnemyLoadoutRules
|
||||||
|
{
|
||||||
|
static bool Selected(int percent, int roll) { return percent >= 0 && percent <= 100 && roll >= 0 && roll < 100 && roll < percent; }
|
||||||
|
static bool AntiAirName(ResourceName prefab)
|
||||||
|
{
|
||||||
|
string path = prefab; path.ToLower();
|
||||||
|
return path.Contains("/launchers/") && !path.Contains("_base.et")
|
||||||
|
&& (path.Contains("stinger") || path.Contains("fim92") || path.Contains("igla") || path.Contains("strela") || path.Contains("manpad"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_EnemyLoadoutCatalog
|
||||||
|
{
|
||||||
|
static const ResourceName FIM = "{3DF204EEE0534EF2}Prefabs/Weapons/Launchers/FIM92_Stinger/Launcher_FIM92_Stinger.et";
|
||||||
|
static const ResourceName IGLA = "{962C400354E4BD6C}Prefabs/Weapons/Launchers/9K38_IGLA/Launcher_9K38_IGLA.et";
|
||||||
|
static const ResourceName MINE = "{B05A816C0BF50802}Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et";
|
||||||
|
protected static ERR_GameMode s_Mode;
|
||||||
|
protected static ref map<string, ResourceName> s_Launchers = new map<string, ResourceName>();
|
||||||
|
static bool Available(ResourceName prefab)
|
||||||
|
{
|
||||||
|
if (prefab.IsEmpty()) return false;
|
||||||
|
Resource resource = Resource.Load(prefab);
|
||||||
|
return resource && resource.IsValid();
|
||||||
|
}
|
||||||
|
static ResourceName Launcher(ERR_GameMode mode, Faction faction)
|
||||||
|
{
|
||||||
|
if (!faction) return FIM;
|
||||||
|
if (s_Mode != mode) { s_Mode = mode; s_Launchers.Clear(); }
|
||||||
|
string key = faction.GetFactionKey();
|
||||||
|
if (s_Launchers.Contains(key)) return s_Launchers.Get(key);
|
||||||
|
ResourceName preferred;
|
||||||
|
ERR_ContentDefinition definition = ERR_ContentRegistry.Definition();
|
||||||
|
if (definition)
|
||||||
|
foreach (ERR_ContentOption option : definition.Options)
|
||||||
|
if (option.NativeFactionKey == key && !option.AntiAirLauncher.IsEmpty()) preferred = option.AntiAirLauncher;
|
||||||
|
if (preferred.IsEmpty())
|
||||||
|
{
|
||||||
|
SCR_Faction nativeFaction = SCR_Faction.Cast(faction);
|
||||||
|
SCR_EntityCatalog catalog;
|
||||||
|
if (nativeFaction) catalog = nativeFaction.GetFactionEntityCatalogOfType(EEntityCatalogType.ITEM, false);
|
||||||
|
array<SCR_EntityCatalogEntry> items = {};
|
||||||
|
if (catalog) catalog.GetEntityList(items);
|
||||||
|
int limit = Math.Min(items.Count(), 4096);
|
||||||
|
for (int i = 0; i < limit; i++)
|
||||||
|
{
|
||||||
|
if (!items[i] || !items[i].IsEnabled()) continue;
|
||||||
|
ResourceName candidate = items[i].GetPrefab();
|
||||||
|
if (ERR_EnemyLoadoutRules.AntiAirName(candidate) && Available(candidate)) { preferred = candidate; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// WCS supplies these native faction AA soldiers, even if the item catalog is absent.
|
||||||
|
if (preferred.IsEmpty() && (key == "USSR" || key == "FIA")) preferred = IGLA;
|
||||||
|
if (!Available(preferred)) preferred = FIM;
|
||||||
|
s_Launchers.Set(key, preferred);
|
||||||
|
ERR_HQ.Event("AA PROFILE", "faction=" + key + "; preferred=" + preferred + "; fallback=" + FIM);
|
||||||
|
return preferred;
|
||||||
|
}
|
||||||
|
static bool Loaded(IEntity item)
|
||||||
|
{
|
||||||
|
if (!item) return false;
|
||||||
|
BaseWeaponComponent weapon = BaseWeaponComponent.Cast(item.FindComponent(BaseWeaponComponent));
|
||||||
|
if (!weapon || weapon.GetWeaponType() != EWeaponType.WT_ROCKETLAUNCHER) return false;
|
||||||
|
array<BaseMuzzleComponent> muzzles = {}; weapon.GetMuzzlesList(muzzles);
|
||||||
|
if (muzzles.IsEmpty() || muzzles.Count() > 8) return false;
|
||||||
|
foreach (BaseMuzzleComponent muzzle : muzzles)
|
||||||
|
{
|
||||||
|
if (!muzzle) continue;
|
||||||
|
for (int barrel = 0; barrel < Math.Min(muzzle.GetBarrelsCount(), 8); barrel++) if (muzzle.IsBarrelChambered(barrel)) return true;
|
||||||
|
BaseMagazineComponent magazine = muzzle.GetMagazine();
|
||||||
|
if (magazine && magazine.GetAmmoCount() > 0) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
static IEntity Find(ResourceName prefab, array<IEntity> items, array<IEntity> excluded)
|
||||||
|
{
|
||||||
|
foreach (IEntity item : items)
|
||||||
|
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == prefab && (!excluded || !excluded.Contains(item))) return item;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_EnemyLauncherGrant
|
||||||
|
{
|
||||||
|
protected bool m_Rolled;
|
||||||
|
protected bool m_Finished;
|
||||||
|
protected bool m_Fallback;
|
||||||
|
protected bool m_Removing;
|
||||||
|
protected int m_Checks;
|
||||||
|
protected ResourceName m_Prefab;
|
||||||
|
protected IEntity m_Issued;
|
||||||
|
protected ref array<IEntity> m_Before = {};
|
||||||
|
protected ref ERR_EnemyEquipmentCallback m_Operation;
|
||||||
|
protected ResourceName Preferred(ERR_GameMode mode, SCR_AIGroup group) { return ERR_EnemyLoadoutCatalog.Launcher(mode, group.GetFaction()); }
|
||||||
|
protected void Finish(IEntity actor, string reason)
|
||||||
|
{
|
||||||
|
m_Finished = true;
|
||||||
|
ERR_HQ.Event("AA EQUIPMENT", string.Format("actor=%1 position=%2 prefab=%3 %4", actor, actor.GetOrigin(), m_Prefab, reason));
|
||||||
|
}
|
||||||
|
protected void Fallback(IEntity actor, string reason)
|
||||||
|
{
|
||||||
|
if (m_Fallback || m_Prefab == ERR_EnemyLoadoutCatalog.FIM) { Finish(actor, "FIM-92 unavailable/unusable: " + reason); return; }
|
||||||
|
ERR_HQ.Event("AA FALLBACK", string.Format("actor=%1 rejected=%2 reason=%3; trying FIM-92", actor, m_Prefab, reason));
|
||||||
|
m_Fallback = true; m_Removing = false; m_Prefab = ERR_EnemyLoadoutCatalog.FIM; m_Operation = null; m_Issued = null; m_Checks = 0;
|
||||||
|
}
|
||||||
|
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||||
|
{
|
||||||
|
if (m_Finished) return;
|
||||||
|
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) { m_Finished = true; return; }
|
||||||
|
if (!m_Rolled)
|
||||||
|
{
|
||||||
|
m_Rolled = true;
|
||||||
|
if (!ERR_EnemyLoadoutRules.Selected(mode.Settings().Runtime("antiAirChancePercent"), Math.RandomInt(0, 100))) { m_Finished = true; return; }
|
||||||
|
m_Prefab = Preferred(mode, group);
|
||||||
|
}
|
||||||
|
if (++m_Checks > 10) { Finish(actor, "bounded startup expired; unresolved operation quarantined, no duplicate grant"); return; }
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
BaseWeaponManagerComponent manager = BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent));
|
||||||
|
if (!inventory || !manager) return;
|
||||||
|
array<IEntity> items = {}; inventory.GetItems(items);
|
||||||
|
array<IEntity> weapons = {}; manager.GetWeaponsList(weapons);
|
||||||
|
if (items.Count() > 512 || weapons.Count() > 16) { Finish(actor, "inventory exceeds inspection budget"); return; }
|
||||||
|
if (m_Removing)
|
||||||
|
{
|
||||||
|
if (!m_Operation || !m_Operation.Finished) return;
|
||||||
|
if (m_Operation.Succeeded && (!m_Issued || !items.Contains(m_Issued))) Fallback(actor, "own unusable replacement removed");
|
||||||
|
else Finish(actor, "could not confirm removal of unusable replacement; no duplicate grant");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach (IEntity existing : weapons)
|
||||||
|
if (existing && existing.GetPrefabData() && (ERR_EnemyLoadoutRules.AntiAirName(existing.GetPrefabData().GetPrefabName()) || existing.GetPrefabData().GetPrefabName() == m_Prefab) && ERR_EnemyLoadoutCatalog.Loaded(existing))
|
||||||
|
{ Finish(actor, "loaded anti-air weapon physically equipped; preserve it without replenishment"); return; }
|
||||||
|
if (m_Operation)
|
||||||
|
{
|
||||||
|
if (!m_Operation.Finished) return;
|
||||||
|
m_Issued = ERR_EnemyLoadoutCatalog.Find(m_Prefab, items, m_Before);
|
||||||
|
if (!m_Issued)
|
||||||
|
{
|
||||||
|
if (!m_Operation.Succeeded) Fallback(actor, "native insertion failed");
|
||||||
|
else Finish(actor, "completed insertion no longer present; may have been looted, no replacement");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Delete only this newly issued, still-owned unusable item, never an authored weapon.
|
||||||
|
if (m_Fallback || m_Prefab == ERR_EnemyLoadoutCatalog.FIM) { Finish(actor, "FIM-92 inserted but not loaded/equipped; inspect mod compatibility"); return; }
|
||||||
|
m_Removing = true; m_Checks = 0; m_Operation = new ERR_EnemyEquipmentCallback();
|
||||||
|
if (!inventory.TryDeleteItem(m_Issued, m_Operation)) Finish(actor, "unusable item removal refused; fallback withheld");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!ERR_EnemyLoadoutCatalog.Available(m_Prefab)) { Fallback(actor, "resource unavailable"); return; }
|
||||||
|
m_Before.Clear(); foreach (IEntity before : items) m_Before.Insert(before);
|
||||||
|
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||||
|
if (!inventory.TrySpawnPrefabToStorage(m_Prefab, null, -1, EStoragePurpose.PURPOSE_WEAPON_PROXY, m_Operation)) Fallback(actor, "native weapon slot insertion refused");
|
||||||
|
}
|
||||||
|
void Stop() { m_Finished = true; }
|
||||||
|
}
|
||||||
|
class ERR_EnemyMineGrant
|
||||||
|
{
|
||||||
|
protected bool m_Rolled;
|
||||||
|
protected bool m_Finished;
|
||||||
|
protected int m_Checks;
|
||||||
|
protected int m_Remaining;
|
||||||
|
protected ref ERR_EnemyEquipmentCallback m_Operation;
|
||||||
|
void Check(ERR_GameMode mode, SCR_AIGroup group, IEntity actor)
|
||||||
|
{
|
||||||
|
if (m_Finished) return;
|
||||||
|
if (!ERR_EnemyEquipmentRules.Eligible(mode, group, actor)) { m_Finished = true; return; }
|
||||||
|
if (!m_Rolled)
|
||||||
|
{
|
||||||
|
m_Rolled = true;
|
||||||
|
if (!ERR_EnemyLoadoutRules.Selected(mode.Settings().Runtime("antiPersonnelMineChancePercent"), Math.RandomInt(0, 100))) { m_Finished = true; return; }
|
||||||
|
m_Remaining = 2;
|
||||||
|
}
|
||||||
|
if (++m_Checks > 10) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "startup deadline; pending native operation will not be duplicated"); return; }
|
||||||
|
InventoryStorageManagerComponent inventory = InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent));
|
||||||
|
if (!inventory) return;
|
||||||
|
if (m_Operation)
|
||||||
|
{
|
||||||
|
if (!m_Operation.Finished) return;
|
||||||
|
if (!m_Operation.Succeeded) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "inventory insertion failed; no loose mines spawned"); return; }
|
||||||
|
m_Operation = null;
|
||||||
|
if (m_Remaining == 0) { m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", string.Format("actor=%1 position=%2 finite two-mine allowance issued", actor, actor.GetOrigin())); return; }
|
||||||
|
}
|
||||||
|
array<IEntity> items = {}; inventory.GetItems(items);
|
||||||
|
int present;
|
||||||
|
foreach (IEntity item : items) if (item && item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)) present++;
|
||||||
|
m_Remaining = Math.Min(m_Remaining, 2 - present);
|
||||||
|
if (m_Remaining <= 0) { m_Finished = true; return; }
|
||||||
|
m_Remaining--; // An accepted grant consumes its allowance even if later dropped or looted.
|
||||||
|
m_Operation = new ERR_EnemyEquipmentCallback();
|
||||||
|
if (!inventory.TrySpawnPrefabToStorage(ERR_EnemyLoadoutCatalog.MINE, null, -1, EStoragePurpose.PURPOSE_DEPOSIT, m_Operation))
|
||||||
|
{ m_Finished = true; ERR_HQ.Event("MINE EQUIPMENT", "no inventory capacity; finite grant refused"); }
|
||||||
|
}
|
||||||
|
void Stop() { m_Finished = true; }
|
||||||
|
}
|
||||||
@@ -92,6 +92,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
protected ref ERR_EnemyEquipment m_Equipment;
|
protected ref ERR_EnemyEquipment m_Equipment;
|
||||||
protected ref ERR_HunterService m_Hunters;
|
protected ref ERR_HunterService m_Hunters;
|
||||||
protected ref ERR_ObjectiveDefence m_ObjectiveDefence;
|
protected ref ERR_ObjectiveDefence m_ObjectiveDefence;
|
||||||
|
protected ref ERR_TownGarrisons m_Towns;
|
||||||
protected ref array<SCR_AICombatComponent> m_SpecialSkills = {};
|
protected ref array<SCR_AICombatComponent> m_SpecialSkills = {};
|
||||||
ERR_HunterService Hunters() { return m_Hunters; }
|
ERR_HunterService Hunters() { return m_Hunters; }
|
||||||
protected ref ERR_ScoutSystem m_Scouts;
|
protected ref ERR_ScoutSystem m_Scouts;
|
||||||
@@ -136,6 +137,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
m_Equipment = new ERR_EnemyEquipment(mode);
|
m_Equipment = new ERR_EnemyEquipment(mode);
|
||||||
m_Hunters = new ERR_HunterService(mode, null);
|
m_Hunters = new ERR_HunterService(mode, null);
|
||||||
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
|
m_ObjectiveDefence = new ERR_ObjectiveDefence(mode, this);
|
||||||
|
m_Towns = new ERR_TownGarrisons(mode);
|
||||||
m_Scouts = new ERR_ScoutSystem(mode);
|
m_Scouts = new ERR_ScoutSystem(mode);
|
||||||
m_Recon = new ERR_AirReconSystem(mode);
|
m_Recon = new ERR_AirReconSystem(mode);
|
||||||
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
m_AirAssault = new ERR_AirAssaultSystem(this, mode);
|
||||||
@@ -232,10 +234,15 @@ class ERR_HQ : ERR_CommandService
|
|||||||
{
|
{
|
||||||
if (m_Units.IsEmpty() || !m_Mode.Forces()) return false;
|
if (m_Units.IsEmpty() || !m_Mode.Forces()) return false;
|
||||||
foreach (ERR_HQUnit unit : m_Units)
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
{
|
||||||
|
ERR_MortarBattery support = m_Mortars.FindGroup(unit.Group);
|
||||||
|
// Failed optional support is terminal, not an outstanding force admission.
|
||||||
|
if (support && (support.Stopped() || !support.SpawnFailure.IsEmpty())) continue;
|
||||||
if (unit.Group && !m_Mode.Forces().FindGroup(unit.Group)) return false;
|
if (unit.Group && !m_Mode.Forces().FindGroup(unit.Group)) return false;
|
||||||
|
}
|
||||||
array<ref ERR_MortarBattery> batteries = {}; m_Mortars.Batteries(batteries);
|
array<ref ERR_MortarBattery> batteries = {}; m_Mortars.Batteries(batteries);
|
||||||
foreach (ERR_MortarBattery battery : batteries)
|
foreach (ERR_MortarBattery battery : batteries)
|
||||||
if (battery && !battery.Stopped() && !battery.StartupActivated) return false;
|
if (battery && !battery.Stopped() && battery.SpawnFailure.IsEmpty() && !battery.StartupActivated) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
protected void AutomaticConvoy(float now)
|
protected void AutomaticConvoy(float now)
|
||||||
@@ -419,7 +426,10 @@ class ERR_HQ : ERR_CommandService
|
|||||||
ERR_HQJournal Journal() { return m_Journal; }
|
ERR_HQJournal Journal() { return m_Journal; }
|
||||||
string Status()
|
string Status()
|
||||||
{
|
{
|
||||||
return string.Format("HQ: %1 groups, %2 delivered contacts, %3 assignments", m_Units.Count(), m_Contacts.Count(), Assigned()) + string.Format("; %1 remembered tracks, %2 swept cells; aggression=%3/30 identified bodies=%4; ", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies()) + m_Hunters.Status() + "; " + m_Mode.Forces().Status() + "; " + m_Mortars.Status() + "; " + m_Logistics.Status() + "; " + m_Resupply.Status() + "; " + m_Recon.Status();
|
return string.Format("Enemy HQ: %1 registered groups; %2 received sightings; %3 assigned responses.", m_Units.Count(), m_Contacts.Count(), Assigned())
|
||||||
|
+ string.Format("\nHQ memory: %1 tracked reports; %2 searched areas.\nAggression: %3 of 30, based on %4 identified bodies (one level per three bodies).", m_Memory.Count(), m_Coverage.Count(), m_Watch.Aggression(), m_Watch.IdentifiedBodies())
|
||||||
|
+ "\n" + m_Hunters.Status() + "\n" + m_Mode.Forces().Status() + "\n" + m_Mortars.Status()
|
||||||
|
+ "\n" + m_Logistics.Status() + "\n" + m_Resupply.Status() + "\n" + m_Recon.Status();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void BindPhysical(ERR_HQUnit unit)
|
protected void BindPhysical(ERR_HQUnit unit)
|
||||||
@@ -481,8 +491,8 @@ class ERR_HQ : ERR_CommandService
|
|||||||
unit.HomeId = home.Definition.Id;
|
unit.HomeId = home.Definition.Id;
|
||||||
// Objective security is permanent; it can report contacts but cannot be tasked
|
// Objective security is permanent; it can report contacts but cannot be tasked
|
||||||
// with searches, reinforcements, resupply or courier work away from its site.
|
// with searches, reinforcements, resupply or courier work away from its site.
|
||||||
unit.ReservedForSupport = ERR_ObjectiveDefence.Objective(home.Definition.Kind);
|
unit.ReservedForSupport = group == home.Group && ERR_ObjectiveDefence.Objective(home.Definition.Kind);
|
||||||
unit.HomeGuard = !home.Definition.Patrol || home.Definition.Patrol.IsEmpty();
|
unit.HomeGuard = group == home.Group && (!home.Definition.Patrol || home.Definition.Patrol.IsEmpty());
|
||||||
}
|
}
|
||||||
int phase = unit.Id % 5;
|
int phase = unit.Id % 5;
|
||||||
unit.NextReport = GetGame().GetWorld().GetWorldTime() + phase * 1000;
|
unit.NextReport = GetGame().GetWorld().GetWorldTime() + phase * 1000;
|
||||||
@@ -493,7 +503,14 @@ class ERR_HQ : ERR_CommandService
|
|||||||
unit.ReservedForSupport = true;
|
unit.ReservedForSupport = true;
|
||||||
BindSpecialSkills(group);
|
BindSpecialSkills(group);
|
||||||
}
|
}
|
||||||
else m_Equipment.RegisterGroup(group, unit.Id, !m_Mortars || !m_Mortars.FindGroup(group));
|
else
|
||||||
|
{
|
||||||
|
int equipmentRadioId = unit.Id;
|
||||||
|
// Widely separated HQ hunters each need a physical manpack uplink.
|
||||||
|
if (m_Hunters && m_Hunters.Contains(group)) equipmentRadioId = 1;
|
||||||
|
if (m_Towns && m_Towns.Contains(group)) equipmentRadioId = 1;
|
||||||
|
m_Equipment.RegisterGroup(group, equipmentRadioId, !m_Mortars || !m_Mortars.FindGroup(group));
|
||||||
|
}
|
||||||
BindPhysical(unit);
|
BindPhysical(unit);
|
||||||
unit.NextStatus = GetGame().GetWorld().GetWorldTime() + 10000 + phase * 1000;
|
unit.NextStatus = GetGame().GetWorld().GetWorldTime() + 10000 + phase * 1000;
|
||||||
unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());
|
unit.AmmoReporting.Initialize(unit.Id, GetGame().GetWorld().GetWorldTime());
|
||||||
@@ -1215,6 +1232,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
}
|
}
|
||||||
m_Hunters.Tick(now);
|
m_Hunters.Tick(now);
|
||||||
m_ObjectiveDefence.Tick(now);
|
m_ObjectiveDefence.Tick(now);
|
||||||
|
m_Towns.Tick(now);
|
||||||
m_Convoys.Tick(now);
|
m_Convoys.Tick(now);
|
||||||
AutomaticConvoy(now);
|
AutomaticConvoy(now);
|
||||||
m_Bodies.TickBudget(now);
|
m_Bodies.TickBudget(now);
|
||||||
@@ -1268,6 +1286,7 @@ class ERR_HQ : ERR_CommandService
|
|||||||
override void Stop()
|
override void Stop()
|
||||||
{
|
{
|
||||||
m_Hunters.Stop(); m_ObjectiveDefence.Stop();
|
m_Hunters.Stop(); m_ObjectiveDefence.Stop();
|
||||||
|
m_Towns.Stop();
|
||||||
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills) if (specialSkill) specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
foreach (SCR_AICombatComponent specialSkill : m_SpecialSkills) if (specialSkill) specialSkill.ERR_ReleaseEnemySkill(m_Mode);
|
||||||
m_SpecialSkills.Clear();
|
m_SpecialSkills.Clear();
|
||||||
foreach (ERR_HQUnit detector : m_Units) if (detector.Detection) detector.Detection.GetOnEnemyDetected().Remove(Detected);
|
foreach (ERR_HQUnit detector : m_Units) if (detector.Detection) detector.Detection.GetOnEnemyDetected().Remove(Detected);
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
|||||||
if (section != "hq") return "";
|
if (section != "hq") return "";
|
||||||
if (option == "interval") return "reportIntervalSeconds";
|
if (option == "interval") return "reportIntervalSeconds";
|
||||||
if (option == "squads") return "supportSquadLimit";
|
if (option == "squads") return "supportSquadLimit";
|
||||||
|
if (option == "mines") return "antiPersonnelMineChancePercent";
|
||||||
if (option == "radius") return "responseRadiusMetres";
|
if (option == "radius") return "responseRadiusMetres";
|
||||||
if (option == "stinger") return "stingerChanceOneIn";
|
if (option == "aa" || option == "stinger") return "antiAirChancePercent";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
override int JournalSequence()
|
override int JournalSequence()
|
||||||
@@ -29,9 +30,9 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
|||||||
// reroll registered soldiers or remove/replenish an issued physical launcher.
|
// reroll registered soldiers or remove/replenish an issued physical launcher.
|
||||||
protected void StingerSetting(ERR_GameMode mode, array<string> parts, array<string> replies)
|
protected void StingerSetting(ERR_GameMode mode, array<string> parts, array<string> replies)
|
||||||
{
|
{
|
||||||
string key = "stingerChanceOneIn";
|
string key = "antiAirChancePercent";
|
||||||
string effect = "Only eligible actors which have not rolled yet use this chance; no reroll, removal or replenishment of existing equipment.";
|
string effect = "Only eligible actors which have not rolled yet use this chance; no reroll, removal or replenishment of existing equipment.";
|
||||||
string range = "0 off, 1 every eligible soldier, 2..10000 one in N";
|
string range = "0..100 percent (0 off, 100 every eligible soldier); faction MANPADS preferred, FIM-92 fallback";
|
||||||
if (parts.Count() == 3 && parts[2] == "show")
|
if (parts.Count() == 3 && parts[2] == "show")
|
||||||
{ replies.Insert(key + "=" + mode.Settings().Get(key).ToString() + ". " + effect); return; }
|
{ replies.Insert(key + "=" + mode.Settings().Get(key).ToString() + ". " + effect); return; }
|
||||||
int value;
|
int value;
|
||||||
@@ -124,7 +125,7 @@ class ERR_HQAdminProvider : ERR_AdminProvider
|
|||||||
replies.Insert("/esc convoys show|set 0..20|random | hq convoy count show|set N (default three; next round)");
|
replies.Insert("/esc convoys show|set 0..20|random | hq convoy count show|set N (default three; next round)");
|
||||||
replies.Insert("/esc hq show | hq memory | hq log show|next|set on|off | hq convoy show|start");
|
replies.Insert("/esc hq show | hq memory | hq log show|next|set on|off | hq convoy show|start");
|
||||||
replies.Insert("/esc hq interval|squads|radius|stinger show|set N | hq convoy auto show|set 0|1|help");
|
replies.Insert("/esc hq interval|squads|radius|stinger show|set N | hq convoy auto show|set 0|1|help");
|
||||||
replies.Insert("Stinger: 0 off, 1 every eligible soldier, 2..10000 one in N; applies only before each actor rolls.");
|
replies.Insert("Stinger: 0..100 percent (0 off, 100 every eligible soldier); faction MANPADS preferred, FIM-92 fallback; applies only before each actor rolls.");
|
||||||
replies.Insert("Convoy auto: default 1, next round, finite fleet. /esc hq heli show | hq heli timer set 1..1440 | hq heli return");
|
replies.Insert("Convoy auto: default 1, next round, finite fleet. /esc hq heli show | hq heli timer set 1..1440 | hq heli return");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ class ERR_HQJournal
|
|||||||
return;
|
return;
|
||||||
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
|
float seconds = GetGame().GetWorld().GetWorldTime() / 1000;
|
||||||
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
|
string line = string.Format("#%1 t=%2s M%3 %4 %5", ++m_Sequence, Math.Round(seconds * 10) / 10.0, messageId, kind, message) + " [session=" + m_Session + "]";
|
||||||
|
// Capture server-local wall time once, so history keeps the event time
|
||||||
|
// when a client opens the menu later or resumes a paused journal.
|
||||||
|
line = ERR_JournalText.Entry(m_Sequence, SCR_DateTimeHelper.GetTimeLocal(), kind, message, messageId) + "\n" + line;
|
||||||
if (m_Recent.Count() >= 300)
|
if (m_Recent.Count() >= 300)
|
||||||
m_Recent.Remove(0);
|
m_Recent.Remove(0);
|
||||||
m_Recent.Insert(line);
|
m_Recent.Insert(line);
|
||||||
@@ -84,7 +87,7 @@ class ERR_HQJournal
|
|||||||
if (!file)
|
if (!file)
|
||||||
{
|
{
|
||||||
m_RetryAt = now + 30000;
|
m_RetryAt = now + 30000;
|
||||||
PrintFormat("Escapists AI [HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
|
PrintFormat("Co10 Escape Reintroduced [AI / HQ log]: cannot open %1; retry in 30s, chat history remains available", Path(), level: LogLevel.ERROR);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
foreach (string line : m_Pending)
|
foreach (string line : m_Pending)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class ERR_HQStatusReport
|
|||||||
class ERR_HQWatchRules
|
class ERR_HQWatchRules
|
||||||
{
|
{
|
||||||
static int Aggression(int identifiedBodies) { return Math.Clamp(identifiedBodies / 3, 0, 30); }
|
static int Aggression(int identifiedBodies) { return Math.Clamp(identifiedBodies / 3, 0, 30); }
|
||||||
static int Hunters(int aggression) { return (Math.Clamp(aggression, 0, 30) + 1) / 2; }
|
static int Hunters(int aggression) { return 3 + (Math.Clamp(aggression, 0, 30) * 12 + 29) / 30; }
|
||||||
static bool Valid(ERR_HQStatusReport report, float now)
|
static bool Valid(ERR_HQStatusReport report, float now)
|
||||||
{
|
{
|
||||||
if (!report || report.SourceId.IsEmpty() || report.SourceId.Length() > 96 || report.MessageId <= 0)
|
if (!report || report.SourceId.IsEmpty() || report.SourceId.Length() > 96 || report.MessageId <= 0)
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// Preserve original hunter actors. Reboarding uses native AI tasks, never new
|
||||||
|
// crew, vehicle teleports, health restoration, or ammunition replacement.
|
||||||
|
class ERR_HunterCrewRecovery
|
||||||
|
{
|
||||||
|
protected ERR_HunterVehicle m_Unit;
|
||||||
|
protected ref SCR_AIVehicleBehavior m_Boarding;
|
||||||
|
protected IEntity m_BoardingActor;
|
||||||
|
protected float m_NextAttempt;
|
||||||
|
protected string m_LastReason;
|
||||||
|
protected bool m_Stopped;
|
||||||
|
string Reason;
|
||||||
|
void ERR_HunterCrewRecovery(ERR_HunterVehicle unit) { m_Unit = unit; }
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
m_Stopped = true;
|
||||||
|
if (m_Boarding) m_Boarding.Complete();
|
||||||
|
m_Boarding = null; m_BoardingActor = null;
|
||||||
|
}
|
||||||
|
bool Lost(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
if (!m_Unit.Group || !m_Unit.Entity) { Reason = "group or vehicle removed"; return true; }
|
||||||
|
if (!ERR_Actors.Alive(m_Unit.Entity)) { Reason = "vehicle destroyed"; return true; }
|
||||||
|
if (m_Unit.Group.GetFaction() != mode.EnemyFaction()) { Reason = "crew faction changed"; return true; }
|
||||||
|
foreach (IEntity member : m_Unit.Members)
|
||||||
|
if (ERR_Actors.Alive(member) && !ERR_Actors.Player(member) && ERR_Actors.Group(member) == m_Unit.Group) return false;
|
||||||
|
Reason = "no living original crew remain in the group";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected void Note(string reason)
|
||||||
|
{
|
||||||
|
Reason = reason;
|
||||||
|
if (reason == m_LastReason) return;
|
||||||
|
m_LastReason = reason;
|
||||||
|
ERR_HQ.Event("HUNTER CREW", m_Unit.Source + ": " + reason);
|
||||||
|
}
|
||||||
|
void Update(ERR_GameMode mode, float now)
|
||||||
|
{
|
||||||
|
if (m_Stopped || !Replication.IsServer() || !m_Unit || m_Unit.Released || Lost(mode)) return;
|
||||||
|
// Activate before admission: distant native compartment-spawn requests need
|
||||||
|
// live agents to finish. Do not wait for seated readiness to activate them.
|
||||||
|
m_Unit.Group.SetPermanentLOD(0); m_Unit.Group.ActivateAI();
|
||||||
|
array<AIAgent> agents = {}; m_Unit.Group.GetAgents(agents);
|
||||||
|
foreach (AIAgent agent : agents)
|
||||||
|
if (agent && ERR_Actors.CanFight(agent.GetControlledEntity())) { agent.SetPermanentLOD(0); agent.ActivateAI(); }
|
||||||
|
SCR_AIGroupUtilityComponent groupUtility = m_Unit.Group.GetGroupUtilityComponent();
|
||||||
|
SCR_AIVehicleUsageComponent usage = SCR_AIVehicleUsageComponent.Cast(m_Unit.Entity.FindComponent(SCR_AIVehicleUsageComponent));
|
||||||
|
if (groupUtility && groupUtility.m_VehicleMgr && usage) groupUtility.m_VehicleMgr.TryAddVehicle(usage);
|
||||||
|
if (m_Unit.CrewValid(mode))
|
||||||
|
{
|
||||||
|
if (m_Boarding) m_Boarding.Complete();
|
||||||
|
m_Boarding = null; m_BoardingActor = null;
|
||||||
|
Note("driver seated and conscious; patrol can continue"); return;
|
||||||
|
}
|
||||||
|
if (m_Unit.Seats.IsEmpty() || !m_Unit.Seats[0]) { Note("waiting for native driver compartment"); return; }
|
||||||
|
BaseCompartmentSlot seat = m_Unit.Seats[0];
|
||||||
|
if (!ERR_Actors.Alive(m_Unit.Driver) || ERR_Actors.Group(m_Unit.Driver) != m_Unit.Group)
|
||||||
|
{
|
||||||
|
foreach (IEntity survivor : m_Unit.Members)
|
||||||
|
if (ERR_Actors.CanFight(survivor) && !ERR_Actors.Player(survivor) && ERR_Actors.Group(survivor) == m_Unit.Group)
|
||||||
|
{ m_Unit.Driver = survivor; break; }
|
||||||
|
}
|
||||||
|
if (!ERR_Actors.CanFight(m_Unit.Driver)) { Note("driver incapacitated; retaining vehicle and destination while awaiting recovery"); return; }
|
||||||
|
if (seat.GetOccupant() && seat.GetOccupant() != m_Unit.Driver)
|
||||||
|
{ Note("driver seat occupied; original survivor waiting, no occupant forcibly removed"); return; }
|
||||||
|
SCR_ChimeraAIAgent driver = SCR_ChimeraAIAgent.Cast(SCR_AIUtils.GetAIAgent(m_Unit.Driver));
|
||||||
|
if (!driver || !driver.m_UtilityComponent) { Note("waiting for original driver's AI utility"); return; }
|
||||||
|
array<ref AIActionBase> actions = {}; driver.m_UtilityComponent.GetActions(actions);
|
||||||
|
if (m_Boarding && m_BoardingActor == m_Unit.Driver && actions.Contains(m_Boarding)) return;
|
||||||
|
if (now < m_NextAttempt) return;
|
||||||
|
m_NextAttempt = now + 10000;
|
||||||
|
if (m_Boarding) m_Boarding.Complete();
|
||||||
|
m_BoardingActor = m_Unit.Driver;
|
||||||
|
IEntity aboard = CompartmentAccessComponent.GetVehicleIn(m_Unit.Driver);
|
||||||
|
if (aboard)
|
||||||
|
{
|
||||||
|
m_Boarding = new SCR_AIGetOutVehicle(driver.m_UtilityComponent, null, aboard, priority: 1000);
|
||||||
|
Note("original survivor leaving another seat before taking the driver seat");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_Boarding = new SCR_AIGetInVehicle(driver.m_UtilityComponent, null, m_Unit.Entity, seat, EAICompartmentType.Pilot, 1000);
|
||||||
|
Note("original driver ordered to walk to the vehicle and board");
|
||||||
|
}
|
||||||
|
driver.m_UtilityComponent.AddAction(m_Boarding);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Deployment geometry only. Pursuit destinations always come from delivered HQ reports.
|
||||||
|
class ERR_HunterOpeningRules
|
||||||
|
{
|
||||||
|
static bool Spread(vector center, vector first, vector second)
|
||||||
|
{
|
||||||
|
vector a = first - center; a[1] = 0;
|
||||||
|
vector b = second - center; b[1] = 0;
|
||||||
|
return vector.DistanceXZ(center, first) >= 600 && vector.DistanceXZ(center, second) >= 600
|
||||||
|
&& vector.Dot(a, b) <= 0; // At least ninety degrees between actual road positions.
|
||||||
|
}
|
||||||
|
static bool FreshOrder(float observed, float queued, float now, float nextContact, bool pendingContact)
|
||||||
|
{
|
||||||
|
return !pendingContact && observed >= 0 && observed > queued && now >= nextContact;
|
||||||
|
}
|
||||||
|
static bool Select(vector center, array<vector> candidates, array<int> selected)
|
||||||
|
{
|
||||||
|
selected.Clear();
|
||||||
|
if (candidates.Count() > 64) return false;
|
||||||
|
for (int i = 0; i < candidates.Count(); i++)
|
||||||
|
for (int j = i + 1; j < candidates.Count(); j++)
|
||||||
|
{
|
||||||
|
if (!Spread(center, candidates[i], candidates[j])) continue;
|
||||||
|
for (int k = j + 1; k < candidates.Count(); k++)
|
||||||
|
if (Spread(center, candidates[i], candidates[k]) && Spread(center, candidates[j], candidates[k]))
|
||||||
|
{ selected.Insert(i); selected.Insert(j); selected.Insert(k); return true; }
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class ERR_HunterOpeningPlan
|
||||||
|
{
|
||||||
|
vector Center;
|
||||||
|
ref array<vector> Positions = {};
|
||||||
|
ref array<vector> Angles = {};
|
||||||
|
protected ref array<vector> m_Candidates = {};
|
||||||
|
protected ref array<vector> m_Angles = {};
|
||||||
|
protected int m_Scanned;
|
||||||
|
protected bool m_Done;
|
||||||
|
protected float m_Heading;
|
||||||
|
void ERR_HunterOpeningPlan(vector center) { Center = center; m_Heading = Math.RandomFloat(0, 360); }
|
||||||
|
bool Ready() { return Positions.Count() == 3; }
|
||||||
|
bool Done() { return m_Done; }
|
||||||
|
static bool AwayFromPlayers(vector position, array<IEntity> players)
|
||||||
|
{
|
||||||
|
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void Step(array<IEntity> players)
|
||||||
|
{
|
||||||
|
if (m_Done) return;
|
||||||
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||||
|
// Eight bounded road/footprint queries per HQ second; four rings, sixteen bearings.
|
||||||
|
for (int work = 0; work < 8 && m_Scanned < 64; work++)
|
||||||
|
{
|
||||||
|
int candidate = m_Scanned++;
|
||||||
|
int ring = candidate / 16;
|
||||||
|
int sector = candidate % 16;
|
||||||
|
float radius = 800 + ring * 450;
|
||||||
|
float bearing = (m_Heading + sector * 22.5) * Math.DEG2RAD;
|
||||||
|
vector seed = Center + Vector(Math.Sin(bearing) * radius, 0, Math.Cos(bearing) * radius);
|
||||||
|
vector position, angles;
|
||||||
|
if (!ERR_SupplyConvoySystem.ApproachDirected(seed, position, angles, 300)) continue;
|
||||||
|
if (vector.DistanceXZ(Center, position) < 600 || !AwayFromPlayers(position, players)) continue;
|
||||||
|
if (!ERR_SupplyConvoyRules.Separated(position, m_Candidates, 100) || !placement.Clear(position, angles[0])) continue;
|
||||||
|
m_Candidates.Insert(position); m_Angles.Insert(angles);
|
||||||
|
}
|
||||||
|
array<int> selected = {};
|
||||||
|
if (ERR_HunterOpeningRules.Select(Center, m_Candidates, selected))
|
||||||
|
{
|
||||||
|
foreach (int index : selected) { Positions.Insert(m_Candidates[index]); Angles.Insert(m_Angles[index]); }
|
||||||
|
m_Done = true;
|
||||||
|
ERR_HQ.Event("HUNTER OPENING", "three clear road starts reserved at least 90 degrees apart; center=" + Center.ToString());
|
||||||
|
}
|
||||||
|
else if (m_Scanned == 64)
|
||||||
|
{
|
||||||
|
m_Done = true;
|
||||||
|
ERR_HQ.Event("HUNTER OPENING FAILED", "no three safe, widely separated road starts in 64 candidates; unsafe spawns withheld");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,9 +6,23 @@ class ERR_HunterVehicle : ERR_ConvoyVehicle
|
|||||||
ref ERR_HQRoute Downlink;
|
ref ERR_HQRoute Downlink;
|
||||||
vector Destination;
|
vector Destination;
|
||||||
int SourceMessage;
|
int SourceMessage;
|
||||||
|
float LastContact = -1;
|
||||||
|
float NextContactOrder;
|
||||||
|
ref ERR_HunterCrewRecovery Recovery;
|
||||||
|
float NextMotionLog;
|
||||||
|
float ProgressAt;
|
||||||
|
vector ProgressPosition;
|
||||||
|
int Repaths;
|
||||||
|
override bool CrewValid(ERR_GameMode mode)
|
||||||
|
{
|
||||||
|
return Group && Entity && ERR_Actors.Alive(Entity) && Group.GetFaction() == mode.EnemyFaction()
|
||||||
|
&& Members.Contains(Driver) && ERR_Actors.CanFight(Driver) && !ERR_Actors.Player(Driver)
|
||||||
|
&& ERR_Actors.Group(Driver) == Group && Seats.Count() > 0 && Seats[0].GetOccupant() == Driver;
|
||||||
|
}
|
||||||
override void Release(ERR_GameMode mode)
|
override void Release(ERR_GameMode mode)
|
||||||
{
|
{
|
||||||
if (Released) return;
|
if (Released) return;
|
||||||
|
if (Recovery) Recovery.Stop();
|
||||||
if (!mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { super.Release(mode); return; }
|
if (!mode || !mode.Active() || ERR_ConvoyRules.Captured(Entity, Group)) { super.Release(mode); return; }
|
||||||
Released = true; Downlink = null; ClearOrder();
|
Released = true; Downlink = null; ClearOrder();
|
||||||
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
ERR_ForceRecord record = mode.Forces().FindGroup(Group);
|
||||||
@@ -33,7 +47,13 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
protected ref array<ref ERR_HunterVehicle> m_Hunters = {};
|
protected ref array<ref ERR_HunterVehicle> m_Hunters = {};
|
||||||
protected float m_NextHunter;
|
protected float m_NextHunter;
|
||||||
protected float m_NextSpawn;
|
protected float m_NextSpawn;
|
||||||
protected int m_Target;
|
protected int m_Target = 3;
|
||||||
|
protected ref ERR_HunterOpeningPlan m_Opening;
|
||||||
|
bool Contains(SCR_AIGroup group)
|
||||||
|
{
|
||||||
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (unit.Group == group) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
int Deployed() { return m_Hunters.Count(); }
|
int Deployed() { return m_Hunters.Count(); }
|
||||||
int ActiveHunters()
|
int ActiveHunters()
|
||||||
{
|
{
|
||||||
@@ -41,12 +61,22 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++;
|
foreach (ERR_HunterVehicle unit : m_Hunters) if (!unit.Released) count++;
|
||||||
return count;
|
return count;
|
||||||
}
|
}
|
||||||
override string Status() { return string.Format("aggression UAZs: %1 active, %2/%3 deployed allowance (maximum 15)", ActiveHunters(), Deployed(), m_Target); }
|
override string Status() { return string.Format("HQ UAZ patrols: %1 active; %2 deployed from a total allowance of %3 (maximum 15). Lost vehicles are not replaced.", ActiveHunters(), Deployed(), m_Target); }
|
||||||
protected bool StartHunter(ERR_HQ hq, float now)
|
protected bool StartHunter(ERR_HQ hq, float now)
|
||||||
{
|
{
|
||||||
if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
if (m_Hunters.Count() >= m_Target || m_Hunters.Count() >= 15 || m_Mode.Forces().Count() >= 250) return false;
|
||||||
array<IEntity> players = {}; ERR_Actors.Players(players);
|
array<IEntity> players = {}; ERR_Actors.Players(players);
|
||||||
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
ERR_ConvoyPlacement placement = new ERR_ConvoyPlacement();
|
||||||
|
if (m_Hunters.Count() < 3)
|
||||||
|
{
|
||||||
|
if (!m_Opening || !m_Opening.Ready()) return false;
|
||||||
|
int slot = m_Hunters.Count();
|
||||||
|
vector openingPosition = m_Opening.Positions[slot];
|
||||||
|
vector openingAngles = m_Opening.Angles[slot];
|
||||||
|
if (!ERR_HunterOpeningPlan.AwayFromPlayers(openingPosition, players) || !placement.Clear(openingPosition, openingAngles[0]))
|
||||||
|
{ ERR_HQ.Event("HUNTER WAIT", "reserved opening road occupied; no unsafe spawn"); return false; }
|
||||||
|
return SpawnHunter(hq, now, openingPosition, openingAngles, "prison-opening");
|
||||||
|
}
|
||||||
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
for (int i = 0; i < m_Mode.Sites().Count(); i++)
|
||||||
{
|
{
|
||||||
ERR_SiteRuntime site = m_Mode.Sites().At((i + m_Hunters.Count()) % m_Mode.Sites().Count());
|
ERR_SiteRuntime site = m_Mode.Sites().At((i + m_Hunters.Count()) % m_Mode.Sites().Count());
|
||||||
@@ -58,25 +88,31 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
bool nearPlayer;
|
bool nearPlayer;
|
||||||
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) nearPlayer = true;
|
foreach (IEntity player : players) if (player && vector.DistanceXZ(player.GetOrigin(), position) < 400) nearPlayer = true;
|
||||||
if (nearPlayer) continue;
|
if (nearPlayer) continue;
|
||||||
|
return SpawnHunter(hq, now, position, angles, site.Definition.Id);
|
||||||
|
}
|
||||||
|
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
protected bool SpawnHunter(ERR_HQ hq, float now, vector position, vector angles, string source)
|
||||||
|
{
|
||||||
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
ERR_HunterVehicle unit = new ERR_HunterVehicle();
|
||||||
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
unit.Source = "aggression-uaz-" + (m_Hunters.Count() + 1).ToString();
|
||||||
unit.Deadline = now + 30000; m_Hunters.Insert(unit);
|
unit.Deadline = now + 120000; m_Hunters.Insert(unit);
|
||||||
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
unit.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.GroupPrefab(), position));
|
||||||
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
unit.Entity = Vehicle.Cast(ERR_Actors.Spawn(ERR_ConvoyResources.VehiclePrefab(0), position, angles));
|
||||||
if (unit.Group) m_Mode.Own(unit.Group);
|
if (unit.Group) m_Mode.Own(unit.Group);
|
||||||
if (unit.Entity) m_Mode.Own(unit.Entity);
|
if (unit.Entity) m_Mode.Own(unit.Entity);
|
||||||
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
if (!unit.Group || !unit.Entity || !ERR_ConvoyRules.EmptyCrewTemplate(unit.Group) || !FillCrew(unit))
|
||||||
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
{ unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native vehicle/crew spawn failed; allowance consumed"); return false; }
|
||||||
|
unit.Recovery = new ERR_HunterCrewRecovery(unit);
|
||||||
|
unit.Recovery.Update(m_Mode, now);
|
||||||
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
FactionAffiliationComponent affiliation = FactionAffiliationComponent.Cast(unit.Entity.FindComponent(FactionAffiliationComponent));
|
||||||
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
if (affiliation) affiliation.SetAffiliatedFaction(m_Mode.EnemyFaction());
|
||||||
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
AICarMovementComponent movement = AICarMovementComponent.Cast(unit.Entity.FindComponent(AICarMovementComponent));
|
||||||
if (movement) movement.SetCruiseSpeed(45);
|
if (movement) movement.SetCruiseSpeed(45);
|
||||||
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 reason=identified bodies %2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, site.Definition.Id, position, unit.Members.Count()));
|
ERR_HQ.Event("HUNTER SPAWN", string.Format("%1 identified bodies=%2 aggression=%3/30 allowance=%4/15 base=%5 position=%6 crew=%7", unit.Source, hq.Watch().IdentifiedBodies(), hq.Watch().Aggression(), m_Target, source, position, unit.Members.Count()));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
ERR_HQ.Event("HUNTER WAIT", "deployment deferred: friendly road obstructed, occupied, budget full or players within 400m");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
protected bool AdmitHunter(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||||
{
|
{
|
||||||
if (!unit.CrewValid(m_Mode)) return false;
|
if (!unit.CrewValid(m_Mode)) return false;
|
||||||
@@ -97,8 +133,6 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
}
|
}
|
||||||
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
protected void QueueDestination(ERR_HunterVehicle unit, ERR_HQ hq, float now)
|
||||||
{
|
{
|
||||||
if (now < unit.NextOrder || unit.Downlink) return;
|
|
||||||
unit.NextOrder = now + 30000;
|
|
||||||
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
array<ref ERR_HQReport> reports = {}; hq.SupportContacts(reports, now);
|
||||||
ERR_HQBelief best;
|
ERR_HQBelief best;
|
||||||
foreach (ERR_HQReport report : reports)
|
foreach (ERR_HQReport report : reports)
|
||||||
@@ -106,12 +140,18 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
ERR_HQBelief belief = hq.Memory().Get(report.Target);
|
||||||
if (belief && belief.Actionable() && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
if (belief && belief.Actionable() && (!best || belief.ObservedAt > best.ObservedAt)) best = belief;
|
||||||
}
|
}
|
||||||
|
bool freshContact;
|
||||||
|
if (best) freshContact = ERR_HunterOpeningRules.FreshOrder(best.ObservedAt, unit.LastContact, now, unit.NextContactOrder, unit.Downlink && unit.SourceMessage > 0);
|
||||||
|
// A first/new delivered sighting preempts routine patrol, including an in-flight patrol order.
|
||||||
|
if (freshContact && unit.Downlink && unit.SourceMessage == 0) unit.Downlink = null;
|
||||||
|
if (unit.Downlink || (!freshContact && now < unit.NextOrder)) return;
|
||||||
|
unit.NextOrder = now + 30000;
|
||||||
vector destination, angles;
|
vector destination, angles;
|
||||||
string reason = "road patrol between friendly objectives";
|
string reason = "road patrol between friendly objectives";
|
||||||
unit.SourceMessage = 0;
|
unit.SourceMessage = 0;
|
||||||
if (best)
|
if (best)
|
||||||
{
|
{
|
||||||
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 500)) return;
|
if (!ERR_SupplyConvoySystem.ApproachDirected(best.PredictedPosition, destination, angles, 250)) return;
|
||||||
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
unit.SourceMessage = best.MessageId; reason = "intercept last delivered sighting; confidence=" + best.Confidence.ToString();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -128,6 +168,11 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
}
|
}
|
||||||
unit.Destination = destination;
|
unit.Destination = destination;
|
||||||
unit.Downlink = hq.Downlink(unit.Group, "HUNTER", unit.Source + " " + reason + " destination=" + destination.ToString(), unit.SourceMessage);
|
unit.Downlink = hq.Downlink(unit.Group, "HUNTER", unit.Source + " " + reason + " destination=" + destination.ToString(), unit.SourceMessage);
|
||||||
|
if (best)
|
||||||
|
{
|
||||||
|
unit.NextContactOrder = now + 5000;
|
||||||
|
if (unit.Downlink) unit.LastContact = best.ObservedAt;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
override void Tick(float now)
|
override void Tick(float now)
|
||||||
{
|
{
|
||||||
@@ -135,18 +180,46 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
m_NextHunter = now + 1000;
|
m_NextHunter = now + 1000;
|
||||||
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return;
|
ERR_HQ hq = ERR_HQ.Cast(m_Mode.Command()); if (!hq) return;
|
||||||
m_Target = hq.Watch().HunterTarget();
|
m_Target = hq.Watch().HunterTarget();
|
||||||
if (now >= m_NextSpawn && Deployed() < m_Target) { m_NextSpawn = now + 30000; StartHunter(hq, now); }
|
if (!m_Opening)
|
||||||
|
{
|
||||||
|
array<IEntity> openingPlayers = {}; ERR_Actors.Players(openingPlayers);
|
||||||
|
vector center = m_Mode.PrisonPosition(); vector sum; int count;
|
||||||
|
foreach (IEntity player : openingPlayers) if (player) { sum += player.GetOrigin(); count++; }
|
||||||
|
if (count > 0) center = sum / count;
|
||||||
|
m_Opening = new ERR_HunterOpeningPlan(center);
|
||||||
|
}
|
||||||
|
if (!m_Opening.Done())
|
||||||
|
{
|
||||||
|
array<IEntity> currentPlayers = {}; ERR_Actors.Players(currentPlayers);
|
||||||
|
m_Opening.Step(currentPlayers);
|
||||||
|
}
|
||||||
|
if (now >= m_NextSpawn && Deployed() < m_Target)
|
||||||
|
{
|
||||||
|
m_NextSpawn = now + 30000;
|
||||||
|
if (Deployed() < 3) m_NextSpawn = now + 1000;
|
||||||
|
StartHunter(hq, now);
|
||||||
|
}
|
||||||
foreach (ERR_HunterVehicle unit : m_Hunters)
|
foreach (ERR_HunterVehicle unit : m_Hunters)
|
||||||
{
|
{
|
||||||
if (unit.Released) continue;
|
if (unit.Released) continue;
|
||||||
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " player captured vehicle/crew; orders removed"); continue; }
|
if (ERR_ConvoyRules.Captured(unit.Entity, unit.Group)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " player captured vehicle/crew; orders removed"); continue; }
|
||||||
|
if (unit.Recovery && unit.Recovery.Lost(m_Mode))
|
||||||
|
{
|
||||||
|
string loss = unit.Recovery.Reason;
|
||||||
|
unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + ": " + loss + "; surviving actors and vehicle retained"); continue;
|
||||||
|
}
|
||||||
|
if (unit.Recovery) unit.Recovery.Update(m_Mode, now);
|
||||||
if (!unit.Registered)
|
if (!unit.Registered)
|
||||||
{
|
{
|
||||||
if (AdmitHunter(unit, hq, now)) continue;
|
if (AdmitHunter(unit, hq, now)) continue;
|
||||||
if (now >= unit.Deadline) { InspectReadiness(unit, now); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native readiness timed out"); }
|
if (now >= unit.Deadline) { InspectReadiness(unit, now); unit.Release(m_Mode); ERR_HQ.Event("HUNTER FAILED", unit.Source + " native readiness timed out"); }
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!unit.CrewValid(m_Mode)) { unit.Release(m_Mode); ERR_HQ.Event("HUNTER RELEASE", unit.Source + " vehicle or original crew no longer operational; no replacement spawn"); continue; }
|
// A temporary unconscious/dismounted driver suspends driving without
|
||||||
|
// throwing away the delivered destination or the original physical crew.
|
||||||
|
if (!unit.CrewValid(m_Mode)) continue;
|
||||||
|
ObserveMovement(unit, now);
|
||||||
|
QueueDestination(unit, hq, now);
|
||||||
if (unit.Downlink)
|
if (unit.Downlink)
|
||||||
{
|
{
|
||||||
int delivered = unit.Downlink.Advance(now, hq.Journal());
|
int delivered = unit.Downlink.Advance(now, hq.Journal());
|
||||||
@@ -157,14 +230,42 @@ class ERR_HunterService : ERR_ConvoyService
|
|||||||
unit.ClearOrder();
|
unit.ClearOrder();
|
||||||
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), unit.Destination));
|
||||||
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||||
|
else { ERR_HQ.Event("HUNTER ORDER FAILED", unit.Source + ": move waypoint could not be created"); unit.Downlink = null; continue; }
|
||||||
ERR_HQ.Event("HUNTER ORDER", unit.Source + " radio delivered; destination=" + unit.Destination.ToString(), unit.SourceMessage);
|
ERR_HQ.Event("HUNTER ORDER", unit.Source + " radio delivered; destination=" + unit.Destination.ToString(), unit.SourceMessage);
|
||||||
}
|
}
|
||||||
unit.Downlink = null;
|
unit.Downlink = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
QueueDestination(unit, hq, now);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
protected void ObserveMovement(ERR_HunterVehicle unit, float now)
|
||||||
|
{
|
||||||
|
vector position = unit.Entity.GetOrigin();
|
||||||
|
if (unit.ProgressAt == 0 || vector.DistanceXZ(position, unit.ProgressPosition) >= 15)
|
||||||
|
{ unit.ProgressAt = now; unit.ProgressPosition = position; unit.Repaths = 0; }
|
||||||
|
SCR_AIGroupUtilityComponent utility = unit.Group.GetGroupUtilityComponent();
|
||||||
|
bool combat = utility && utility.m_Perception && !utility.m_Perception.m_aTargets.IsEmpty();
|
||||||
|
bool travelling = unit.Waypoint && !unit.Waypoint.IsWithinCompletionRadius(unit.Group);
|
||||||
|
if (!travelling || combat) unit.ProgressAt = now;
|
||||||
|
if (now >= unit.NextMotionLog)
|
||||||
|
{
|
||||||
|
unit.NextMotionLog = now + 30000;
|
||||||
|
float speed;
|
||||||
|
if (unit.Entity.GetPhysics()) speed = unit.Entity.GetPhysics().GetVelocity().Length();
|
||||||
|
vector deliveredDestination;
|
||||||
|
if (unit.Waypoint) deliveredDestination = unit.Waypoint.GetOrigin();
|
||||||
|
ERR_HQ.Event("HUNTER MOVEMENT", string.Format("%1 position=%2 speed=%3m/s delivered destination=%4 travelling=%5 combat=%6 no progress=%7s replans=%8", unit.Source, position, speed, deliveredDestination, travelling, combat, (now - unit.ProgressAt) / 1000, unit.Repaths));
|
||||||
|
}
|
||||||
|
if (!travelling || combat || now - unit.ProgressAt < 60000 || unit.Repaths >= 3) return;
|
||||||
|
unit.ProgressAt = now; unit.Repaths++;
|
||||||
|
// Destination can already contain a newer order awaiting radio delivery.
|
||||||
|
// Retry only the waypoint the crew has actually received.
|
||||||
|
vector retryDestination = unit.Waypoint.GetOrigin();
|
||||||
|
unit.ClearOrder();
|
||||||
|
unit.Waypoint = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination));
|
||||||
|
if (unit.Waypoint) { unit.Waypoint.SetCompletionRadius(50); unit.Group.AddWaypoint(unit.Waypoint); }
|
||||||
|
ERR_HQ.Event("HUNTER REPATH", unit.Source + ": no movement for 60 seconds; native route requested again, attempt " + unit.Repaths.ToString());
|
||||||
|
}
|
||||||
override void Stop()
|
override void Stop()
|
||||||
{
|
{
|
||||||
m_Stopped = true;
|
m_Stopped = true;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class ERR_MortarLogistics
|
|||||||
protected ERR_SiteRuntime m_Site;
|
protected ERR_SiteRuntime m_Site;
|
||||||
protected IEntity m_Cache;
|
protected IEntity m_Cache;
|
||||||
protected SCR_AIGroup m_Group;
|
protected SCR_AIGroup m_Group;
|
||||||
|
protected ERR_SiteRuntime m_CrewSite;
|
||||||
protected ERR_MortarBattery m_Battery;
|
protected ERR_MortarBattery m_Battery;
|
||||||
protected IEntity m_OriginalGun;
|
protected IEntity m_OriginalGun;
|
||||||
protected SCR_AIGroup m_OriginalBatteryGroup;
|
protected SCR_AIGroup m_OriginalBatteryGroup;
|
||||||
@@ -88,6 +89,15 @@ class ERR_MortarLogistics
|
|||||||
foreach (IEntity item : items)
|
foreach (IEntity item : items)
|
||||||
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL) shells.Insert(item);
|
if (item && item.GetPrefabData() && item.GetPrefabData().GetPrefabName() == ERR_MortarBattery.SHELL) shells.Insert(item);
|
||||||
}
|
}
|
||||||
|
static ERR_SiteRuntime CourierSource(ERR_SiteRuntime site)
|
||||||
|
{
|
||||||
|
if (!site) return null;
|
||||||
|
if (site.Responders) return site.Responders;
|
||||||
|
// Historical sites explicitly authored a roaming patrol. A permanent-only
|
||||||
|
// garrison is never silently substituted when a mobile team is unavailable.
|
||||||
|
if (site.PatrolCycle) return site;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
bool Prepare()
|
bool Prepare()
|
||||||
{
|
{
|
||||||
if (!Replication.IsServer() || m_Started || m_Stopped) return false;
|
if (!Replication.IsServer() || m_Started || m_Stopped) return false;
|
||||||
@@ -98,8 +108,10 @@ class ERR_MortarLogistics
|
|||||||
// Production uses the existing guard outpost; no replacement car/cache is created.
|
// Production uses the existing guard outpost; no replacement car/cache is created.
|
||||||
m_Site = scenario.Sites().Find("everon-supply-2");
|
m_Site = scenario.Sites().Find("everon-supply-2");
|
||||||
if (!m_Site) m_Site = scenario.Sites().Find("everon-outpost-1");
|
if (!m_Site) m_Site = scenario.Sites().Find("everon-outpost-1");
|
||||||
if (!m_Site || !m_Site.Group || !m_Site.PatrolCycle || !scenario.Sites().Owns(m_Site.Container)) return false;
|
if (!m_Site || !scenario.Sites().Owns(m_Site.Container)) return false;
|
||||||
m_Cache = m_Site.Container; m_Group = m_Site.Group;
|
m_CrewSite = CourierSource(m_Site);
|
||||||
|
if (!m_CrewSite || !m_CrewSite.Group) { Print("Co10 Escape Reintroduced [mortar supplies]: source needs a mobile team; permanent defenders cannot be borrowed", LogLevel.ERROR); return false; }
|
||||||
|
m_Cache = m_Site.Container; m_Group = m_CrewSite.Group;
|
||||||
array<ref ERR_MortarBattery> batteries = {}; m_HQ.Mortars().Batteries(batteries);
|
array<ref ERR_MortarBattery> batteries = {}; m_HQ.Mortars().Batteries(batteries);
|
||||||
if (batteries.IsEmpty()) return false;
|
if (batteries.IsEmpty()) return false;
|
||||||
m_Battery = batteries[0]; m_OriginalGun = m_Battery.Gun; m_OriginalBatteryGroup = m_Battery.Group;
|
m_Battery = batteries[0]; m_OriginalGun = m_Battery.Gun; m_OriginalBatteryGroup = m_Battery.Group;
|
||||||
@@ -112,7 +124,7 @@ class ERR_MortarLogistics
|
|||||||
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
if (!agent || !ERR_Actors.CanFight(agent.GetControlledEntity())) return false;
|
||||||
m_OriginalMembers.Insert(agent.GetControlledEntity());
|
m_OriginalMembers.Insert(agent.GetControlledEntity());
|
||||||
}
|
}
|
||||||
if (m_OriginalMembers.Count() != m_Site.InitialGuardCount) return false;
|
if (m_OriginalMembers.Count() != m_CrewSite.InitialGuardCount) return false;
|
||||||
m_InitialDeadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
m_InitialDeadline = GetGame().GetWorld().GetWorldTime() + 15000;
|
||||||
// SiteService owns this existing cache and its contents through rollback/world teardown.
|
// SiteService owns this existing cache and its contents through rollback/world teardown.
|
||||||
// This bounded startup allocation is never reopened, including partial failure/theft.
|
// This bounded startup allocation is never reopened, including partial failure/theft.
|
||||||
@@ -151,7 +163,7 @@ class ERR_MortarLogistics
|
|||||||
}
|
}
|
||||||
protected bool OriginalGroup()
|
protected bool OriginalGroup()
|
||||||
{
|
{
|
||||||
if (!m_Site || m_Site.Group != m_Group || m_Site.Container != m_Cache || !m_Group || !m_Cache || m_Group.GetPlayerCount(true) > 0 || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
|
if (!m_Site || CourierSource(m_Site) != m_CrewSite || !m_CrewSite || m_CrewSite.Group != m_Group || m_Site.Container != m_Cache || !m_Group || !m_Cache || m_Group.GetPlayerCount(true) > 0 || m_Group.GetFaction() != m_Mode.EnemyFaction()) return false;
|
||||||
array<AIAgent> agents = {}; m_Group.GetAgents(agents); bool living;
|
array<AIAgent> agents = {}; m_Group.GetAgents(agents); bool living;
|
||||||
foreach (AIAgent agent : agents)
|
foreach (AIAgent agent : agents)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class ERR_MortarReadiness
|
|||||||
protected ERR_MortarBattery m_Battery;
|
protected ERR_MortarBattery m_Battery;
|
||||||
protected ref array<IEntity> m_Members = {};
|
protected ref array<IEntity> m_Members = {};
|
||||||
protected ref array<ref ERR_MortarInitialItem> m_Operations = {};
|
protected ref array<ref ERR_MortarInitialItem> m_Operations = {};
|
||||||
|
protected float m_StartedAt = -1;
|
||||||
protected bool m_Allocated;
|
protected bool m_Allocated;
|
||||||
protected bool m_Ready;
|
protected bool m_Ready;
|
||||||
protected bool m_Stopped;
|
protected bool m_Stopped;
|
||||||
@@ -106,7 +107,10 @@ class ERR_MortarReadiness
|
|||||||
if (!mode || !group || !m_Battery.Gun || group.GetFaction() != mode.EnemyFaction() || group.GetPlayerCount(true) > 0) return Fail("initial support group/gun/faction changed");
|
if (!mode || !group || !m_Battery.Gun || group.GetFaction() != mode.EnemyFaction() || group.GetPlayerCount(true) > 0) return Fail("initial support group/gun/faction changed");
|
||||||
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Battery.Gun.FindComponent(DamageManagerComponent));
|
DamageManagerComponent damage = DamageManagerComponent.Cast(m_Battery.Gun.FindComponent(DamageManagerComponent));
|
||||||
if (!damage || damage.IsDestroyed()) return Fail("initial support gun missing damage manager or destroyed");
|
if (!damage || damage.IsDestroyed()) return Fail("initial support gun missing damage manager or destroyed");
|
||||||
bool expired = now < m_Battery.SpawnedAt || now - m_Battery.SpawnedAt >= 90000;
|
// HQ is paused in prison. Time before the first eligible initialization poll
|
||||||
|
// must not consume the finite native expansion/inventory deadline.
|
||||||
|
if (m_StartedAt < 0) m_StartedAt = now;
|
||||||
|
bool expired = now < m_StartedAt || now - m_StartedAt >= 90000;
|
||||||
array<AIAgent> agents = {}; group.GetAgents(agents);
|
array<AIAgent> agents = {}; group.GetAgents(agents);
|
||||||
array<IEntity> physical = {};
|
array<IEntity> physical = {};
|
||||||
int gunners, operators;
|
int gunners, operators;
|
||||||
|
|||||||
@@ -354,7 +354,11 @@ class ERR_MortarSystem
|
|||||||
if (battery.StartupActivated && battery.Operational()) ready++;
|
if (battery.StartupActivated && battery.Operational()) ready++;
|
||||||
shells += battery.CountShells();
|
shells += battery.CountShells();
|
||||||
}
|
}
|
||||||
return string.Format("mortars %1/%2; HE shells %3", ready, m_Batteries.Count(), shells);
|
string text = string.Format("Mortars: %1 of %2 operational; %3 HE shells in inventory.", ready, m_Batteries.Count(), shells);
|
||||||
|
ERR_GameMode mode = ERR_GameMode.Get();
|
||||||
|
if (mode && mode.Phase() <= ERR_Phase.PRISON) text += " Crews activate after the prison gate is unlocked.";
|
||||||
|
else if (ready < m_Batteries.Count()) text += " Other batteries are still preparing or unavailable; check the journal.";
|
||||||
|
return text;
|
||||||
}
|
}
|
||||||
void Stop()
|
void Stop()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ modded class ERR_GameMode
|
|||||||
super.EOnInit(owner);
|
super.EOnInit(owner);
|
||||||
string outcome;
|
string outcome;
|
||||||
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "mortar-station") return;
|
if (!Replication.IsServer() || SCR_Global.IsEditMode() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "mortar-station") return;
|
||||||
m_ERRStationDeadline = System.GetTickCount() + 180000;
|
m_ERRStationDeadline = System.GetTickCount() + 300000;
|
||||||
GetGame().GetCallqueue().CallLater(ERR_StationProbeTick, 500, true);
|
GetGame().GetCallqueue().CallLater(ERR_StationProbeTick, 500, true);
|
||||||
}
|
}
|
||||||
protected bool ERR_StationCheck(bool condition, string detail)
|
protected bool ERR_StationCheck(bool condition, string detail)
|
||||||
@@ -35,6 +35,7 @@ modded class ERR_GameMode
|
|||||||
float now = GetWorld().GetWorldTime();
|
float now = GetWorld().GetWorldTime();
|
||||||
if (m_ERRStationStage == 0)
|
if (m_ERRStationStage == 0)
|
||||||
{
|
{
|
||||||
|
if (now < 110000) return; // Reproduce a real session spending over 90 seconds in prison.
|
||||||
array<ref ERR_MortarBattery> batteries = {}; ERR_MortarSystem.Get().Batteries(batteries);
|
array<ref ERR_MortarBattery> batteries = {}; ERR_MortarSystem.Get().Batteries(batteries);
|
||||||
if (batteries.IsEmpty()) return;
|
if (batteries.IsEmpty()) return;
|
||||||
m_ERRStationBattery = batteries[0];
|
m_ERRStationBattery = batteries[0];
|
||||||
@@ -45,6 +46,12 @@ modded class ERR_GameMode
|
|||||||
ERR_MortarStation station = m_ERRStationBattery.ERR_StationProbe();
|
ERR_MortarStation station = m_ERRStationBattery.ERR_StationProbe();
|
||||||
if (m_ERRStationStage == 1)
|
if (m_ERRStationStage == 1)
|
||||||
{
|
{
|
||||||
|
array<ref ERR_MortarBattery> initial = {}; ERR_MortarSystem.Get().Batteries(initial);
|
||||||
|
foreach (ERR_MortarBattery battery : initial)
|
||||||
|
{
|
||||||
|
if (!ERR_StationCheck(battery.SpawnFailure.IsEmpty(), "delayed prison release preserves mortar initialization deadline")) return;
|
||||||
|
if (!battery.StartupActivated) return;
|
||||||
|
}
|
||||||
if (!m_ERRStationBattery.StartupActivated || !station) return;
|
if (!m_ERRStationBattery.StartupActivated || !station) return;
|
||||||
m_ERRStationGunner = station.Gunner();
|
m_ERRStationGunner = station.Gunner();
|
||||||
if (!m_ERRStationGunner || CompartmentAccessComponent.GetVehicleIn(m_ERRStationGunner) != m_ERRStationBattery.Gun) return;
|
if (!m_ERRStationGunner || CompartmentAccessComponent.GetVehicleIn(m_ERRStationGunner) != m_ERRStationBattery.Gun) return;
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class ERR_NativeScoutProbe
|
|||||||
if (matches < 1 || matches != post.Magazines) return false;
|
if (matches < 1 || matches != post.Magazines) return false;
|
||||||
for (int other = 0; other < index; other++)
|
for (int other = 0; other < index; other++)
|
||||||
if (vector.DistanceXZ(post.Position, scouts.At(other).Position) < 900) return false;
|
if (vector.DistanceXZ(post.Position, scouts.At(other).Position) < 900) return false;
|
||||||
PrintFormat("Escapists [scout probe]: PASS post=%1 height=%2 full magazines=%3 physical manpack, return fire, reserved", index + 1, post.Position[1], matches);
|
PrintFormat("Co10 Escape Reintroduced [scout probe]: PASS post=%1 height=%2 full magazines=%3 physical manpack, return fire, reserved", index + 1, post.Position[1], matches);
|
||||||
}
|
}
|
||||||
ERR_HQReport report = new ERR_HQReport(); report.RoadStatus = "on road";
|
ERR_HQReport report = new ERR_HQReport(); report.RoadStatus = "on road";
|
||||||
ERR_HQReport snapshot = report.Snapshot(); report.RoadStatus = "off road";
|
ERR_HQReport snapshot = report.Snapshot(); report.RoadStatus = "off road";
|
||||||
@@ -47,7 +47,7 @@ class ERR_NativeScoutProbe
|
|||||||
if (!after.HasVelocity || after.Velocity != "1 0 0" || after.TravelDirection() != "E") return false;
|
if (!after.HasVelocity || after.Velocity != "1 0 0" || after.TravelDirection() != "E") return false;
|
||||||
after.ObservedAt = 42000; ERR_ScoutMotion.Estimate(after, before);
|
after.ObservedAt = 42000; ERR_ScoutMotion.Estimate(after, before);
|
||||||
if (after.HasVelocity) return false;
|
if (after.HasVelocity) return false;
|
||||||
Print("Escapists [scout probe]: PASS observed motion, direction, stale-sighting refusal and immutable road snapshot");
|
Print("Co10 Escape Reintroduced [scout probe]: PASS observed motion, direction, stale-sighting refusal and immutable road snapshot");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#ifdef WORKBENCH
|
||||||
|
class ERR_NativeTownService : ERR_TownGarrisons
|
||||||
|
{
|
||||||
|
ref array<vector> Blockers = {};
|
||||||
|
ERR_TownGarrison Selected;
|
||||||
|
vector Approach;
|
||||||
|
protected override void Players(out array<vector> positions)
|
||||||
|
{
|
||||||
|
super.Players(positions);
|
||||||
|
foreach (vector blocker : Blockers) positions.Insert(blocker);
|
||||||
|
}
|
||||||
|
bool Select(int count)
|
||||||
|
{
|
||||||
|
if (m_Towns.IsEmpty())
|
||||||
|
{
|
||||||
|
vector low, high; GetGame().GetWorldEntity().GetWorldBounds(low, high);
|
||||||
|
GetGame().GetWorld().QueryEntitiesByAABB(low - "0 5000 0", high + "0 5000 0", Remember, Settlement, EQueryEntitiesFlags.ALL);
|
||||||
|
}
|
||||||
|
foreach (ERR_TownGarrison town : m_Towns)
|
||||||
|
{
|
||||||
|
if (town.Finished) continue;
|
||||||
|
for (int i = 0; i < 24; i++)
|
||||||
|
{
|
||||||
|
float angle = i * 15 * Math.DEG2RAD;
|
||||||
|
vector point = ERR_Actors.Ground(town.Centre + Vector(Math.Cos(angle) * 690, 0, Math.Sin(angle) * 690));
|
||||||
|
if (!m_Placement.Clear(point, null)) continue;
|
||||||
|
Selected = town; town.Count = count; Approach = point; return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
override void Tick(float now)
|
||||||
|
{
|
||||||
|
if (m_Stopped || !Selected || now < m_Next) return;
|
||||||
|
m_Next = now + 2000;
|
||||||
|
array<vector> players = {}; Players(players);
|
||||||
|
Attempt(Selected, players, now);
|
||||||
|
}
|
||||||
|
bool BirthsSafe()
|
||||||
|
{
|
||||||
|
if (!Selected || !Selected.Admitted || m_Mode.ForceLiving(Selected.Group) != Selected.Count) return false;
|
||||||
|
array<vector> players = {}; Players(players);
|
||||||
|
array<AIAgent> agents = {}; Selected.Group.GetAgents(agents);
|
||||||
|
foreach (AIAgent agent : agents)
|
||||||
|
if (!agent || !agent.GetControlledEntity() || !ERR_TownSpawnRules.Safe(agent.GetControlledEntity().GetOrigin(), players)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
void ClearSquad()
|
||||||
|
{
|
||||||
|
ERR_Actors.DeleteSpawned(Selected.Group); Selected.Group = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_HQ
|
||||||
|
{
|
||||||
|
void ERR_ProbeTowns(ERR_TownGarrisons service) { m_Towns.Stop(); m_Towns = service; }
|
||||||
|
bool ERR_ProbeTownLinked(SCR_AIGroup group)
|
||||||
|
{
|
||||||
|
foreach (ERR_HQUnit unit : m_Units)
|
||||||
|
if (unit.Group == group) return !unit.HomeGuard && !unit.ReservedForSupport && unit.Eligible(m_Mode) && unit.StatusSampler.Bound();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modded class ERR_GameMode
|
||||||
|
{
|
||||||
|
protected ref ERR_NativeTownService m_ERRTownProbe;
|
||||||
|
protected int m_ERRTownStage, m_ERRTownDeadline;
|
||||||
|
protected float m_ERRTownWait;
|
||||||
|
protected override void EOnInit(IEntity owner)
|
||||||
|
{
|
||||||
|
super.EOnInit(owner);
|
||||||
|
string outcome;
|
||||||
|
if (SCR_Global.IsEditMode() || !Replication.IsServer() || !System.GetCLIParam("errSmokeOutcome", outcome) || outcome != "town-garrisons") return;
|
||||||
|
m_ERRTownDeadline = System.GetTickCount() + 240000;
|
||||||
|
GetGame().GetCallqueue().CallLater(ERR_TownProbeTick, 250, true);
|
||||||
|
}
|
||||||
|
protected bool ERR_TownCheck(bool ok, string message)
|
||||||
|
{
|
||||||
|
if (ok) { Print("ERR SMOKE PASS: towns " + message); return true; }
|
||||||
|
Print("ERR SMOKE FAIL: towns " + message, LogLevel.ERROR);
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_TownProbeTick); return false;
|
||||||
|
}
|
||||||
|
protected void ERR_TownProbeTick()
|
||||||
|
{
|
||||||
|
if (System.GetTickCount() >= m_ERRTownDeadline || !PreparationError().IsEmpty()) { ERR_TownCheck(false, "startup/test deadline: " + PreparationError()); return; }
|
||||||
|
if (!Active() || !GetGame().GetPlayerController()) return;
|
||||||
|
IEntity player = GetGame().GetPlayerController().GetControlledEntity();
|
||||||
|
if (!player) return;
|
||||||
|
DamageManagerComponent.Cast(player.FindComponent(DamageManagerComponent)).EnableDamageHandling(false);
|
||||||
|
ERR_HQ hq = ERR_HQ.Get(); if (!hq) return;
|
||||||
|
float now = GetWorld().GetWorldTime();
|
||||||
|
if (now < m_ERRTownWait) return;
|
||||||
|
if (m_ERRTownStage == 0)
|
||||||
|
{
|
||||||
|
m_ERRTownProbe = new ERR_NativeTownService(this); hq.ERR_ProbeTowns(m_ERRTownProbe);
|
||||||
|
if (!ERR_TownCheck(m_ERRTownProbe.Select(3), "found native named settlement with clear approach")) return;
|
||||||
|
if (!ERR_TownCheck(UnlockPrison(player), "normal prison release")) return;
|
||||||
|
player.SetOrigin(m_ERRTownProbe.Approach);
|
||||||
|
// A second teammate inside the town must veto the entire spawn, even
|
||||||
|
// while the approaching player satisfies the activation trigger.
|
||||||
|
m_ERRTownProbe.Blockers.Insert(m_ERRTownProbe.Selected.Centre);
|
||||||
|
m_ERRTownWait = now + 6000; m_ERRTownStage = 1; return;
|
||||||
|
}
|
||||||
|
if (m_ERRTownStage == 1)
|
||||||
|
{
|
||||||
|
if (!ERR_TownCheck(!m_ERRTownProbe.Selected.Admitted && (!m_ERRTownProbe.Selected.Group || m_ERRTownProbe.Selected.Group.GetAgentsCount() == 0), "nearby second teammate blocks all native character creation")) return;
|
||||||
|
m_ERRTownProbe.Blockers.Clear(); m_ERRTownStage = 2; return;
|
||||||
|
}
|
||||||
|
if (m_ERRTownStage == 2 || m_ERRTownStage == 4)
|
||||||
|
{
|
||||||
|
if (!m_ERRTownProbe.Selected.Admitted) return;
|
||||||
|
if (!ERR_TownCheck(m_ERRTownProbe.BirthsSafe(), string.Format("%1 physical soldiers all at least 700 m from player", m_ERRTownProbe.Selected.Count))) return;
|
||||||
|
if (!ERR_TownCheck(hq.ERR_ProbeTownLinked(m_ERRTownProbe.Selected.Group), "squad bound to VirtualHQ reporting and available for hunt orders")) return;
|
||||||
|
if (!ERR_TownCheck(m_ERRTownProbe.Selected.Group.m_aUnitPrefabSlots.IsEmpty(), "no native expansion slots or delayed replacement soldiers")) return;
|
||||||
|
m_ERRTownProbe.ClearSquad(); m_ERRTownWait = now + 5000; m_ERRTownStage++; return;
|
||||||
|
}
|
||||||
|
if (m_ERRTownStage == 3)
|
||||||
|
{
|
||||||
|
if (!ERR_TownCheck(!m_ERRTownProbe.Selected.Group && m_ERRTownProbe.Selected.Finished, "eliminated three-man squad is not replaced")) return;
|
||||||
|
if (!ERR_TownCheck(m_ERRTownProbe.Select(10), "second distinct native town selected")) return;
|
||||||
|
player.SetOrigin(m_ERRTownProbe.Approach); m_ERRTownStage = 4; return;
|
||||||
|
}
|
||||||
|
if (!ERR_TownCheck(!m_ERRTownProbe.Selected.Group && m_ERRTownProbe.Selected.Finished, "eliminated ten-man squad is not replaced")) return;
|
||||||
|
m_ERRTownProbe.Stop();
|
||||||
|
GetGame().GetCallqueue().Remove(ERR_TownProbeTick);
|
||||||
|
Print("ERR SMOKE COMPLETE: native towns, 3 and 10 soldiers, split-player exclusion, HQ integration and no replacement waves");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -24,7 +24,7 @@ class ERR_ObjectiveDefence
|
|||||||
static bool Objective(int kind)
|
static bool Objective(int kind)
|
||||||
{
|
{
|
||||||
return kind == ERR_LocationKind.RADIO || kind == ERR_LocationKind.DEPOT
|
return kind == ERR_LocationKind.RADIO || kind == ERR_LocationKind.DEPOT
|
||||||
|| kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.EXTRACTION;
|
|| kind == ERR_LocationKind.HOSPITAL || kind == ERR_LocationKind.OUTPOST || kind == ERR_LocationKind.EXTRACTION;
|
||||||
}
|
}
|
||||||
protected void Initialize()
|
protected void Initialize()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ class ERR_ReconAirports
|
|||||||
m_bComplete = m_vCursor[2] >= m_vMax[2];
|
m_bComplete = m_vCursor[2] >= m_vMax[2];
|
||||||
if (m_bComplete)
|
if (m_bComplete)
|
||||||
{
|
{
|
||||||
PrintFormat("Escapists AI [air recon]: airport search finished; airports=%1", m_Positions.Count());
|
PrintFormat("Co10 Escape Reintroduced [AI / air recon]: airport search finished; airports=%1", m_Positions.Count());
|
||||||
for (int airportIndex = 0; airportIndex < m_Positions.Count(); airportIndex++)
|
for (int airportIndex = 0; airportIndex < m_Positions.Count(); airportIndex++)
|
||||||
PrintFormat("Escapists AI [air recon]: airport=%1 centre=%2 runway/tower samples=%3", airportIndex, m_Positions[airportIndex], m_PositionSamples[airportIndex]);
|
PrintFormat("Co10 Escape Reintroduced [AI / air recon]: airport=%1 centre=%2 runway/tower samples=%3", airportIndex, m_Positions[airportIndex], m_PositionSamples[airportIndex]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +74,7 @@ class ERR_ReconAirports
|
|||||||
}
|
}
|
||||||
m_Positions.Insert(entity.GetOrigin());
|
m_Positions.Insert(entity.GetOrigin());
|
||||||
m_PositionSamples.Insert(1);
|
m_PositionSamples.Insert(1);
|
||||||
PrintFormat("Escapists AI [air recon]: airport structure=%1 position=%2", entity.GetPrefabData().GetPrefabName(), entity.GetOrigin());
|
PrintFormat("Co10 Escape Reintroduced [AI / air recon]: airport structure=%1 position=%2", entity.GetPrefabData().GetPrefabName(), entity.GetOrigin());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ class ERR_ScoutSystem
|
|||||||
if (ERR_ScoutTerrain.Separated(point, m_Posts, m_Definition) && ERR_ScoutTerrain.Survey(point, m_IslandHigh, heading))
|
if (ERR_ScoutTerrain.Separated(point, m_Posts, m_Definition) && ERR_ScoutTerrain.Survey(point, m_IslandHigh, heading))
|
||||||
{
|
{
|
||||||
ERR_ScoutPost post = new ERR_ScoutPost(); post.Position = point; post.Heading = heading; m_Posts.Insert(post);
|
ERR_ScoutPost post = new ERR_ScoutPost(); post.Position = point; post.Heading = heading; m_Posts.Insert(post);
|
||||||
PrintFormat("Escapists [scout survey]: post=%1 position=%2 heading=%3", m_Posts.Count(), point, heading);
|
PrintFormat("Co10 Escape Reintroduced [scout survey]: post=%1 position=%2 heading=%3", m_Posts.Count(), point, heading);
|
||||||
}
|
}
|
||||||
if (System.GetTickCount() - tick >= 6) return ERR_PrepareResult.PENDING;
|
if (System.GetTickCount() - tick >= 6) return ERR_PrepareResult.PENDING;
|
||||||
}
|
}
|
||||||
@@ -218,7 +218,7 @@ class ERR_ScoutSystem
|
|||||||
record.Pinned = true;
|
record.Pinned = true;
|
||||||
m_Mode.RegisterGroup(post.Group);
|
m_Mode.RegisterGroup(post.Group);
|
||||||
post.Ready = true;
|
post.Ready = true;
|
||||||
PrintFormat("Escapists [scout ready]: position=%1 backpack magazines=%2", post.Position, post.Magazines);
|
PrintFormat("Co10 Escape Reintroduced [scout ready]: position=%1 backpack magazines=%2", post.Position, post.Magazines);
|
||||||
return ERR_PrepareResult.READY;
|
return ERR_PrepareResult.READY;
|
||||||
}
|
}
|
||||||
ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
|
ERR_PrepareResult Prepare(ERR_ScenarioDefinition definition)
|
||||||
|
|||||||
@@ -0,0 +1,217 @@
|
|||||||
|
// Town activation is only a population trigger, never an observation for HQ.
|
||||||
|
class ERR_TownSpawnRules
|
||||||
|
{
|
||||||
|
static const float DISTANCE = 700;
|
||||||
|
static const float TOWN_RADIUS = 200;
|
||||||
|
static bool Safe(vector position, array<vector> players, float minimum = 700)
|
||||||
|
{
|
||||||
|
if (!players || players.IsEmpty() || !ERR_ForceRules.FiniteVector(position)) return false;
|
||||||
|
foreach (vector player : players)
|
||||||
|
if (!ERR_ForceRules.FiniteVector(player) || vector.DistanceXZ(position, player) < minimum) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool Approached(vector centre, array<vector> players)
|
||||||
|
{
|
||||||
|
if (!players || !ERR_ForceRules.FiniteVector(centre)) return false;
|
||||||
|
foreach (vector player : players)
|
||||||
|
if (ERR_ForceRules.FiniteVector(player) && vector.DistanceXZ(centre, player) <= DISTANCE) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ERR_TownGarrison
|
||||||
|
{
|
||||||
|
vector Centre;
|
||||||
|
string Id;
|
||||||
|
string Name;
|
||||||
|
int Count;
|
||||||
|
bool Finished;
|
||||||
|
bool Admitted;
|
||||||
|
float NextLog;
|
||||||
|
SCR_AIGroup Group;
|
||||||
|
AIWaypoint Hold;
|
||||||
|
}
|
||||||
|
|
||||||
|
// One finite squad per named settlement per round. Empty native groups have no
|
||||||
|
// queued slots: each character is created synchronously after a fresh distance
|
||||||
|
// check, so native expansion cannot introduce late soldiers beside a player.
|
||||||
|
class ERR_TownGarrisons
|
||||||
|
{
|
||||||
|
protected ERR_GameMode m_Mode;
|
||||||
|
protected ref array<ref ERR_TownGarrison> m_Towns = {};
|
||||||
|
protected ref ERR_SpawnPlacement m_Placement = new ERR_SpawnPlacement();
|
||||||
|
protected float m_Next;
|
||||||
|
protected int m_Cursor;
|
||||||
|
protected bool m_Stopped;
|
||||||
|
void ERR_TownGarrisons(ERR_GameMode mode) { m_Mode = mode; }
|
||||||
|
|
||||||
|
protected void Players(out array<vector> positions)
|
||||||
|
{
|
||||||
|
positions.Clear();
|
||||||
|
array<int> ids = {}; GetGame().GetPlayerManager().GetPlayers(ids);
|
||||||
|
foreach (int id : ids)
|
||||||
|
{
|
||||||
|
IEntity actor = GetGame().GetPlayerManager().GetPlayerControlledEntity(id);
|
||||||
|
// Include unconscious and dead controlled bodies: do not spawn beside a
|
||||||
|
// teammate just because that teammate temporarily cannot fight.
|
||||||
|
if (actor) positions.Insert(actor.GetOrigin());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
protected bool Settlement(IEntity entity)
|
||||||
|
{
|
||||||
|
if (!entity) return false;
|
||||||
|
MapDescriptorComponent descriptor = MapDescriptorComponent.Cast(entity.FindComponent(MapDescriptorComponent));
|
||||||
|
if (!descriptor) return false;
|
||||||
|
int type = descriptor.GetBaseType();
|
||||||
|
return type == EMapDescriptorType.MDT_NAME_CITY || type == EMapDescriptorType.MDT_NAME_TOWN
|
||||||
|
|| type == EMapDescriptorType.MDT_NAME_VILLAGE || type == EMapDescriptorType.MDT_NAME_SETTLEMENT;
|
||||||
|
}
|
||||||
|
protected bool Remember(IEntity entity)
|
||||||
|
{
|
||||||
|
vector centre = entity.GetOrigin();
|
||||||
|
foreach (ERR_TownGarrison known : m_Towns)
|
||||||
|
if (vector.DistanceXZ(known.Centre, centre) < 50) return true;
|
||||||
|
if (m_Towns.Count() >= 256) return false;
|
||||||
|
ERR_TownGarrison town = new ERR_TownGarrison();
|
||||||
|
town.Centre = centre; town.Count = Math.RandomIntInclusive(3, 10);
|
||||||
|
town.Id = "town-" + m_Towns.Count().ToString();
|
||||||
|
town.Name = entity.GetName(); if (town.Name.IsEmpty()) town.Name = town.Id;
|
||||||
|
m_Towns.Insert(town);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
protected void Discover(array<vector> players)
|
||||||
|
{
|
||||||
|
foreach (vector player : players)
|
||||||
|
GetGame().GetWorld().QueryEntitiesByAABB(player - "700 5000 700", player + "700 5000 700", Remember, Settlement, EQueryEntitiesFlags.ALL);
|
||||||
|
}
|
||||||
|
bool Contains(SCR_AIGroup group)
|
||||||
|
{
|
||||||
|
if (!group) return false;
|
||||||
|
foreach (ERR_TownGarrison town : m_Towns) if (town.Group == group && town.Admitted) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
protected void Waiting(ERR_TownGarrison town, float now, string reason)
|
||||||
|
{
|
||||||
|
if (now < town.NextLog) return;
|
||||||
|
town.NextLog = now + 60000;
|
||||||
|
ERR_HQ.Event("TOWN GARRISON", string.Format("%1 at %2: waiting to place %3 soldiers; %4. No nearby soldiers spawned.", town.Name, town.Centre, town.Count, reason));
|
||||||
|
}
|
||||||
|
protected bool Plan(ERR_TownGarrison town, array<vector> players, out array<vector> positions)
|
||||||
|
{
|
||||||
|
positions.Clear();
|
||||||
|
AIPathfindingComponent path = AIPathfindingComponent.Cast(town.Group.FindComponent(AIPathfindingComponent));
|
||||||
|
if (!path) return false;
|
||||||
|
NavmeshWorldComponent navmesh = path.GetNavmeshComponent();
|
||||||
|
int first = Math.RandomInt(0, 24);
|
||||||
|
for (int ring = 0; ring < 6; ring++)
|
||||||
|
for (int direction = 0; direction < 24; direction++)
|
||||||
|
{
|
||||||
|
float angle = ((first + direction) * 15) * Math.DEG2RAD;
|
||||||
|
float radius = Math.Min(ring * 40, 185);
|
||||||
|
vector anchor = ERR_Actors.Ground(town.Centre + Vector(Math.Cos(angle) * radius, 0, Math.Sin(angle) * radius));
|
||||||
|
if (!ERR_TownSpawnRules.Safe(anchor, players, 710)) continue;
|
||||||
|
if (navmesh && !navmesh.IsTileLoaded(anchor)) { navmesh.LoadTileIn(anchor); continue; }
|
||||||
|
positions.Clear();
|
||||||
|
map<int, vector> reserved = new map<int, vector>();
|
||||||
|
for (int slot = 0; slot < town.Count; slot++)
|
||||||
|
{
|
||||||
|
int column = slot % 4;
|
||||||
|
int row = slot / 4;
|
||||||
|
vector point = ERR_Actors.Ground(anchor + Vector(column * 3 - 4.5, 0, row * 3 - 3));
|
||||||
|
vector navigable;
|
||||||
|
if (!path.GetClosestPositionOnNavmesh(point, "3 3 3", navigable)) break;
|
||||||
|
point = ERR_Actors.Ground(navigable);
|
||||||
|
if (vector.DistanceXZ(point, town.Centre) > ERR_TownSpawnRules.TOWN_RADIUS || !ERR_TownSpawnRules.Safe(point, players, 702) || !m_Placement.Clear(point, reserved)) break;
|
||||||
|
positions.Insert(point); reserved.Set(slot, point);
|
||||||
|
}
|
||||||
|
if (positions.Count() == town.Count) return true;
|
||||||
|
}
|
||||||
|
positions.Clear(); return false;
|
||||||
|
}
|
||||||
|
protected void Attempt(ERR_TownGarrison town, array<vector> players, float now)
|
||||||
|
{
|
||||||
|
if (town.Finished || !ERR_TownSpawnRules.Approached(town.Centre, players)) return;
|
||||||
|
if (m_Mode.Forces().Count() >= 250) { Waiting(town, now, "mission force budget is full"); return; }
|
||||||
|
if (!town.Group)
|
||||||
|
{
|
||||||
|
town.Group = SCR_AIGroup.Cast(ERR_Actors.Spawn("{EC9F1AA42D0B7863}Prefabs/ERR/PrisonPatrol.et", town.Centre));
|
||||||
|
if (!town.Group) { town.Finished = true; return; }
|
||||||
|
m_Mode.Own(town.Group);
|
||||||
|
town.Group.SetFaction(m_Mode.EnemyFaction());
|
||||||
|
town.Group.SetSpawnImmediately(false); town.Group.SetDeleteWhenEmpty(false);
|
||||||
|
town.Group.SetPermanentLOD(0);
|
||||||
|
}
|
||||||
|
array<vector> positions = {};
|
||||||
|
if (!Plan(town, players, positions)) { Waiting(town, now, "no clear navigable town position at least 700 m from every player"); return; }
|
||||||
|
Resource profile = Resource.Load(m_Mode.FactionSelection().Choice("enemy").Group);
|
||||||
|
array<ResourceName> roster = {};
|
||||||
|
IEntitySource source;
|
||||||
|
if (profile && profile.IsValid()) source = profile.GetResource().ToEntitySource();
|
||||||
|
if (!source || !source.Get("m_aUnitPrefabSlots", roster) || roster.IsEmpty()) { Fail(town, "enemy faction has no infantry roster"); return; }
|
||||||
|
// Load every resource before the final checks, not part-way through creation.
|
||||||
|
array<ref Resource> loaded = {};
|
||||||
|
foreach (ResourceName prefab : roster)
|
||||||
|
{
|
||||||
|
Resource resource = Resource.Load(prefab);
|
||||||
|
if (!resource || !resource.IsValid()) { Fail(town, "enemy soldier prefab could not be loaded"); return; }
|
||||||
|
loaded.Insert(resource);
|
||||||
|
}
|
||||||
|
Players(players);
|
||||||
|
foreach (vector planned : positions)
|
||||||
|
if (!ERR_TownSpawnRules.Safe(planned, players)) { Waiting(town, now, "a player approached during preparation"); return; }
|
||||||
|
town.Finished = true;
|
||||||
|
float nearest = 1e20;
|
||||||
|
for (int index = 0; index < positions.Count(); index++)
|
||||||
|
{
|
||||||
|
Players(players);
|
||||||
|
if (!ERR_TownSpawnRules.Safe(positions[index], players)) { Fail(town, "player entered the exclusion area during creation"); return; }
|
||||||
|
IEntity actor = ERR_Actors.Spawn(roster[index % roster.Count()], positions[index]);
|
||||||
|
if (!actor) { Fail(town, "native soldier creation failed"); return; }
|
||||||
|
m_Mode.Own(actor);
|
||||||
|
if (!ERR_TownSpawnRules.Safe(actor.GetOrigin(), players) || !ERR_Actors.SetFaction(actor, m_Mode.EnemyFaction()) || !town.Group.AddAIEntityToGroup(actor))
|
||||||
|
{ ERR_Actors.DeleteSpawned(actor); Fail(town, "native soldier placement or group admission failed"); return; }
|
||||||
|
foreach (vector observer : players) nearest = Math.Min(nearest, vector.DistanceXZ(actor.GetOrigin(), observer));
|
||||||
|
}
|
||||||
|
town.Group.SetOrigin(positions[0]);
|
||||||
|
ERR_ForceRecord record = m_Mode.Forces().Register(town.Id, ERR_ForceKind.GARRISON, town.Centre, town.Count, null, town.Group, now);
|
||||||
|
if (!record) { Fail(town, "force ledger refused the squad"); return; }
|
||||||
|
town.Admitted = true;
|
||||||
|
town.Hold = AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.DefendWaypoint(), positions[0]));
|
||||||
|
if (town.Hold) { m_Mode.Own(town.Hold); town.Hold.SetCompletionRadius(60); town.Group.AddWaypoint(town.Hold); }
|
||||||
|
m_Mode.RegisterGroup(town.Group); town.Group.ActivateAI();
|
||||||
|
ERR_HQ.Event("TOWN GARRISON", string.Format("%1 at %2: spawned %3 enemy soldiers; nearest player at creation %4 m. All report to VirtualHQ and are available for orders. One squad this round; no replacements.", town.Name, town.Centre, town.Count, Math.Round(nearest)));
|
||||||
|
}
|
||||||
|
protected void Fail(ERR_TownGarrison town, string reason)
|
||||||
|
{
|
||||||
|
town.Finished = true;
|
||||||
|
ERR_Actors.DeleteSpawned(town.Group); town.Group = null;
|
||||||
|
ERR_HQ.Event("TOWN GARRISON", town.Name + ": creation cancelled: " + reason + ". No replacement wave.");
|
||||||
|
}
|
||||||
|
void Tick(float now)
|
||||||
|
{
|
||||||
|
if (m_Stopped || !Replication.IsServer() || !m_Mode.Active() || m_Mode.Phase() <= ERR_Phase.PRISON || now < m_Next) return;
|
||||||
|
m_Next = now + 2000;
|
||||||
|
array<vector> players = {}; Players(players);
|
||||||
|
if (players.IsEmpty()) return;
|
||||||
|
Discover(players);
|
||||||
|
// At most one finite squad per update; round-robin avoids a blocked town
|
||||||
|
// preventing other settlements from activating for separated teammates.
|
||||||
|
for (int checked = 0; checked < m_Towns.Count(); checked++)
|
||||||
|
{
|
||||||
|
m_Cursor = m_Cursor % m_Towns.Count();
|
||||||
|
ERR_TownGarrison town = m_Towns[m_Cursor++];
|
||||||
|
if (town.Finished || !ERR_TownSpawnRules.Approached(town.Centre, players)) continue;
|
||||||
|
Attempt(town, players, now); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void Stop()
|
||||||
|
{
|
||||||
|
m_Stopped = true;
|
||||||
|
foreach (ERR_TownGarrison town : m_Towns)
|
||||||
|
{
|
||||||
|
if (town.Hold) { if (town.Group) town.Group.RemoveWaypoint(town.Hold); ERR_Actors.DeleteSpawned(town.Hold); town.Hold = null; }
|
||||||
|
// Admitted units remain under normal mission/body ownership until teardown.
|
||||||
|
if (!town.Admitted) { ERR_Actors.DeleteSpawned(town.Group); town.Group = null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,7 @@ class ERR_IntelDiscoveryText
|
|||||||
static string Type(int kind, string id)
|
static string Type(int kind, string id)
|
||||||
{
|
{
|
||||||
if (kind == ERR_LocationKind.RADIO) return "radio station";
|
if (kind == ERR_LocationKind.RADIO) return "radio station";
|
||||||
|
if (kind == ERR_LocationKind.HOSPITAL) return "field hospital";
|
||||||
if (kind == ERR_LocationKind.OUTPOST) return "outpost";
|
if (kind == ERR_LocationKind.OUTPOST) return "outpost";
|
||||||
if (kind == ERR_LocationKind.MORTAR) return "mortar site";
|
if (kind == ERR_LocationKind.MORTAR) return "mortar site";
|
||||||
if (id.Contains("medical")) return "medical stash";
|
if (id.Contains("medical")) return "medical stash";
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ FrameWidgetClass {
|
|||||||
FrameWidgetClass { Name "SettingsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "SettingsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "SettingsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "SettingsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Mission objectives and traffic. Choose a value or Random, then Apply." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Type a value or choose Random, then press Apply. Random stays saved and rolls once each round." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Label_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -108 }
|
Name "Label_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -108 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Radio defence (30-1800 seconds)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "Radio transmission defence (30-1800 s)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,6 +38,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -156 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Seconds to defend the transmitting radio. Changes also affect a defence already in progress." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
|
Name "radioDefenceSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
|
||||||
components {
|
components {
|
||||||
@@ -74,6 +80,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 224 OffsetTop 224 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -244 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Convoys per round, not vehicles. 0 disables automatic convoys; lost convoys stay lost. Next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
|
Name "convoyCount" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
|
||||||
components {
|
components {
|
||||||
@@ -99,31 +111,55 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "MissionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 272 OffsetTop 272 SizeX 936 OffsetRight -964 SizeY 60 OffsetBottom -332 }
|
Name "WeaponRetentionLabel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 264 OffsetTop 264 SizeX 500 OffsetRight -528 SizeY 32 OffsetBottom -296 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Convoy count applies next round. Use the other pages above for world, intel and HQ settings." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Keep weapon when unconscious" "Font Size" 20 "Min Font Size" 20
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Saved_keepWeaponOnUnconscious" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 308 OffsetTop 308 SizeX 936 OffsetRight -964 SizeY 26 OffsetBottom -334 }
|
||||||
|
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 17 "Min Font Size" 17 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "KeepWeaponOn" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 264 OffsetTop 264 SizeX 140 OffsetRight -800 SizeY 42 OffsetBottom -306 }
|
||||||
|
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Enabled" } }
|
||||||
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "KeepWeaponOff" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 812 OffsetLeft 812 PositionY 264 OffsetTop 264 SizeX 152 OffsetRight -964 SizeY 42 OffsetBottom -306 }
|
||||||
|
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Disabled" } }
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "MissionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 340 OffsetTop 340 SizeX 936 OffsetRight -964 SizeY 90 OffsetBottom -430 }
|
||||||
|
Color 0.9 0.92 0.94 1 Text "Enabled: your held firearm stays with you and is equipped after revival. Disabled: normal weapon dropping. Applies from the next knockout; existing casualties keep their chosen rule." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
} }
|
} }
|
||||||
FrameWidgetClass { Name "HQPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "HQPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "HQHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "HQHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Control how often HQ receives reports and how it assigns ground support." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Enemy HQ response and soldier equipment. Percentages are chances per soldier, not exact headcounts." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Label_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 80 OffsetTop 80 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -108 }
|
Name "Label_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 72 OffsetTop 72 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -100 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Report interval (5-60 seconds)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "Sighting-report interval (5-60 s)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Saved_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 112 OffsetTop 112 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -136 }
|
Name "Saved_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 104 OffsetTop 104 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -128 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 128 OffsetTop 128 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -148 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Time between sighting-report attempts. Radio transmission takes extra time. Applies immediately." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
|
Name "reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 72 OffsetTop 72 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -114 }
|
||||||
components {
|
components {
|
||||||
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||||
m_sLabel ""
|
m_sLabel ""
|
||||||
@@ -131,7 +167,7 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "Apply_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 80 OffsetTop 80 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -122 }
|
Name "Apply_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 72 OffsetTop 72 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -114 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Apply"
|
m_sText "Apply"
|
||||||
@@ -139,7 +175,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "Random_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 80 OffsetTop 80 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -122 }
|
Name "Random_reportIntervalSeconds" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 72 OffsetTop 72 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -114 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Random"
|
m_sText "Random"
|
||||||
@@ -147,19 +183,25 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Label_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 168 OffsetTop 168 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -196 }
|
Name "Label_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 150 OffsetTop 150 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -178 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Support squad limit (0-8)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "HQ ground support squads (0-8)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Saved_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 200 OffsetTop 200 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -224 }
|
Name "Saved_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 182 OffsetTop 182 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -206 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 206 OffsetTop 206 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -226 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Base limit for HQ response squads; identified losses can raise it. 0 disables this response." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
|
Name "supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 150 OffsetTop 150 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -192 }
|
||||||
components {
|
components {
|
||||||
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||||
m_sLabel ""
|
m_sLabel ""
|
||||||
@@ -167,7 +209,7 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "Apply_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 168 OffsetTop 168 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -210 }
|
Name "Apply_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 150 OffsetTop 150 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -192 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Apply"
|
m_sText "Apply"
|
||||||
@@ -175,7 +217,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "Random_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 168 OffsetTop 168 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -210 }
|
Name "Random_supportSquadLimit" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 150 OffsetTop 150 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -192 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Random"
|
m_sText "Random"
|
||||||
@@ -183,19 +225,25 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Label_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 256 OffsetTop 256 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -284 }
|
Name "Label_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 228 OffsetTop 228 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -256 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Response radius (500-5000 metres)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "HQ response radius (500-5000 m)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Saved_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 288 OffsetTop 288 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -312 }
|
Name "Saved_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 260 OffsetTop 260 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -284 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 284 OffsetTop 284 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -304 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "How far HQ looks for nearby response squads, in metres. Applies immediately." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 256 OffsetTop 256 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -298 }
|
Name "responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 228 OffsetTop 228 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -270 }
|
||||||
components {
|
components {
|
||||||
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||||
m_sLabel ""
|
m_sLabel ""
|
||||||
@@ -203,7 +251,7 @@ ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "Apply_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 256 OffsetTop 256 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -298 }
|
Name "Apply_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 228 OffsetTop 228 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -270 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Apply"
|
m_sText "Apply"
|
||||||
@@ -211,7 +259,91 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "Random_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 256 OffsetTop 256 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -298 }
|
Name "Random_responseRadiusMetres" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 228 OffsetTop 228 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -270 }
|
||||||
|
components {
|
||||||
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
|
m_sText "Random"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Label_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 306 OffsetTop 306 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -334 }
|
||||||
|
Color 0.9 0.92 0.94 1 Text "Anti-air launcher carriers (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||||
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Saved_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 338 OffsetTop 338 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -362 }
|
||||||
|
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||||
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 362 OffsetTop 362 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -382 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "New soldiers only: faction anti-air launcher, or FIM-92 fallback. 0 = no extra launchers." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
|
Name "antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 306 OffsetTop 306 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -348 }
|
||||||
|
components {
|
||||||
|
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||||
|
m_sLabel ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "Apply_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 306 OffsetTop 306 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -348 }
|
||||||
|
components {
|
||||||
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
|
m_sText "Apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "Random_antiAirChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 306 OffsetTop 306 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -348 }
|
||||||
|
components {
|
||||||
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
|
m_sText "Random"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Label_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 384 OffsetTop 384 SizeX 368 OffsetRight -396 SizeY 28 OffsetBottom -412 }
|
||||||
|
Color 0.9 0.92 0.94 1 Text "Anti-personnel mine carriers (0-100%)" "Font Size" 20 "Min Font Size" 20 Wrap 1 FontProperties FontProperties {
|
||||||
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Saved_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 416 OffsetTop 416 SizeX 936 OffsetRight -964 SizeY 24 OffsetBottom -440 }
|
||||||
|
Color 0.9 0.92 0.94 1 Text "Waiting for saved value" "Font Size" 16 "Min Font Size" 16 Wrap 1 FontProperties FontProperties {
|
||||||
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 440 OffsetTop 440 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -460 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "New soldiers only: up to two carried AP mines. Mines are not automatically planted." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
|
Name "antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 384 OffsetTop 384 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -426 }
|
||||||
|
components {
|
||||||
|
SCR_EditBoxComponent "{547290FFBD5B33E9}" {
|
||||||
|
m_sLabel ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "Apply_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 660 OffsetLeft 660 PositionY 384 OffsetTop 384 SizeX 112 OffsetRight -772 SizeY 42 OffsetBottom -426 }
|
||||||
|
components {
|
||||||
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
|
m_sText "Apply"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "Random_antiPersonnelMineChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 784 OffsetLeft 784 PositionY 384 OffsetTop 384 SizeX 180 OffsetRight -964 SizeY 42 OffsetBottom -426 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Random"
|
m_sText "Random"
|
||||||
@@ -222,7 +354,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
FrameWidgetClass { Name "WorldPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "WorldPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "WorldHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "WorldHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Clock, date and weather apply next round. Random day chooses a daylight start time." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "World settings apply next round. Choose a value or Random, then press Apply to save." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
@@ -237,6 +369,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 126 OffsetTop 126 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -146 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "24-hour clock. Random = any time; Random day = daylight only. Takes effect next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 70 OffsetTop 70 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -112 }
|
Name "startTimeMinutes" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 70 OffsetTop 70 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -112 }
|
||||||
components {
|
components {
|
||||||
@@ -273,6 +411,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 210 OffsetTop 210 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -230 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "1 = real-time clock; 12 = one game hour in five real minutes. Takes effect next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 154 OffsetTop 154 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -196 }
|
Name "timeMultiplier" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 154 OffsetTop 154 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -196 }
|
||||||
components {
|
components {
|
||||||
@@ -309,6 +453,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 294 OffsetTop 294 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -314 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Use YYYY-MM-DD, or authored for the island default. Random picks a date each round." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 238 OffsetTop 238 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -280 }
|
Name "startDateCode" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 238 OffsetTop 238 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -280 }
|
||||||
components {
|
components {
|
||||||
@@ -345,6 +495,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 378 OffsetTop 378 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -398 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Use authored, clear, overcast or rain. Random chooses one of the three weather types next round." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 322 OffsetTop 322 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -364 }
|
Name "weatherProfile" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 322 OffsetTop 322 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -364 }
|
||||||
components {
|
components {
|
||||||
@@ -381,7 +537,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
FrameWidgetClass { Name "IntelPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "IntelPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "IntelHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "IntelHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Set document frequency and how much each discovery reveals." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Control how often soldiers carry intel and whether the first clue reveals an exact location." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
@@ -396,6 +552,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 136 OffsetTop 136 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -156 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "Chance for each new enemy soldier to carry intel. Reassign carried intel updates existing soldiers." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
|
Name "aiIntelChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 80 OffsetTop 80 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -122 }
|
||||||
components {
|
components {
|
||||||
@@ -432,6 +594,12 @@ TextWidgetClass {
|
|||||||
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
TextWidgetClass {
|
||||||
|
Name "Help_intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 224 OffsetTop 224 SizeX 936 OffsetRight -964 SizeY 20 OffsetBottom -244 }
|
||||||
|
Color 0.72 0.81 0.87 1 Text "First clue: chance to reveal the exact site. Otherwise a question mark shows an approximate area." "Font Size" 16 "Min Font Size" 16 Wrap 1
|
||||||
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
|
}
|
||||||
|
|
||||||
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
ButtonWidgetClass : "{DA43C77A0D1E59F8}UI/layouts/ERR/AdminValue.layout" {
|
||||||
Name "intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
|
Name "intelRevealChancePercent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 408 OffsetLeft 408 PositionY 168 OffsetTop 168 SizeX 240 OffsetRight -648 SizeY 42 OffsetBottom -210 }
|
||||||
components {
|
components {
|
||||||
@@ -468,13 +636,13 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
Name "RerollIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 306 OffsetLeft 306 PositionY 276 OffsetTop 276 SizeX 260 OffsetRight -566 SizeY 42 OffsetBottom -318 }
|
Name "RerollIntel" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 306 OffsetLeft 306 PositionY 276 OffsetTop 276 SizeX 260 OffsetRight -566 SizeY 42 OffsetBottom -318 }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Reroll AI intel"
|
m_sText "Reassign carried intel"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "IntelRerollHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 336 OffsetTop 336 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -388 }
|
Name "IntelRerollHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 336 OffsetTop 336 SizeX 936 OffsetRight -964 SizeY 52 OffsetBottom -388 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Reroll updates existing AI documents. Recovered intel is preserved." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Reassign carried intel uses the saved chance on existing AI. Intel already collected stays revealed." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
@@ -486,16 +654,16 @@ TextWidgetClass {
|
|||||||
FrameWidgetClass { Name "FactionsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "FactionsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "FactionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "FactionHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Choose three distinct factions or Random. Saved choices apply next round." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Choose three different factions, then Apply. Random all saves immediately and picks anew each round." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass { Name "FactionLabel_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 76 OffsetTop 76 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -108 } Color 0.9 0.92 0.94 1 Text "Escapists faction" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
TextWidgetClass { Name "FactionLabel_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 76 OffsetTop 76 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -108 } Color 0.9 0.92 0.94 1 Text "Your faction (Escapists)" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||||
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 76 OffsetTop 76 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -118 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
|
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 76 OffsetTop 76 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -118 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
|
||||||
TextWidgetClass { Name "FactionState_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 124 OffsetTop 124 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -168 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
TextWidgetClass { Name "FactionState_escapists" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 124 OffsetTop 124 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -168 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||||
TextWidgetClass { Name "FactionLabel_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 184 OffsetTop 184 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -216 } Color 0.9 0.92 0.94 1 Text "Enemy faction" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
TextWidgetClass { Name "FactionLabel_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 184 OffsetTop 184 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -216 } Color 0.9 0.92 0.94 1 Text "Main enemy (controls HQ)" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||||
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 184 OffsetTop 184 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -226 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
|
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 184 OffsetTop 184 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -226 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
|
||||||
TextWidgetClass { Name "FactionState_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -276 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
TextWidgetClass { Name "FactionState_enemy" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 232 OffsetTop 232 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -276 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||||
TextWidgetClass { Name "FactionLabel_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 292 OffsetTop 292 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -324 } Color 0.9 0.92 0.94 1 Text "Independent / FIA faction" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
TextWidgetClass { Name "FactionLabel_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 292 OffsetTop 292 SizeX 280 OffsetRight -308 SizeY 32 OffsetBottom -324 } Color 0.9 0.92 0.94 1 Text "Other hostile faction (FIA role)" "Font Size" 22 "Min Font Size" 22 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||||
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 292 OffsetTop 292 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -334 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
|
ButtonWidgetClass : "{4B5AE6E64037FFB4}UI/layouts/WidgetLibrary/ComboBox/WLib_ComboBox.layout" { Name "FactionSelect_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 320 OffsetLeft 320 PositionY 292 OffsetTop 292 SizeX 644 OffsetRight -964 SizeY 42 OffsetBottom -334 } components { SCR_ComboBoxComponent "{546B1D464BC32853}" { m_bUseLabel 0 m_sLabel "" m_fSizeWithoutLabel 42 } } }
|
||||||
TextWidgetClass { Name "FactionState_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 340 OffsetTop 340 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -384 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
TextWidgetClass { Name "FactionState_independent" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 340 OffsetTop 340 SizeX 936 OffsetRight -964 SizeY 44 OffsetBottom -384 } Color 0.9 0.92 0.94 1 Text "Waiting for server..." "Font Size" 18 "Min Font Size" 18 Wrap 1 FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" } }
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "FactionsRandomAll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 418 OffsetTop 418 SizeX 220 OffsetRight -248 SizeY 42 OffsetBottom -460 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Random all" } } }
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" { Name "FactionsRandomAll" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 418 OffsetTop 418 SizeX 220 OffsetRight -248 SizeY 42 OffsetBottom -460 } components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Random all" } } }
|
||||||
@@ -504,7 +672,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
FrameWidgetClass { Name "ModsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "ModsPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "ModsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "ModsHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Choose content the mission may use. Required Workshop downloads stay installed." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Enable or disable the factions this mission may use. This does not uninstall Workshop dependencies." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
@@ -519,7 +687,7 @@ VerticalLayoutWidgetClass { Name "ModList" Slot AlignableSlot { HorizontalAlign
|
|||||||
FrameWidgetClass { Name "JournalPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "JournalPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "JournalHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "JournalHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Radio reports, orders and physical support activity from the mission HQ." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Each entry explains what happened. Queued means awaiting radio delivery; received means delivered." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
ScrollLayoutWidgetClass {
|
ScrollLayoutWidgetClass {
|
||||||
@@ -560,6 +728,10 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
Name "JournalDetails" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 310 OffsetLeft 310 PositionY 438 OffsetTop 438 SizeX 360 OffsetRight -670 SizeY 42 OffsetBottom -480 }
|
||||||
|
components { SCR_ButtonTextComponent "{5548F0143AE99095}" { m_sText "Show technical details" } }
|
||||||
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
Name "LatestJournal" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 742 OffsetLeft 742 PositionY 438 OffsetTop 438 SizeX 222 OffsetRight -964 SizeY 42 OffsetBottom -480 }
|
Name "LatestJournal" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 742 OffsetLeft 742 PositionY 438 OffsetTop 438 SizeX 222 OffsetRight -964 SizeY 42 OffsetBottom -480 }
|
||||||
components {
|
components {
|
||||||
@@ -572,7 +744,7 @@ ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_But
|
|||||||
FrameWidgetClass { Name "StatusPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
FrameWidgetClass { Name "StatusPage" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 0 OffsetLeft 0 PositionY 160 OffsetTop 160 SizeX 1000 OffsetRight -1000 SizeY 486 OffsetBottom -646 } {
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "StatusHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
Name "StatusHelp" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 6 OffsetTop 6 SizeX 936 OffsetRight -964 SizeY 50 OffsetBottom -56 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Live mission state. Scroll to see all services and support activity." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Live server state. Mortar counts mean operational / deployed; UAZ allowances include past losses." "Font Size" 19 "Min Font Size" 19 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
ScrollLayoutWidgetClass {
|
ScrollLayoutWidgetClass {
|
||||||
@@ -602,7 +774,7 @@ ScrollLayoutWidgetClass {
|
|||||||
} }
|
} }
|
||||||
TextWidgetClass {
|
TextWidgetClass {
|
||||||
Name "Title" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 20 OffsetTop 20 SizeX 790 OffsetRight -818 SizeY 44 OffsetBottom -64 }
|
Name "Title" Slot FrameWidgetSlot { Anchor 0 0 0 0 PositionX 28 OffsetLeft 28 PositionY 20 OffsetTop 20 SizeX 790 OffsetRight -818 SizeY 44 OffsetBottom -64 }
|
||||||
Color 0.9 0.92 0.94 1 Text "Escapists Reintroduced" "Font Size" 32 "Min Font Size" 32 Wrap 1
|
Color 0.9 0.92 0.94 1 Text "Co10 Escape Reintroduced" "Font Size" 32 "Min Font Size" 32 Wrap 1
|
||||||
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
FontProperties FontProperties { Font "{3E7733BAC8C831F6}UI/Fonts/RobotoCondensed/RobotoCondensed_Regular.fnt" }
|
||||||
}
|
}
|
||||||
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
ButtonWidgetClass : "{75C912A1C89BE6C2}UI/layouts/WidgetLibrary/Buttons/WLib_ButtonText.layout" {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ ButtonWidgetClass : "{9ECCD201BCF07E95}UI/layouts/Menus/PauseMenu/PauseMenuButto
|
|||||||
Slot LayoutSlot { }
|
Slot LayoutSlot { }
|
||||||
components {
|
components {
|
||||||
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
SCR_ButtonTextComponent "{5548F0143AE99095}" {
|
||||||
m_sText "Escapists Reintroduced"
|
m_sText "Co10 Escape Reintroduced"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
GameProject {
|
GameProject {
|
||||||
ID "ERR_Core"
|
ID "ERR_Core"
|
||||||
GUID "2528466DE220D91C"
|
GUID "2528466DE220D91C"
|
||||||
TITLE "Escapists Reintroduced"
|
TITLE "Co10 Escape Reintroduced"
|
||||||
Dependencies {
|
Dependencies {
|
||||||
"58D0FB3206B6F859"
|
"58D0FB3206B6F859"
|
||||||
"595F2BF2F44836FB"
|
"595F2BF2F44836FB"
|
||||||
|
|||||||
+11
-7
@@ -6,22 +6,22 @@ Implemented 2026-09-23 in the combined Core addon. These are server-owned missio
|
|||||||
|
|
||||||
Only a fresh BODY report delivered to HQ identifies a body for aggression. The identity is the immutable formation SourceId plus original roster slot. A status-known death can subsequently be identified once; multiple observers, repeat deliveries and status reports cannot inflate this counter. Missing or silent units do not count.
|
Only a fresh BODY report delivered to HQ identifies a body for aggression. The identity is the immutable formation SourceId plus original roster slot. A status-known death can subsequently be identified once; multiple observers, repeat deliveries and status reports cannot inflate this counter. Missing or silent units do not count.
|
||||||
|
|
||||||
`ERR_HQWatchRules.Aggression(bodies)` is `clamp(floor(bodies / 3), 0, 30)`. `Hunters(level)` is `ceil(level / 2)`, capped at 15:
|
`ERR_HQWatchRules.Aggression(bodies)` is `clamp(floor(bodies / 3), 0, 30)`. `Hunters(level)` is `3 + ceil(clamp(level, 0, 30) × 12 / 30)`, capped at 15:
|
||||||
|
|
||||||
| Identified bodies | Aggression | Cumulative UAZ allowance |
|
| Identified bodies | Aggression | Cumulative UAZ allowance |
|
||||||
|---:|---:|---:|
|
|---:|---:|---:|
|
||||||
| 0–2 | 0 | 0 |
|
| 0–2 | 0 | 3 |
|
||||||
| 3–8 | 1–2 | 1 |
|
| 3–8 | 1–2 | 4 |
|
||||||
| 9–14 | 3–4 | 2 |
|
| 9–14 | 3–4 | 5 |
|
||||||
| 45 | 15 | 8 |
|
| 45 | 15 | 9 |
|
||||||
| 87–89 | 29 | 15 |
|
| 87–89 | 29 | 15 |
|
||||||
| 90+ | 30 | 15 |
|
| 90+ | 30 | 15 |
|
||||||
|
|
||||||
The existing confirmed-loss infantry response budget remains independent. The new allowance creates **additional hunter UAZ-PKM vehicles**, separate from supply convoys, each with the selected primary enemy faction's driver and gunner. There are at most 15 deployments per round, not an endless replacement pool. Destroyed, captured or failed vehicles consume their deployment; surviving entities and loot remain physical. Surviving AI crew retain their HQ registration and skill profile when a driving task ends; capture releases the mission binding. The status string distinguishes active vehicles from the cumulative allowance.
|
The existing confirmed-loss infantry response budget remains independent. The new allowance creates **additional hunter UAZ-PKM vehicles**, separate from supply convoys, each with the selected primary enemy faction's driver and gunner. There are at most 15 deployments per round, not an endless replacement pool. Destroyed, captured or failed vehicles consume their deployment; surviving entities and loot remain physical. Surviving AI crew retain their HQ registration and skill profile when a driving task ends; capture releases the mission binding. The status string distinguishes active vehicles from the cumulative allowance.
|
||||||
|
|
||||||
Deployment waits until the prison is unlocked and checks friendly-site road access, terrain, collision, the force-ledger cap and a 400 m player exclusion radius. A blocked candidate is retried after 30 seconds; native crew readiness has a 30-second deadline. Vehicles are registered only with their actual original native agents and seat occupants. Faction selection, inventory, radio fitting and body/intel ownership use existing mission services.
|
Deployment waits until the prison is unlocked. Three initial UAZs are planned around the opening player centroid (prison position if no player exists), on actual roads at least 90 degrees apart and at least 600 m from that center. The bounded survey tries up to 64 candidates in four rings (800–2150 m), eight queries per HQ second, and retains three clear positions before spawning at most one vehicle per second. Every spawn rechecks terrain/collision, the force ledger and a 400 m exclusion around every player. Unsafe placement is withheld and logged. Additional aggression deployments use friendly-site roads and 30-second spacing. Native crew readiness has a 30-second deadline. Vehicles are registered only with their actual original native agents and seat occupants. Faction selection, inventory, radio fitting and body/intel ownership use existing mission services.
|
||||||
|
|
||||||
Without delivered sightings, hunters patrol roads between friendly objectives. With fresh actionable HQ memory, they intercept a road near the last reported/predicted position. Destination selection does not query live player positions. Orders must traverse the existing physical radio network. Loss of communications prevents new orders; it does not magically reveal a player or cancel the last received order.
|
Each hunter crew requests one physical manpack through the existing finite radio-fitting service; destroyed or looted radios are not replenished. Without delivered sightings, hunters patrol roads between friendly objectives. With fresh actionable HQ memory, they intercept a road near the last reported/predicted position. A fresh delivered contact bypasses the 30-second patrol cooldown and cancels a pending patrol downlink. An interception already in radio transit is allowed to complete; updates are limited to one attempt per five seconds. The destination is a road within 250 m of the delivered belief. Destination selection does not query live player positions. Orders must traverse the existing physical radio network. Loss of communications prevents new orders; it does not magically reveal a player or cancel the last received order.
|
||||||
|
|
||||||
## Objective crews
|
## Objective crews
|
||||||
|
|
||||||
@@ -57,3 +57,7 @@ The complete HQ journal is `$logs:rereintroduced-hq.log` (existing filename reta
|
|||||||
Production-method policy tests cover body/status deduplication, three-body thresholds, 30/15 caps, reset, authority/capture boundaries, maximum skill binding/restoration, regular aim-error selection and native perception fallback. The native `aggression` fixture exercises actual body intake, a physical UAZ with seated crew, radio delivery and movement, objective reservations, medical-stash reinforcement admission and teardown. This is local native Workbench validation, not a dedicated-server multiplayer or full 15-UAZ performance benchmark.
|
Production-method policy tests cover body/status deduplication, three-body thresholds, 30/15 caps, reset, authority/capture boundaries, maximum skill binding/restoration, regular aim-error selection and native perception fallback. The native `aggression` fixture exercises actual body intake, a physical UAZ with seated crew, radio delivery and movement, objective reservations, medical-stash reinforcement admission and teardown. This is local native Workbench validation, not a dedicated-server multiplayer or full 15-UAZ performance benchmark.
|
||||||
|
|
||||||
Final local run: `/tmp/erri-aggression-native7/logs/logs_2026-09-23_15-01-06`. All 14 native assertions and the completion marker passed: full medical defence at terrain height, seated driver/gunner, real radio delivery, driving over 30 m, bounded deployment and surviving-crew profile after release. Source stayed unchanged during the run. The runner reported a transient empty-argv Wine `enfMain` during cleanup; a subsequent explicit private-prefix idle check passed. Existing external localization/resource messages remain in the native log; this is not a clean-log claim. `python tools/check.py` passed, and the final hunter lifecycle and affected skill/body/registration policy tests passed separately.
|
Final local run: `/tmp/erri-aggression-native7/logs/logs_2026-09-23_15-01-06`. All 14 native assertions and the completion marker passed: full medical defence at terrain height, seated driver/gunner, real radio delivery, driving over 30 m, bounded deployment and surviving-crew profile after release. Source stayed unchanged during the run. The runner reported a transient empty-argv Wine `enfMain` during cleanup; a subsequent explicit private-prefix idle check passed. Existing external localization/resource messages remain in the native log; this is not a clean-log claim. `python tools/check.py` passed, and the final hunter lifecycle and affected skill/body/registration policy tests passed separately.
|
||||||
|
|
||||||
|
Opening-patrol regressions: `test_hunter_opening.py` executes the actual geometry and order-selection methods, covering angular separation, insufficient candidates, first-contact preemption, in-flight order preservation, lost radio and retry throttling. `test_hq_watch.py` checks the three-to-fifteen allowance across all aggression levels.
|
||||||
|
|
||||||
|
2026-09-23 opening-patrol validation: `/tmp/erri-opening-hunters-native2/logs/logs_2026-09-23_17-32-18` completed all 16 native assertions. Three physical UAZs with seated two-man crews and manpacks were confirmed at mutually separated road starts before any fixture body reports. Three distinct delivered body reports then raised allowance to four. The test observed real HQ radio orders and over 30 m of native driving, preserved objective security, and verified survivor handling. Source/addon fingerprints remained unchanged and cleanup was confirmed. Existing external resource errors remain; multiplayer and an actual patrol sighting through the complete uplink were not tested. First-contact preemption is covered by the production-method regression with radio adapters.
|
||||||
|
|||||||
@@ -11,15 +11,12 @@ Adapted from independently authored, user-authorized AI code. Original files rem
|
|||||||
| `escapists-ai/Scripts/Game/EscapistsAI/EAI_AIResponseSystem.c` | `b3e1263e986837744a70fbaedaa6e57afae7f2981a21211d3a2066e2c63ac84d` | First combat-component section's Expert skill clamp and native reset handling only; no convoy spawning, original faction/group hooks or other sections imported |
|
| `escapists-ai/Scripts/Game/EscapistsAI/EAI_AIResponseSystem.c` | `b3e1263e986837744a70fbaedaa6e57afae7f2981a21211d3a2066e2c63ac84d` | First combat-component section's Expert skill clamp and native reset handling only; no convoy spawning, original faction/group hooks or other sections imported |
|
||||||
| `escapists-ai/Scripts/Game/EscapistsAI/EAI_AIConfig.c` | `8c320d3163d443b43e50547734702484c8ad51f2cffe88cf6dfd33aaf339ec01` | `stingerChanceOneIn`, default 15 and valid range 0..10000 |
|
| `escapists-ai/Scripts/Game/EscapistsAI/EAI_AIConfig.c` | `8c320d3163d443b43e50547734702484c8ad51f2cffe88cf6dfd33aaf339ec01` | `stingerChanceOneIn`, default 15 and valid range 0..10000 |
|
||||||
|
|
||||||
The successor HQ owns `ERR_EnemyEquipment`. Registration supplies exact physical groups; two seconds after group registration, at most five scans discover initialized members. Entries retain exact actor/group identity, with 128 group and 2048 actor limits. Per-actor inventory submission is finite. Accepted pending operations are never repeated; unresolved completion at the fifth check ends processing permanently for that actor. Explicit failure may retry before the fifth check. A success callback ends issuance even if the item was dropped before it could be observed. A late callback after Stop cannot restart this service, although native accepted insertion itself is not cancellable through this API.
|
The successor HQ owns `ERR_EnemyEquipment`. Registration supplies exact physical groups; five startup scans discover initialized members. Entries retain exact actor/group identity, with 256 group and 2048 actor limits. Inventory grants are finite. Accepted pending operations are never repeated; a late callback after Stop cannot restart issuance. See the current loadout policy below for launcher and mine checks.
|
||||||
|
|
||||||
The exact allowed launcher is `{3DF204EEE0534EF2}Prefabs/Weapons/Launchers/FIM92_Stinger/Launcher_FIM92_Stinger.et`, owned by WCS Armaments. The successor requests insertion into `PURPOSE_WEAPON_PROXY`, preserving native slot constraints and other equipment. It creates no loose ammunition or replenishment; actual prefab loadout and operational AI firing need native validation.
|
|
||||||
|
|
||||||
Expert minimum binds only after initialization to an eligible, living, nonplayer member of a registered military enemy group in the active successor mission. Native SetAISkill and ResetAISkill enforce that scoped floor; higher skills are preserved. No perception/target knowledge is changed. Unregistration or Stop clears the binding and restores the skill captured at first binding. Convoy-gunner Cylon specialization is not implemented here because no production convoy-gunner registration exists yet.
|
The original fixed-FIM and one-in-N policy has been superseded by [percentage-based faction loadouts](enemy-loadouts.md). FIM-92 remains the fallback. Native slot constraints, mission ownership and finite grants remain enforced.
|
||||||
|
|
||||||
Settings migrate through the existing additive-default reader and import helper. Existing valid ten-key/six-key files preserve their values and obtain 15 for missing `stingerChanceOneIn`; explicitly present malformed values remain invalid. `/esc hq stinger show|set N` changes only actors that have not rolled yet: 0 disables, 1 selects every eligible actor, 2..10000 selects one in N. The existing ten native menu fields are preserved; this extra key is available through text commands. Unsupported convoy/simulation/start keys are still reported as deferred by the importer, never exposed as working settings.
|
Old `stingerChanceOneIn` settings migrate to the nearest integer percentage; 15 becomes 7%, 1 becomes 100%, and 0 stays off. The HQ page exposes both anti-air and AP-mine percentages. `/esc hq stinger` remains an alias but now accepts a percentage.
|
||||||
|
|
||||||
`tests/test_enemy_equipment.py` executes actual eligibility, callback, insertion, scoped skill and coordinator bodies against native API doubles. It covers pending nonduplication, success after item removal, finite rejected/failed requests, timeout quarantine, capture, absence, wrong mode/faction/player/client/editor, native resets, higher skills, restoration, delayed group membership, registration dedupe, terminal stop and bounded scans. These are policy regressions, not native inventory or multiplayer proof. Expanded external runtime qualification remains separate.
|
|
||||||
|
|
||||||
## Magnified optics
|
## Magnified optics
|
||||||
|
|
||||||
|
|||||||
+40
-8
@@ -19,6 +19,7 @@ Open the normal game chat and enter a command. Core provides:
|
|||||||
| `/esc status` | Server administrator | Current objective or startup failure cause; adds HQ status when Chat is loaded |
|
| `/esc status` | Server administrator | Current objective or startup failure cause; adds HQ status when Chat is loaded |
|
||||||
| `/esc killais` | Server administrator | Kill AI characters within 1,000 m of your living character; preserve players and vehicles |
|
| `/esc killais` | Server administrator | Kill AI characters within 1,000 m of your living character; preserve players and vehicles |
|
||||||
| `/esc settings` | Server administrator | Current shared settings and values |
|
| `/esc settings` | Server administrator | Current shared settings and values |
|
||||||
|
| `/esc mission keepweapon set 0` / `1` | Server administrator | Disable/enable retaining the held firearm while unconscious and equipping it after revival; applies from the next knockout |
|
||||||
| `/esc mission radio show` | Server administrator | Radio defence duration in seconds |
|
| `/esc mission radio show` | Server administrator | Radio defence duration in seconds |
|
||||||
| `/esc mission radio set N` | Server administrator | Save a whole-number duration from 30 to 1800 seconds |
|
| `/esc mission radio set N` | Server administrator | Save a whole-number duration from 30 to 1800 seconds |
|
||||||
| `/esc mission time show` | Server administrator | Current clock and saved next-round time/speed |
|
| `/esc mission time show` | Server administrator | Current clock and saved next-round time/speed |
|
||||||
@@ -39,8 +40,9 @@ Additional AI commands:
|
|||||||
| Command | Effect |
|
| Command | Effect |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `/esc hq memory` | Show bounded delivered-report beliefs, confidence, uncertainty and travel estimates |
|
| `/esc hq memory` | Show bounded delivered-report beliefs, confidence, uncertainty and travel estimates |
|
||||||
| `/esc hq stinger show` | Show the saved one-in-N initial equipment chance |
|
| `/esc hq aa show` / `/esc hq aa set N` | Enemy MANPADS grant chance, 0–100% or Random; default 7% |
|
||||||
| `/esc hq stinger set N` | Set 0 to disable, 1 for every eligible actor, or 2–10000 for one in N; affects actors that have not rolled yet |
|
| `/esc hq mines show` / `/esc hq mines set N` | Enemy AP-mine grant chance, 0–100% or Random; default 60%, up to two carried mines |
|
||||||
|
| `/esc hq stinger show` / `/esc hq stinger set N` | Legacy alias for anti-air percentage, now 0–100 rather than one-in-N |
|
||||||
| `/esc hq convoy show` | Show the finite deployment task, retained vehicles, native cargo insertion results and stop reason |
|
| `/esc hq convoy show` | Show the finite deployment task, retained vehicles, native cargo insertion results and stop reason |
|
||||||
| `/esc hq convoy start` | Deploy the next unused slot in this round’s configured convoy allowance after physical force admission |
|
| `/esc hq convoy start` | Deploy the next unused slot in this round’s configured convoy allowance after physical force admission |
|
||||||
| `/esc convoys show` | Show the saved count and active fleet |
|
| `/esc convoys show` | Show the saved count and active fleet |
|
||||||
@@ -60,33 +62,57 @@ Core also provides `/esc mission abandon`: an administrator-only explicit defeat
|
|||||||
|
|
||||||
## Mission menu
|
## Mission menu
|
||||||
|
|
||||||
**Esc → Escapists Reintroduced** opens one administrator menu. Its top navigation
|
**Esc → Co10 Escape Reintroduced** opens one administrator menu. Its top navigation
|
||||||
stays visible on every page:
|
stays visible on every page:
|
||||||
|
|
||||||
| Page | Controls |
|
| Page | Controls |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Mission | Radio defence duration and next-round convoy count |
|
| Mission | Radio defence duration, next-round convoy count and unconscious weapon retention |
|
||||||
| World | Start time, Random day, time speed, date and weather |
|
| World | Start time, Random day, time speed, date and weather |
|
||||||
| Factions | Escapists, enemy and independent faction choices; Random all |
|
| Factions | Escapists, enemy and independent faction choices; Random all |
|
||||||
| Mods | Content pack and individual faction switches |
|
| Mods | Content pack and individual faction switches |
|
||||||
| HQ | Report interval, support squad limit and response radius |
|
| HQ | Report interval, support squad limit, response radius, anti-air percentage and AP-mine percentage |
|
||||||
| Intel | Document frequency, exact-reveal chance, summary and reroll |
|
| Intel | Document frequency, exact-reveal chance, summary and reroll |
|
||||||
| Status | Scrollable live mission and service status |
|
| Status | Scrollable live mission and service status |
|
||||||
| Journal | Scrollable HQ activity, pause/resume and jump to latest |
|
| Journal | Readable HQ activity, technical-detail toggle, pause/resume and jump to latest |
|
||||||
|
|
||||||
Changing pages preserves drafts and never applies them. Escape returns to Mission;
|
Changing pages preserves drafts and never applies them. Escape returns to Mission;
|
||||||
a second Escape closes the menu (an active text edit consumes Escape first).
|
a second Escape closes the menu (an active text edit consumes Escape first).
|
||||||
Server authorization and capability checks still control every mutation.
|
Server authorization and capability checks still control every mutation.
|
||||||
|
|
||||||
|
Each of the thirteen value fields includes a short explanation of its effect
|
||||||
|
and when it applies. Equipment percentages affect newly equipped soldiers; time,
|
||||||
|
weather and convoy settings apply next round. The faction page distinguishes your
|
||||||
|
faction, the enemy that controls HQ, and the other hostile faction.
|
||||||
|
|
||||||
|
**Keep weapon when unconscious** has explicit Enabled/Disabled buttons on Mission. It defaults to Enabled and applies from each player’s next knockout. With it enabled, the same gun is retained and re-equipped after revival; Disabled restores the normal ground drop. Unconscious and dead spectators use **Left/Right arrows** to change teammates.
|
||||||
|
|
||||||
|
The Journal normally shows sentences with real server-local clock time
|
||||||
|
(`HH:MM:SS`), event numbers and radio message numbers. Timestamps are captured
|
||||||
|
when events occur and stay unchanged when history is opened or resumed. It distinguishes local sightings, reports awaiting transmission,
|
||||||
|
received reports and delivered orders. **Show technical details** also displays
|
||||||
|
the original records, including elapsed seconds, coordinates, identifiers and diagnostic fields;
|
||||||
|
switching views preserves history and the live cursor. The server journal file
|
||||||
|
contains each readable summary followed by its unchanged technical record.
|
||||||
|
Status labels explicitly identify mortar readiness, identified-body aggression
|
||||||
|
and the total UAZ deployment allowance.
|
||||||
|
|
||||||
|
Validated on 2026-09-23 with the complete repository checks and the isolated native
|
||||||
|
`prisoner-kit` fixture (359 assertions, no script/runtime failures). Native checks
|
||||||
|
cover all thirteen description bounds using DPI-correct measurements, eight-page
|
||||||
|
navigation and the readable/technical journal toggle without cursor/history loss.
|
||||||
|
The fixture still reports existing installed-content resource/keyword errors;
|
||||||
|
this is local Workbench validation, not a dedicated-server multiplayer test.
|
||||||
|
|
||||||
## Optional content
|
## Optional content
|
||||||
|
|
||||||
Use **Esc → Escapists Reintroduced → Mods** for server-admin RHS and individual faction switches.
|
Use **Esc → Co10 Escape Reintroduced → Mods** for server-admin RHS and individual faction switches.
|
||||||
Selections apply after mission restart, while Workshop dependencies remain loaded.
|
Selections apply after mission restart, while Workshop dependencies remain loaded.
|
||||||
See [content packs and the modder registry](content-packs.md).
|
See [content packs and the modder registry](content-packs.md).
|
||||||
|
|
||||||
## Random settings
|
## Random settings
|
||||||
|
|
||||||
Every editable setting on **Mission**, **World**, **HQ** and **Intel** has a
|
The numeric and world settings on **Mission**, **World**, **HQ** and **Intel** have a
|
||||||
**Random** button. It selects the draft; press **Apply** to save it. You can still
|
**Random** button. It selects the draft; press **Apply** to save it. You can still
|
||||||
enter a fixed value. The start-time row also offers **Random day**.
|
enter a fixed value. The start-time row also offers **Random day**.
|
||||||
|
|
||||||
@@ -154,3 +180,9 @@ Everon also sets `ScoutCount 10` in its scenario definition. This creates ten fi
|
|||||||
Search helicopter: `heliTimerMinutes` defaults to 15 minutes after prison escape
|
Search helicopter: `heliTimerMinutes` defaults to 15 minutes after prison escape
|
||||||
and after aircraft loss. Use `/esc hq heli show`, `/esc hq heli timer set 15`,
|
and after aircraft loss. Use `/esc hq heli show`, `/esc hq heli timer set 15`,
|
||||||
or `/esc hq heli return`. See [search-helicopter.md](search-helicopter.md).
|
or `/esc hq heli return`. See [search-helicopter.md](search-helicopter.md).
|
||||||
|
|
||||||
|
Enemy equipment percentages affect soldiers whose initial loadout has not yet been rolled. They do not replace authored equipment or replenish dropped/looted items. Mines are carried, not automatically planted. See [enemy loadouts](enemy-loadouts.md) for faction launcher selection and fallback.
|
||||||
|
|
||||||
|
**Factions → Random all** immediately saves Random for all three roles; no second Apply is needed. Random remains saved after closing the menu and is resolved afresh at each round startup. Current factions stay unchanged until the next round. Individual dropdown edits still use **Apply factions**.
|
||||||
|
|
||||||
|
The 2026-09-23 native `prisoner-kit` fixture verified that Random all saves via the authenticated owner RPC, survives a fresh settings load and menu reopen without Apply, and preserves current-round faction roles. It completed 357 assertions without script/runtime failures; installed external resource errors still make the engine report non-clean. Regression coverage is in `test_faction_menu.py` and `test_faction_selection.py`.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Mission content packs and factions
|
# Mission content packs and factions
|
||||||
|
|
||||||
Administrators open **Esc → Escapists Reintroduced → Mods** (also available in `/esc menu`). RHS
|
Administrators open **Esc → Co10 Escape Reintroduced → Mods** (also available in `/esc menu`). RHS
|
||||||
and its US Marines, Russian Armed Forces and ION factions default to enabled.
|
and its US Marines, Russian Armed Forces and ION factions default to enabled.
|
||||||
Each faction can be disabled independently. Disabling the parent pack excludes
|
Each faction can be disabled independently. Disabling the parent pack excludes
|
||||||
all its factions and the mission's optional RHS optics, while retaining child
|
all its factions and the mission's optional RHS optics, while retaining child
|
||||||
@@ -131,3 +131,5 @@ replication acceptance. No Workshop publishing was performed.
|
|||||||
Faction hostility follows the native
|
Faction hostility follows the native
|
||||||
[SCR_FactionManager API](https://community.bistudio.com/wikidata/external-data/arma-reforger/ArmaReforgerScriptAPIPublic/interfaceSCR__FactionManager.html),
|
[SCR_FactionManager API](https://community.bistudio.com/wikidata/external-data/arma-reforger/ArmaReforgerScriptAPIPublic/interfaceSCR__FactionManager.html),
|
||||||
with both directions validated before preparation can finish.
|
with both directions validated before preparation can finish.
|
||||||
|
|
||||||
|
Faction entries may optionally specify `AntiAirLauncher` for a loaded MANPADS prefab. The HQ anti-air percentage uses that override, otherwise searches the native faction item catalog and falls back to FIM-92 when necessary. See [enemy loadouts](enemy-loadouts.md) for supported naming, compatibility checks and finite grants.
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# Enemy infantry loadouts
|
||||||
|
|
||||||
|
**Esc → Co10 Escape Reintroduced → HQ** has two percentage settings, each accepting
|
||||||
|
0–100 or Random:
|
||||||
|
|
||||||
|
| Setting | Default | Effect |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Anti-air launchers | 7% | One initially loaded faction MANPADS, with FIM-92 fallback |
|
||||||
|
| Anti-personnel mines | 60% | Up to two carried PMN-4 mines, subject to inventory space |
|
||||||
|
|
||||||
|
These are independent per-soldier grant probabilities, not exact population quotas.
|
||||||
|
They apply to eligible living, nonplayer military members of mission-owned hostile
|
||||||
|
groups, including hostile independent factions. Existing authored equipment is
|
||||||
|
preserved. Specialist loadouts that bypass ordinary fitting remain unchanged.
|
||||||
|
Changing a value affects soldiers whose initial roll has not happened yet; it does
|
||||||
|
not reroll existing soldiers. Mines are inventory loot, not automatically planted.
|
||||||
|
|
||||||
|
Commands are `/esc hq aa show|set N|set random` and
|
||||||
|
`/esc hq mines show|set N|set random`. The old `/esc hq stinger set N` alias now
|
||||||
|
means a percentage. Server authentication and the existing settings save/validation
|
||||||
|
path apply. Canonical keys are `antiAirChancePercent` and
|
||||||
|
`antiPersonnelMineChancePercent`. Old `stingerChanceOneIn` files migrate to the
|
||||||
|
nearest integer percentage: 0 → 0%, 1 → 100%, 15 → 7%, 37 → 3%.
|
||||||
|
Very small probabilities can round to zero. The canonical key takes precedence.
|
||||||
|
Missing settings default to 7% and 60%.
|
||||||
|
|
||||||
|
## Faction launchers and modder overrides
|
||||||
|
|
||||||
|
An `ERR_ContentOption` can specify `AntiAirLauncher` as a prefab ResourceName.
|
||||||
|
The lookup matches `NativeFactionKey` in `Configs/ERR/ContentRegistry.conf`.
|
||||||
|
Otherwise, enabled entries in that faction's native ITEM catalog are checked for
|
||||||
|
MANPADS prefab names (Stinger/FIM92, Igla, Strela or MANPAD under Launchers).
|
||||||
|
Catalog inspection is bounded to 4,096 entries. Unrecognized naming requires an
|
||||||
|
explicit override. USSR/FIA use the installed WCS Igla when no catalog candidate
|
||||||
|
exists. Other factions, unavailable resources and confirmed insertion failures
|
||||||
|
fall back to the WCS FIM-92; no additional mod dependency was added.
|
||||||
|
|
||||||
|
The runtime verifies that the replacement physically occupies a weapon slot,
|
||||||
|
is a rocket launcher, and has a chambered round or loaded magazine. A newly
|
||||||
|
issued unusable replacement is removed only while still in that soldier's
|
||||||
|
inventory, with confirmed deletion before the FIM request. Authored equipment
|
||||||
|
is never deleted to make room. This verifies equipment and ammunition, not the
|
||||||
|
mod's targeting/lock-on behavior in combat.
|
||||||
|
|
||||||
|
Inventory operations have finite startup checks. Pending operations are not
|
||||||
|
repeated or replaced while their result is unknown. Dropping, firing or looting a
|
||||||
|
granted launcher does not earn a new one; mine allowance is likewise finite.
|
||||||
|
No loose items are spawned when inventory capacity is unavailable.
|
||||||
|
`AA PROFILE`, `AA EQUIPMENT`, `AA FALLBACK` and `MINE EQUIPMENT` journal entries
|
||||||
|
record selection, grants and failure reasons.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
`test_enemy_loadout.py` executes production grant methods with native API doubles:
|
||||||
|
all percentage boundaries, preferred success, fallback, exact-item removal,
|
||||||
|
pending-operation quarantine, missing/looted items, capture, authored mines and
|
||||||
|
finite mine grants. Existing coordinator, admin/menu and settings migration tests
|
||||||
|
cover integration. `--project core --smoke enemy-loadout` is a Workbench-only
|
||||||
|
fixture for physical inventory, loaded weapons and HQ menu capture.
|
||||||
|
|
||||||
|
The 2026-09-23 local Workbench fixture completed four assertions with no script
|
||||||
|
or runtime failures: persisted percentage settings, a physical test group, loaded
|
||||||
|
WCS Igla and fallback FIM-92 plus two carried AP mines, and HQ navigation.
|
||||||
|
The native checker still reports installed RHS/WCS localization/resource and
|
||||||
|
external prefab-field errors; this is not a clean whole-mod engine report.
|
||||||
|
Multiplayer replication and actual missile targeting/firing were not tested.
|
||||||
|
|
||||||
|
Final local fixture logs: `/tmp/erri-loadouts-native2/logs/logs_2026-09-23_15-29-59`. Source and external-addon fingerprints remained unchanged; runner cleanup was confirmed. Native menu navigation passed, but the engine screenshot omitted the overlay and the separate display capture failed, so visual layout acceptance remains unverified.
|
||||||
@@ -135,6 +135,11 @@
|
|||||||
"guid": "9ADD554959C09197",
|
"guid": "9ADD554959C09197",
|
||||||
"addon": "65930CB4CD0237B2",
|
"addon": "65930CB4CD0237B2",
|
||||||
"verification": "Exact installed owning resourceDatabase.rdb path/type/GUID and inspected native prefab/scripts, 2026-09-22."
|
"verification": "Exact installed owning resourceDatabase.rdb path/type/GUID and inspected native prefab/scripts, 2026-09-22."
|
||||||
|
},
|
||||||
|
"Prefabs/Weapons/Launchers/9K38_IGLA/Launcher_9K38_IGLA.et": {
|
||||||
|
"guid": "962C400354E4BD6C",
|
||||||
|
"addon": "629B2BA37EFFD577",
|
||||||
|
"verification": "Installed WCS USSR AA character references this exact prefab; inspected launcher has infrared guidance and a loaded disposable muzzle, 2026-09-23."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Random field hospitals and camps
|
||||||
|
|
||||||
|
Everon now has **three field hospitals and five field camps**. This replaces the old medical stash and eastern outpost and adds six locations. Their positions and headings are chosen again at round start. The prison, radio, four mortars and existing supply/extraction objectives remain separate.
|
||||||
|
|
||||||
|
Each field site has four permanent defenders and a separate two-person mobile team, drawn from the selected enemy faction's group roster. Both groups are created once, registered with the force ledger, and receive the existing AI skill, equipment, intel and reporting systems. HQ reserves the defenders against hunting, support, courier and logistics assignments. The mobile team is eligible for delivered HQ hunting orders and otherwise defends its home assembly area. Its standing home waypoint remains available after a temporary HQ assignment ends. Losing mobile soldiers does not turn the permanent guards into replacements. Guards still fight locally and can be killed; the existing objective-defence system permits one finite relief squad after a garrison is lost.
|
||||||
|
|
||||||
|
Hospitals have a vanilla medical tent with red-cross markings, a table, sandbags, and a finite cache of dressings, saline, morphine and radio documents. The first camp also retains the existing finite twelve-shell mortar depot, with courier duty assigned to its mobile team. The loose supplies can be carried away and used for treatment; no endless item replenishment was added. Camps have two tents, sandbag cover, a table and finite rifles, magazines, binoculars and dressings. New scenery requests a native navigation rebuild.
|
||||||
|
|
||||||
|
The original medical clue route remains marked after prison escape. The other field sites are hidden until seen nearby or revealed through existing intel. Partial clues use an approximate question mark; identified hospitals use the medical icon and camps use the outpost icon. Intel carried by AI can refer to these sites, and corpse inspection therefore feeds their compromise into the existing HQ security response.
|
||||||
|
|
||||||
|
## Placement and authoring
|
||||||
|
|
||||||
|
Edit `addons/core/Configs/ERR/EveronSites.conf`. `GuardCount` selects the permanent roster size (1–8, or zero for the full faction group). `ResponseCount` selects a separate mobile roster (0–8). `GuardOffset` and `ResponseOffset` are rotated with the composition. Do not put a roaming `Patrol` on a permanent garrison; use its separate response team instead. The selected profile must provide usable infantry members.
|
||||||
|
|
||||||
|
`RandomClear` supports hospitals, outposts and depots as well as the existing generated objectives. Each new field site is at least 600 metres from other committed locations; unresolved fallback seeds do not reserve ground. The survey checks water, terrain variation, physical obstacles, the composition footprint, and both squad assembly areas. Each site has a bounded 12,000-candidate/30-second search and a validated authored fallback. Invalid terrain is never accepted just to satisfy a requested count; preparation reports the site's ID if placement fails. Custom scenery still needs to fit the surveyed footprint (ten metres either side of its centre).
|
||||||
|
|
||||||
|
Scenario bounds are 32 sites, 24 initial groups including mobile teams, 512 stock items and 128 scenery props. The default field sites total 32 fixed defenders and 16 mobile soldiers, in addition to other mission forces. These finite limits are not performance measurements.
|
||||||
|
|
||||||
|
## Original investigation
|
||||||
|
|
||||||
|
The installed original was inspected read-only. Its location preparer chooses weighted types at random-location holders, randomly enables static hospitals, and attaches existing static buildings as compositions. Its location character manager creates local defenders independently of its patrol manager. The successor uses newly authored vanilla-resource layouts and its existing terrain sampler and HQ rather than importing the original's compositions or lifecycle code. This is a field-hospital/camp implementation, not full parity with every original city, checkpoint, depot or hideout.
|
||||||
|
|
||||||
|
Reference SHA-256 values:
|
||||||
|
|
||||||
|
- `ESCT_LocationPreparer.c`: `71b32259306b4a5794283d31485a5c4533a3e95364218fcb0e3e46998df89df6`
|
||||||
|
- `ESCT_LocationCharacterManagerComponent.c`: `15090d0a7a5a1f972db4a46d9dc027d6fd45b3f4e1f00b6620a05419c7918694`
|
||||||
|
- `ESCT_LocationPatrolManagerComponent.c`: `5da0ed5d50ec7bee4496d8e9007255c400c059cb54ff7efd6a2428aebaa4ddcd`
|
||||||
|
|
||||||
|
The medical tent's GUID was verified through the installed vanilla foundation variant referencing `Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et`. No additional Workshop dependency is introduced.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
`tests/test_field_sites.py` executes actual roster selection and placement-separation code. The HQ registration regression checks that groups sharing a home receive different permanent/mobile eligibility. The `site-garrisons` Workbench scenario uses the production randomized configuration and checks physical squads, finite inventories, marker discovery, generated spacing and role separation before/after prison escape. It does not fabricate enemy reports or claim a dedicated multiplayer hunting test.
|
||||||
|
|
||||||
|
The final local Workbench run on 24 September completed with **106 passing assertions and no script/runtime failures**. It verified all eight randomized sites, exact direct stock (including the separate mortar shells and excluding nested rifle attachments), 32 permanent defenders, 16 eligible mobile soldiers, map discovery and role separation across live HQ ticks after unlocking the prison. The complete repository checks also passed.
|
||||||
|
|
||||||
|
Local evidence is retained in `.local/analysis/field-sites-2026-09-24/`. The runner reported a transient process-classification failure during immediate shutdown verification; a separate follow-up confirmed the disposable prefix was idle and the addon source hash unchanged. The original runner result is preserved. Existing RHS/WCS resource/keyword engine errors remain in the native report. This is not dedicated multiplayer, manual interaction, sustained hunting/navigation or performance qualification. No Workshop publication or live-server update was performed.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Gameplay recovery after the 23 September RKS #2 session
|
||||||
|
|
||||||
|
The session analysis and deployed source snapshot are in `.local/analysis/rks2-2026-09-23-gameplay/analysis.md` (local evidence, not a published asset).
|
||||||
|
|
||||||
|
- Mortar initialization receives 90 seconds from its first eligible poll. Waiting in prison does not spend that deadline. Failed/stopped mortar preparation no longer prevents independent supply-convoy admission; other outstanding group admissions still block it.
|
||||||
|
- Hunter UAZs activate their native agents before checking seated readiness and allow up to 120 seconds for startup. Losing the gunner does not invalidate a seated, conscious original driver. Incapacitation/dismount suspends driving without deleting the delivered destination; the original driver receives native boarding tasks. A surviving original crew member may take over a vacant driver seat. Occupied seats are never forcibly cleared, including seats containing casualties.
|
||||||
|
- Hunter movement reports include source ID, position, speed, destination, combat state, time without progress and route retries. A vehicle without movement for 60 seconds outside combat gets at most three native route retries until it moves again. Destroyed/captured vehicles and groups without living original crew are released. No replacement actors, repaired vehicles or teleport-based unsticking are introduced.
|
||||||
|
- Body-search jobs log their job ID, team/body positions and remaining distance. Outside combat, 120 seconds without closing by ten metres triggers another approach. After three failed route attempts, a job may select another team, up to twice, within the existing finite team budget. Physical proximity and inspection are still required before missing intel becomes known.
|
||||||
|
- Compromised-site messages distinguish waiting for an eligible team/radio route from a queued dispatch. They no longer say reinforcement was requested when no team was assigned.
|
||||||
|
- Parachute placement checks up to 72 candidates: twelve bearings, three distances (300/500/700 metres) and two corridor orientations. Every candidate retains the existing ground, water and descent-clearance checks. Logs summarize rejected candidates and distinguish a successful corridor from no safe corridor. Failed attempts retain the existing 15-second retry delay; the eight-minute committed-drop cooldown is unchanged.
|
||||||
|
- The radio has an explicit interaction point attached to the radio mesh, an omnidirectional three-metre context, and its existing eight-second action. Server authorization remains unchanged.
|
||||||
|
|
||||||
|
Automated checks include `tests/test_gameplay_session_regressions.py`, the existing policy suites, the native `radio-interaction` fixture, and the delayed-prison `mortar-station` fixture. The radio fixture uses native client context collection rather than injecting actions into the handler. Its final action execution is scripted; this does not claim human input or dedicated-client replication coverage.
|
||||||
|
|
||||||
|
Native radio comparison: the deployed prefab failed `GetTransformationWorld()` for its interaction context. With the authored point, `/tmp/erri-radio-context-fixed/logs/logs_2026-09-23_21-43-16` passed all seven assertions, including actual native client selection in the furnished tent and transition into radio defence. The runner recorded unchanged sources and confirmed cleanup. Existing RHS/WCS resource/keyword errors still make the aggregate engine-error report nonzero; no script/runtime failures were recorded in this successful fixture.
|
||||||
|
|
||||||
|
The native `mortar-station` fixture held the prison closed for 110 seconds before releasing it. The one battery in that fixture initialized, seated its gunner, retained its crew against competing orders, and released survivors only after destruction. Evidence: `/tmp/erri-behavior-mortar-station/logs/logs_2026-09-23_21-44-58`. This is not a four-battery or dedicated-server test. Its smoke completed, but the runner could not classify a process during immediate cleanup verification; that PID was subsequently confirmed absent and the next run passed prefix preflight.
|
||||||
|
|
||||||
|
The extended native UAZ fixture also exposed a separate equipment-coordinator bug: temporary unconsciousness permanently stopped an actor's skill entry. Ownership now remains valid for living unconscious AI, while equipment/skill work requires consciousness. The existing entry and completed grant state survive recovery; death, capture, faction change and unregister remain terminal. Policy tests exercise both temporary suspension and permanent release.
|
||||||
|
|
||||||
|
One intermediate UAZ test (`/tmp/erri-behavior-aggression-fixed/logs/logs_2026-09-23_21-55-15`) stopped on an engine behavior-tree `NodeError` for `supply-convoy-1-vehicle-3`: failed movement from `<5924.18,210.115,7040.86>` to `<5880.83,211.208,7023.45>`. This was not a compile error or a hunter recovery assertion. Cleanup and unchanged-source verification passed. That supply-convoy navigation case remains unresolved; the hunter recovery changes do not guarantee every native road/path request succeeds.
|
||||||
|
|
||||||
|
Route retries reuse the physical waypoint's last delivered destination, even when a newer HQ order has already been queued. An executable policy regression checks this boundary, the three-retry limit, reset after movement and suspension during combat.
|
||||||
|
|
||||||
|
Final native hunter validation: `/tmp/erri-behavior-aggression-final/logs/logs_2026-09-23_21-58-13` completed with 19 passing assertions and no script/runtime failures. It verified three separated opening UAZs, a delivered HQ order and more than 30 metres of physical movement, gunner death without losing the patrol, temporary driver unconsciousness, native dismount/reboarding with the same actors, and the surviving driver's enhanced skill after driving-task release. Source hashes remained unchanged and runner cleanup was confirmed. Existing external resource/keyword engine errors still produce a nonzero aggregate CLI exit. This is local Workbench coverage, not a dedicated multiplayer playtest.
|
||||||
|
|
||||||
|
`python tools/check.py` passed after the recovery/ownership changes; the delivered-route retry regression and hunter lifecycle checks were rerun after the final route adjustment. No Workshop publication, Git push or live-server change was performed for these fixes.
|
||||||
@@ -19,7 +19,7 @@ The first release remains vanilla Everon, prison start and twelve players. Alter
|
|||||||
| Radio objectives, tasks and discovery | partial (first-release) | 21 | Independent radio defence and native task-detail refresh are implemented. Validate visible task/map usability, simultaneous real clients, discovery replication and complete manual objective route. |
|
| Radio objectives, tasks and discovery | partial (first-release) | 21 | Independent radio defence and native task-detail refresh are implemented. Validate visible task/map usability, simultaneous real clients, discovery replication and complete manual objective route. |
|
||||||
| Physical equipment, storage and scavenging | partial (first-release) | 23 | Two independent clue caches and optional medical/outpost stock are authored. Manual access, route balance, medical treatment, destruction and network ownership remain unverified; finite stock replaces refill systems. |
|
| Physical equipment, storage and scavenging | partial (first-release) | 23 | Two independent clue caches and optional medical/outpost stock are authored. Manual access, route balance, medical treatment, destruction and network ownership remain unverified; finite stock replaces refill systems. |
|
||||||
| Usable vehicles, locks, alarms and navigation | partial (first-release) | 17 | Finite optional vanilla ground transport passes native parking, seats, engine, fuel depletion and loss checks. Manual driving/access, route navigation, repair, alarms and remote vehicle ownership remain validation/integration work. |
|
| Usable vehicles, locks, alarms and navigation | partial (first-release) | 17 | Finite optional vanilla ground transport passes native parking, seats, engine, fuel depletion and loss checks. Manual driving/access, route navigation, repair, alarms and remote vehicle ownership remain validation/integration work. |
|
||||||
| Locations, compositions and population ownership | partial (first-release) | 32 | Seven sites and bounded proximity/discovery events are implemented. Expand towns/checkpoints and turret population, validate authored static cover, loss retention, manual access and multiplayer cleanup. |
|
| Locations, compositions and population ownership | partial (first-release) | 32 | Sixteen sites now include three randomized field hospitals and five camps with separate permanent defenders and mobile teams; see [field sites](field-sites.md). Towns/checkpoints, turret population, manual access, loss retention and multiplayer cleanup remain to be qualified. |
|
||||||
| Player identity, spawning and reconnect | partial (first-release) | 14 | Native local slot reservations, final-assignment guards and owned pending cleanup pass. Active admission now counts blocked geometry and missing prerequisites toward its bounded retry budget; the actual-method regression passes. Real dedicated-server concurrent joins, preload races, reconnects, connection-ID reuse, spent lives, late arrivals and spectator streaming remain. |
|
| Player identity, spawning and reconnect | partial (first-release) | 14 | Native local slot reservations, final-assignment guards and owned pending cleanup pass. Active admission now counts blocked geometry and missing prerequisites toward its bounded retry budget; the actual-method regression passes. Real dedicated-server concurrent joins, preload races, reconnects, connection-ID reuse, spent lives, late arrivals and spectator streaming remain. |
|
||||||
| Mission setup, configuration and administration | partial (first-release) | 16 | Core administration/settings and saved environment changes pass local native checks, including two restart rounds. Ordinary local startup/admission passes for loose Chat and an exact archived packed Core candidate; the latter predates current mortar edits and does not qualify packaging. Verify current-source startup, remote authorization, player-facing pre-round setup and difficulty choices. |
|
| Mission setup, configuration and administration | partial (first-release) | 16 | Core administration/settings and saved environment changes pass local native checks, including two restart rounds. Ordinary local startup/admission passes for loose Chat and an exact archived packed Core candidate; the latter predates current mortar edits and does not qualify packaging. Verify current-source startup, remote authorization, player-facing pre-round setup and difficulty choices. |
|
||||||
| Mission state, restart and base framework | partial (first-release) | 7 | Separate remaining objective/lifecycle policies, test mission failure reporting and multiplayer restart/cleanup. |
|
| Mission state, restart and base framework | partial (first-release) | 7 | Separate remaining objective/lifecycle policies, test mission failure reporting and multiplayer restart/cleanup. |
|
||||||
|
|||||||
@@ -1,9 +1,31 @@
|
|||||||
# Player lifecycle and pending admission
|
# Player lifecycle and pending admission
|
||||||
|
|
||||||
|
Admitted prisoners now survive lethal damage as treatable unconscious casualties.
|
||||||
|
If everyone is unconscious and neither treatment nor recovery progresses for 30
|
||||||
|
seconds, the round ends in defeat. See [recovery rules](recovery.md) for medical
|
||||||
|
behavior and reconnect reservations. The death/spectator paths below remain as
|
||||||
|
fallbacks for deleted bodies and administrative/native lifecycle events.
|
||||||
|
|
||||||
|
## Unconscious teammate camera
|
||||||
|
|
||||||
|
An admitted player who becomes unconscious now gets a local third-person spectator camera without losing possession of the injured body. Teammates can continue treatment, including the existing epinephrine recovery path. Native healing restores the player's own camera automatically; the round's all-unconscious failure timer is unchanged.
|
||||||
|
|
||||||
|
The server selects conscious, friendly players whose controlled bodies belong to the current round. It excludes the unconscious observer's own body, unfinished admissions and stale/replaced bodies. If the target becomes unavailable, it selects another valid teammate. With no conscious teammate, the camera remains near the casualty's own body. Press **Left/Right arrow** to cycle teammates without administrator rights; holding an arrow does not repeatedly switch targets, and open menus suppress cycling. `/esc spectate next` and `/esc spectate previous` remain available as chat alternatives. Native manual-camera look and zoom controls orbit the target at a bounded distance; there is no free-flight or target possession. The follow view also handles teammates in vehicles and traces camera clearance against terrain and objects. Default screen effects hide while a manual camera is active and return with the normal player camera.
|
||||||
|
|
||||||
|
Existing dead/late-join spectator reasons retain their prior eligibility and wording. The new unconscious reason explains treatment and recovery rather than telling a living player to wait for the next round. Closing the round clears the selected target and the existing forced target/vehicle streaming leases.
|
||||||
|
|
||||||
|
The owner's original `ESCT_SpectatorComponent`, `ESCT_OrbitPlayerManualCameraComponent`, and `ESCT_OrbitDistanceManualCameraComponent` were inspected read-only. The successor extends its existing observer instead of importing the original editor mode, fade dependencies or epinephrine-based camera exclusion. Their SHA-256 values are respectively `543891977028488a149498ab777477b4cc415e35d54d63d239a9358ae08393be`, `f11166bc01dc63b5503f74f40d7901f683c004b2f92b913fbdf8104d923e8c9b`, and `6245587ac6fad9819bd3098d14cb2f000539557cb4f0781f25611ad10be26354`.
|
||||||
|
|
||||||
The successor keeps one round-owned life record per authenticated account. A connection number is only the current binding to that record. A new life can enter during prison, radio search or radio defence; extraction and terminal phases refuse new admission. Successful initial assignment spends the life. Cancelling an unassigned preload never restores or spends a life.
|
The successor keeps one round-owned life record per authenticated account. A connection number is only the current binding to that record. A new life can enter during prison, radio search or radio defence; extraction and terminal phases refuse new admission. Successful initial assignment spends the life. Cancelling an unassigned preload never restores or spends a life.
|
||||||
|
|
||||||
Native reconnect reservation and handoff remain separate from new-character admission. A returning account may reclaim its surviving reserved actor through Reforger's reconnect system. A dead actor remains a spent life; unconsciousness is not death. Reserved living actors remain participants while their connections are absent. These policies are implemented, but real remote reconnect and reservation-expiry acceptance remains unfinished.
|
Native reconnect reservation and handoff remain separate from new-character admission. A returning account may reclaim its surviving reserved actor through Reforger's reconnect system. A dead actor remains a spent life; unconsciousness is not death. Reserved living actors remain participants while their connections are absent. These policies are implemented, but real remote reconnect and reservation-expiry acceptance remains unfinished.
|
||||||
|
|
||||||
|
## Weapon retention on unconsciousness
|
||||||
|
|
||||||
|
**Esc → Co10 Escape Reintroduced → Mission → Keep weapon when unconscious** offers Enabled/Disabled buttons. Enabled is the default, including for older settings files. Administrators can also use `/esc mission keepweapon set 1` or `0`. The preference persists in the server profile and is captured at each knockout; changing it does not alter a casualty already being treated.
|
||||||
|
|
||||||
|
Enabled retains the actual held firearm, including its magazine and attachments, and selects it again after revival. It never creates a replacement or takes a gun acquired by another inventory. Disabled allows the normal ground drop. Grenades, gadgets and mounted weapons are excluded. The native test verifies inventory retention and the same rifle in hand after waking; it does not qualify the unconscious ragdoll's hand pose. Revival inside a vehicle leaves the gun in inventory rather than forcing an equip animation.
|
||||||
|
|
||||||
## Placement
|
## Placement
|
||||||
|
|
||||||
Prison placement searches a 4×4 lattice with axis coordinates -4.5, -1.5, 1.5 and 4.5m relative to the authored center. The four inner positions are checked first, then eight edge positions, then four corners. Neighboring centers are 3m apart and remain 1.5m inside walls positioned at ±6m. The inner and edge positions are within 5m of the prison center. Terrain height and slope, physical scene/character occupancy, and pending reservations can reject a slot. Connection IDs 1 and 13 receive no special or shared position. Disconnected physical bodies still obstruct the native query.
|
Prison placement searches a 4×4 lattice with axis coordinates -4.5, -1.5, 1.5 and 4.5m relative to the authored center. The four inner positions are checked first, then eight edge positions, then four corners. Neighboring centers are 3m apart and remain 1.5m inside walls positioned at ±6m. The inner and edge positions are within 5m of the prison center. Terrain height and slope, physical scene/character occupancy, and pending reservations can reject a slot. Connection IDs 1 and 13 receive no special or shared position. Disconnected physical bodies still obstruct the native query.
|
||||||
@@ -31,3 +53,5 @@ The native lifecycle fixture in [050403](evidence/native-2026-09-19-050403/repor
|
|||||||
`tests/test_participant_selection.py` executes the actual `CurrentActor`, `SpawnPosition` and server observer-selection methods with engine enumeration, geometry and streaming represented by adapters. It covers unregistered and pending actors, missing identity, disconnect, mismatched connection/body, recycled IDs, dead/foreign bodies, accepted canonical targets, cycling, loss of a previously selected target, unconscious following and unchanged prison/geometry fallback. This closes a source-proven membership omission; it does not establish native possession or multiplayer streaming behavior for the revision.
|
`tests/test_participant_selection.py` executes the actual `CurrentActor`, `SpawnPosition` and server observer-selection methods with engine enumeration, geometry and streaming represented by adapters. It covers unregistered and pending actors, missing identity, disconnect, mismatched connection/body, recycled IDs, dead/foreign bodies, accepted canonical targets, cycling, loss of a previously selected target, unconscious following and unchanged prison/geometry fallback. This closes a source-proven membership omission; it does not establish native possession or multiplayer streaming behavior for the revision.
|
||||||
|
|
||||||
`tests/test_spawn_retry_budget.py` executes the actual `DoSpawn_S` method with native controller, respawn, geometry and scheduler adapters. It covers persistent blocked positions, missing prerequisites, thirty-attempt exhaustion, temporary recovery, request rejection, preparation waiting, spent/late life refusal and unchanged pending requests. The counter change has source-local regression evidence only until the next native integration run; this does not qualify slow remote clients or real multiplayer admission.
|
`tests/test_spawn_retry_budget.py` executes the actual `DoSpawn_S` method with native controller, respawn, geometry and scheduler adapters. It covers persistent blocked positions, missing prerequisites, thirty-attempt exhaustion, temporary recovery, request rejection, preparation waiting, spent/late life refusal and unchanged pending requests. The counter change has source-local regression evidence only until the next native integration run; this does not qualify slow remote clients or real multiplayer admission.
|
||||||
|
|
||||||
|
The combined unconscious-camera/weapon revision passed eighteen native Workbench assertions on 24 September 2026: real player incapacitation, current manual camera activation, unchanged possession/living body, no-target fallback, physical-target tracking and movement, native FullHeal recovery, restoration of the player camera, repeated knockout, default-enabled retention of the exact rifle, the same rifle equipped after revival, and normal ground drop with retention disabled. There were no script/runtime failures, and runner cleanup and unchanged-source verification succeeded. Existing RHS/WCS and vanilla resource errors remain in the log, so this is not an error-free engine log. A WORKBENCH-only target stand-in exercises camera tracking because this runner has one real player; it does not establish remote multiplayer streaming. The actual server selection/client policy regressions cover self-exclusion, conscious teammates, canonical identity, target loss, recovery, inactive rounds and delayed unconscious metadata. Arrow input edge/hold/menu behavior and firearm categories have source-policy tests; the new menu controls have structural and command-route checks. The complete repository checks passed. Evidence is in `.local/analysis/unconscious-weapon-2026-09-24`.
|
||||||
|
|||||||
+35
-6
@@ -1,11 +1,40 @@
|
|||||||
# Incapacitated survivors and administrator abandonment
|
# Revivable players and all-unconscious mission failure
|
||||||
|
|
||||||
Core observes the controlled, living bodies of participants who have already used their life. A conscious participant can help; an incapacitated participant remains a survivor. A disconnected or temporarily unpossessed living body remains reserved and is not counted as an available rescuer. Dead bodies and spectators do not count. This observation does not change health, consume another life, advance a phase or end a round.
|
Admitted player bodies receive server-owned casualty protection. Ordinary damage
|
||||||
|
still causes wounds and bleeding. Damage to physical, overall-health and blood
|
||||||
|
hitzones is limited above their configured destruction threshold; an otherwise
|
||||||
|
lethal hit knocks the prisoner unconscious. Resilience can still reach zero, and
|
||||||
|
healing damage is passed through unchanged. Further hits and bleeding cannot
|
||||||
|
finish the casualty. AI retain their normal damage rules. Protection stays with
|
||||||
|
the original body across reconnects and does not grant a new life or respawn.
|
||||||
|
|
||||||
When no controlled survivor is conscious and at least one is incapacitated, the replicated mission status and native objective description explain that recovery remains possible and an administrator may abandon the round. Reconnection is mentioned when a living reserved body exists. The message clears when the condition changes; identical samples do not emit duplicate events. No additional death timer is imposed. Blood loss may still require treatment: the message is not a promise that every casualty will wake unaided.
|
The round fails after **30 uninterrupted seconds** with every remaining prisoner
|
||||||
|
unconscious, no conscious reserved prisoner available to reconnect, and no
|
||||||
|
ongoing treatment or observed blood/resilience recovery. Waking up, a treatment
|
||||||
|
in progress, or increasing blood/resilience resets that timer. A passive recovery
|
||||||
|
effect that is present but makes no progress does not prevent failure. An empty
|
||||||
|
server does not start this timer. Mission status explains whether recovery is
|
||||||
|
continuing or the team is in the failure grace period.
|
||||||
|
|
||||||
`/esc mission abandon` is restricted to the existing server-validated administrator command channel. It accepts only the exact command during an active mission, records a specific result reason and enters the ordinary defeat/cleanup/restart path. Unauthorized, malformed, preparation and repeated terminal requests do not change the round. This gives administrators an explicit way to end a hopeless round while preserving recoverable and reconnecting survivors.
|
Treatment uses the existing medical system. Bandaging and saline remain relevant;
|
||||||
|
ACE Medical epinephrine heals resilience and is not blocked by casualty protection.
|
||||||
|
ACE's own treatment prerequisites still apply (including stopping bleeding and
|
||||||
|
restoring sufficient health). Installing this mission does not itself install
|
||||||
|
ACE Medical or add an unlimited epinephrine action.
|
||||||
|
|
||||||
The `recovery` WORKBENCH fixture exercises actual controller incapacitation, the shared status, observer event deduplication, survival beyond the ordinary 30-second all-dead grace, restoration without replacing the actor, unauthorized/malformed requests and authorized abandonment. Controller state changes in that fixture establish mission policy only. The separate `medical` fixture tests actual blood loss and native treatment recovery without directly restoring consciousness. Neither fixture establishes remote-client replication, manual treatment actions or multiplayer acceptance.
|
`/esc mission abandon` remains an administrator-only way to end a round explicitly.
|
||||||
|
The all-dead/missing-body fallback and exhausted-admission handling also remain,
|
||||||
|
for deleted bodies and administrative/native paths outside ordinary damage.
|
||||||
|
|
||||||
Native run `native-2026-09-19-064406` passed all 54 combined assertions with zero engine errors. The fixture restored consciousness once, then tested authorized abandonment during a second active all-incapacitated warning. Terminal cleanup removed the status and counts. This is local policy evidence; multiplayer replication and manual treatment remain unqualified.
|
The isolated `recovery` Workbench fixture exercises damage exceeding full health
|
||||||
|
on health, blood and head hitzones, continued survival while treatment is flagged
|
||||||
|
active, restoration by native resilience healing, and the actual all-unconscious
|
||||||
|
failure timer. This is separate from remote-client/manual treatment acceptance.
|
||||||
|
|
||||||
|
Validation on 2026-09-23: all repository checks passed after updating the existing
|
||||||
|
ServerTick test adapters. The isolated `recovery` run at
|
||||||
|
`/tmp/erri-revivable-native2/logs/logs_2026-09-23_20-02-04` completed 66 assertions
|
||||||
|
with no script/runtime failures, unchanged source and verified cleanup. Existing
|
||||||
|
RHS/WCS resource and inherited prefab-keyword errors remain in that engine log.
|
||||||
|
The recovery effect was a native timed resilience-healing effect; an actual ACE
|
||||||
|
injector action and remote multiplayer treatment were not exercised by this run.
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
# Single Workshop mod
|
# Single Workshop mod
|
||||||
|
|
||||||
All five owned modules are now one project: `addons/core/addon.gproj`, titled
|
All five owned modules are now one project: `addons/core/addon.gproj`, titled
|
||||||
**Escapists Reintroduced**, with the existing Core GUID `2528466DE220D91C`.
|
**Co10 Escape Reintroduced**, with the existing Core GUID `2528466DE220D91C`.
|
||||||
There are no separate AI, Intel, Map or Chat project descriptors to publish.
|
There are no separate AI, Intel, Map or Chat project descriptors to publish.
|
||||||
The project ID `ERR_Core` is retained; it now identifies the complete mod.
|
The project ID `ERR_Core` is retained; it now identifies the complete mod.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Town garrisons
|
||||||
|
|
||||||
|
After the prison opens, approaching within 700 metres horizontally of a native named town, city, village or settlement activates its population attempt. Each settlement rolls **3–10 enemy soldiers once per round**, using the selected enemy faction's infantry roster. Duplicate map descriptors within 50 metres share the same entry.
|
||||||
|
|
||||||
|
The squad must fit on clear, navigable ground within 200 metres of the settlement's map centre. Every soldier's actual spawn point must be **at least 700 metres from every controlled player**, including unconscious teammates. Placement leaves a small extra margin. If another player is inside the town, the terrain is unsuitable, navigation is still loading, or the mission force budget is full, the town waits. It never relaxes the distance to fill its quota. A rapid approach can therefore leave a town empty until a safe approach becomes possible.
|
||||||
|
|
||||||
|
The owner first plans the whole formation and loads the faction resources, then rechecks player positions immediately before each synchronous character creation. The group has no queued native roster slots, so a delayed expansion cannot create additional soldiers beside an approaching player. A native creation/admission failure cancels that attempt rather than repeatedly replenishing part of a squad.
|
||||||
|
|
||||||
|
All members enter the existing force ledger, intel, equipment and VirtualHQ systems. Each squad requests a physical HQ manpack through the existing equipment service. Initially it guards its assembly area; all members are available for delivered HQ hunting/investigation orders. Town proximity itself does not reveal player positions to HQ: normal AI observations and radio delivery still apply. These squads are distinct from the permanently reserved hospital/camp guards.
|
||||||
|
|
||||||
|
Town soldiers are not replaced after casualties and are not despawned/recreated as players cross the activation boundary. One squad at most is admitted per two-second update, with round-robin attempts for towns approached by separated teammates. The existing finite force budget applies. The owner and its pending work are stopped with HQ at round teardown.
|
||||||
|
|
||||||
|
The 700-metre rule here applies to these new town formations. Existing mission preparation and other reinforcement systems retain their separate lifecycle rules.
|
||||||
|
|
||||||
|
## Diagnostics and validation
|
||||||
|
|
||||||
|
Look for `TOWN GARRISON` in the HQ log. Admission records include the settlement name/identifier, map position, squad size, minimum player distance measured during creation, and HQ role. Deferred attempts explain why they cannot spawn; repeated waiting messages are limited to one per minute per town. The usual `SPAWN GROUP`/`SPAWN MEMBER` events retain individual identities.
|
||||||
|
|
||||||
|
`tests/test_town_garrisons.py` executes the actual distance/activation policy with exact boundaries, split players, empty/invalid observations and a player crossing the exclusion distance. `tests/test_intel_registration.py` checks the actual HQ registration method's town radio selection and unrestricted order eligibility. The isolated `town-garrisons` Workbench fixture exercises native map descriptors, navigation, three- and ten-member physical squads, blocked placement with a second observer in town, HQ binding and no replacement after elimination. It is not a live dedicated-server playtest.
|
||||||
|
|
||||||
|
The 24 September 2026 local Workbench test completed with **12 passing assertions and no script/runtime failures**. Minimum player distances at creation were 718 m for the three-man squad and 720 m for the ten-man squad. Repository checks passed. Existing RHS/WCS localization and vanilla resource keyword errors remain in the native log. The runner hit a transient exiting-process classification error during cleanup; a separate read-only follow-up confirmed that its private prefix was idle and addon sources were unchanged. Original runner results are preserved alongside the follow-up in `.local/analysis/town-garrisons-2026-09-24`.
|
||||||
@@ -102,6 +102,7 @@
|
|||||||
"Prefabs/Structures/BuildingParts/Doors/Door_Metal_A_RIGHT_EXT_COV_B.et": "67F17696FF278AE0",
|
"Prefabs/Structures/BuildingParts/Doors/Door_Metal_A_RIGHT_EXT_COV_B.et": "67F17696FF278AE0",
|
||||||
"Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21",
|
"Prefabs/Structures/Military/Camps/TentSmallUS_01/TentSmallUS_01.et": "01AE5FD77A9A4C21",
|
||||||
"Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646",
|
"Prefabs/Structures/Military/Camps/TentUSSR_01/TentUSSR_01_base.et": "BE5D5CFA90224646",
|
||||||
|
"Prefabs/Structures/Military/Camps/TentUS_01/TentUSMedical_01.et": "F8427C3EFC52644E",
|
||||||
"Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875",
|
"Prefabs/Structures/Walls/Concrete/ConcreteWall_01/ConcreteWall_01_3m.et": "60E5729BEA18F875",
|
||||||
"Prefabs/Systems/Radio/RadioManager.et": "B8E09FAB91C4ECCD",
|
"Prefabs/Systems/Radio/RadioManager.et": "B8E09FAB91C4ECCD",
|
||||||
"Prefabs/Tasks/BaseTask.et": "1D0F815858EE24AD",
|
"Prefabs/Tasks/BaseTask.et": "1D0F815858EE24AD",
|
||||||
@@ -116,6 +117,7 @@
|
|||||||
"Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et": "BD496EE1B40DC510",
|
"Prefabs/Weapons/Attachments/Optics/Optic_4x20/Optic_4x20.et": "BD496EE1B40DC510",
|
||||||
"Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et": "D2018EDB1BBF4C88",
|
"Prefabs/Weapons/Attachments/Optics/Optic_ARTII/Optic_ARTII.et": "D2018EDB1BBF4C88",
|
||||||
"Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et": "C850A33226B8F9C1",
|
"Prefabs/Weapons/Attachments/Optics/Optic_PSO1/Optic_PSO1.et": "C850A33226B8F9C1",
|
||||||
|
"Prefabs/Weapons/Explosives/Mine_PMN4/Mine_PMN4.et": "B05A816C0BF50802",
|
||||||
"Prefabs/Weapons/Handguns/PM/Handgun_PM.et": "C0F7DD85A86B2900",
|
"Prefabs/Weapons/Handguns/PM/Handgun_PM.et": "C0F7DD85A86B2900",
|
||||||
"Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et": "BBB50A815A2F916B",
|
"Prefabs/Weapons/Magazines/Magazine_545x39_AK_30rnd_Ball.et": "BBB50A815A2F916B",
|
||||||
"Prefabs/Weapons/Magazines/Magazine_762x51_M14_20rnd_SpecialBall.et": "627255315038152A",
|
"Prefabs/Weapons/Magazines/Magazine_762x51_M14_20rnd_SpecialBall.et": "627255315038152A",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Workshop preview
|
# Workshop preview
|
||||||
|
|
||||||
Project name: **Escapists Reintroduced**. Author: **Jakub Jim Žáček**.
|
Project name: **Co10 Escape Reintroduced**. Author: **Jakub Jim Žáček**.
|
||||||
Both scenario headers contain this author, including the loading-screen metadata.
|
Both scenario headers contain this author, including the loading-screen metadata.
|
||||||
The current scenario is **Escapists Reintroduced — Everon**. The project name
|
The current scenario is **Co10 Escape Reintroduced — Everon**. The project name
|
||||||
stays **Escapists Reintroduced**; each island's mission header supplies its own
|
stays **Co10 Escape Reintroduced**; each island's mission header supplies its own
|
||||||
island suffix and world resource. The island is scenario metadata, not a shared
|
island suffix and world resource. The island is scenario metadata, not a shared
|
||||||
UI label hardcoded across all maps.
|
UI label hardcoded across all maps.
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Correct the project name to Escapists Reintroduced and the current scenario title to Escapists Reintroduced — Everon, including the loading screen. Set the scenario author to Jakub Jim Žáček. Each island's scenario can use its own island suffix.
|
Correct the project name to Co10 Escape Reintroduced and the current scenario title to Co10 Escape Reintroduced — Everon, including the loading screen. Set the scenario author to Jakub Jim Žáček. Each island's scenario can use its own island suffix.
|
||||||
|
|
||||||
Restore the framed metal prison gate used by the earlier improvised prison, including its original-size door and adjoining wall fit. Keep enemy AI inactive during preparation and imprisonment, then resume the existing soldiers when the prison objective completes through lockpicking (or a valid physical escape). Pause HQ tactical updates and dormant patrol movement until escape.
|
Restore the framed metal prison gate used by the earlier improvised prison, including its original-size door and adjoining wall fit. Keep enemy AI inactive during preparation and imprisonment, then resume the existing soldiers when the prison objective completes through lockpicking (or a valid physical escape). Pause HQ tactical updates and dormant patrol movement until escape.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ source = ROOT / 'addons/core/Scripts/Game/ERR/00_Core/ERR_AdminMenu.c'
|
|||||||
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
|
layout = ROOT / 'addons/core/UI/layouts/ERR/AdminMenu.layout'
|
||||||
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
|
keys = re.findall(r'"([A-Za-z]+)"', body(source, 'static array<string> Keys('))
|
||||||
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
|
settings = re.findall(r'"([A-Za-z]+)"', body(BASE / 'addons/core/Scripts/Game/ERR/00_Core/ERR_Settings.c', 'static array<string> Keys('))
|
||||||
assert len(keys) == 11 and set(settings) - set(keys) == {"stingerChanceOneIn", "automaticConvoyEnabled", "heliTimerMinutes"}
|
assert len(keys) == 13 and set(settings) - set(keys) == {"automaticConvoyEnabled", "heliTimerMinutes", "keepWeaponOnUnconscious"}
|
||||||
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
|
assert set(keys) <= set(settings) # New text-command setting does not alter the fixed native menu.
|
||||||
# Native layouts place child widget declarations inside an anonymous Children
|
# Native layouts place child widget declarations inside an anonymous Children
|
||||||
# block. A missing brace can preserve most widget names yet prevent menu load.
|
# block. A missing brace can preserve most widget names yet prevent menu load.
|
||||||
@@ -47,12 +47,13 @@ names = re.findall(r'\bName "([^"]+)"', layout.read_text())
|
|||||||
assert 'RandomDay_startTimeMinutes' in names
|
assert 'RandomDay_startTimeMinutes' in names
|
||||||
assert len(names) == len(set(names)), 'Menu widget names must be unique'
|
assert len(names) == len(set(names)), 'Menu widget names must be unique'
|
||||||
for key in keys:
|
for key in keys:
|
||||||
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Random_' + key))
|
assert all(name in names for name in (key, 'Apply_' + key, 'Saved_' + key, 'Random_' + key, 'Help_' + key))
|
||||||
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
|
for name in ('Close', 'SettingsPage', 'HQPage', 'JournalPage', 'JournalText', 'JournalContent', 'JournalScroll', 'PauseJournal', 'LatestJournal', 'Status'):
|
||||||
assert name in names
|
assert name in names
|
||||||
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
|
for name in re.findall(r'"([A-Za-z]+)"', body(source, 'override void OnMenuOpen(').split('foreach')[0]):
|
||||||
assert name in names
|
assert name in names
|
||||||
journal = body(source, 'static string JournalText(')
|
journal = body(source, 'static string JournalText(').replace('ERR_JournalText.Menu', 'ERR_JournalText::Menu')
|
||||||
|
menu_format = body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_JournalText.c', 'static string Menu(')
|
||||||
follow = body(source, 'static bool FollowJournal(')
|
follow = body(source, 'static bool FollowJournal(')
|
||||||
command = body(source, 'static string Command(')
|
command = body(source, 'static string Command(')
|
||||||
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
|
available = body(source, 'static bool Available(').replace('Keys().Contains(key)', 'Known(key)')
|
||||||
@@ -66,9 +67,14 @@ struct string : std::string {
|
|||||||
string(std::string s):std::string(s){}
|
string(std::string s):std::string(s){}
|
||||||
bool IsEmpty(){return empty();}
|
bool IsEmpty(){return empty();}
|
||||||
int Length(){return size();}
|
int Length(){return size();}
|
||||||
|
int IndexOf(string s){auto i=find(s);return i==npos?-1:int(i);}
|
||||||
|
string Substring(int i,int n){return substr(i,n);}
|
||||||
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
|
void Replace(string a,string b){size_t i=0;while((i=find(a,i))!=npos){replace(i,a.size(),b);i+=b.size();}}
|
||||||
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
|
string Trim(){auto a=find_first_not_of(" \t\r\n");if(a==npos)return "";auto b=find_last_not_of(" \t\r\n");return substr(a,b-a+1);}
|
||||||
};
|
};
|
||||||
|
struct ERR_JournalText {
|
||||||
|
''' + menu_format + r'''
|
||||||
|
};
|
||||||
struct Fields {
|
struct Fields {
|
||||||
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
|
static bool Known(string key) {return ''' + known + ';}\n' + command + '\n' + available + '\n' + follow + '\n' + journal + r'''
|
||||||
};
|
};
|
||||||
@@ -91,7 +97,7 @@ int main() {
|
|||||||
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
|
{"intelRevealChancePercent","intel reveal"},{"startTimeMinutes","mission time"},
|
||||||
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
|
{"timeMultiplier","mission speed"},{"startDateCode","mission date"},
|
||||||
{"weatherProfile","mission weather"},{"reportIntervalSeconds","hq interval"},
|
{"weatherProfile","mission weather"},{"reportIntervalSeconds","hq interval"},
|
||||||
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"}};
|
{"supportSquadLimit","hq squads"},{"responseRadiusMetres","hq radius"},{"convoyCount","hq convoy count"},{"antiAirChancePercent","hq aa"},{"antiPersonnelMineChancePercent","hq mines"}};
|
||||||
for(auto pair:cases){
|
for(auto pair:cases){
|
||||||
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
|
assert(Fields::Command(pair.first," 42 ")==pair.second+" set 42");
|
||||||
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
|
assert(Fields::Command(pair.first,"Random")==pair.second+" set Random");
|
||||||
@@ -106,7 +112,7 @@ int main() {
|
|||||||
for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
|
for(string key:{"heliTimer","prisonDistance","stingerChance","mission abandon","","bad"}){
|
||||||
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
|
assert(Fields::Command(key,"1").empty());assert(!Fields::Available(key,true,true));
|
||||||
}
|
}
|
||||||
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount"}){
|
for(string key:{"reportIntervalSeconds","supportSquadLimit","responseRadiusMetres","convoyCount","antiAirChancePercent","antiPersonnelMineChancePercent"}){
|
||||||
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
|
assert(!Fields::Available(key,false,true));assert(Fields::Available(key,true,false));
|
||||||
}
|
}
|
||||||
assert(!Fields::Available("aiIntelChancePercent",true,false));
|
assert(!Fields::Available("aiIntelChancePercent",true,false));
|
||||||
@@ -114,4 +120,10 @@ int main() {
|
|||||||
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
|
for(string key:{"intelRevealChancePercent","radioDefenceSeconds","startTimeMinutes","timeMultiplier","startDateCode","weatherProfile"}) assert(Fields::Available(key,false,false));
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: actual menu command routing covers all eleven settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
|
print('PASS: actual menu command routing covers all thirteen settings, rejects unsupported keys, gates companions, and matches unique authored widgets')
|
||||||
|
|
||||||
|
assert all(name in names for name in ('KeepWeaponOn', 'KeepWeaponOff', 'Saved_keepWeaponOnUnconscious'))
|
||||||
|
click=body(source,'protected void OnButton(')
|
||||||
|
assert 'm_Controller.ERR_AdminSubmit("mission keepweapon set " + retain, true)' in click
|
||||||
|
assert click.index('!m_Ready') < click.index('"mission keepweapon set "')
|
||||||
|
print('PASS: explicit weapon retention buttons use the same ready/authorized server command path')
|
||||||
|
|||||||
@@ -62,7 +62,8 @@ int main(){
|
|||||||
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
|
admin.Register(&replacement);assert(admin.m_Providers.size()==2&&admin.JournalSequence()==20);
|
||||||
HQAliases ha;IntelAliases ia;
|
HQAliases ha;IntelAliases ia;
|
||||||
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
|
assert(ha.Setting("hq","interval")=="reportIntervalSeconds"&&ha.Setting("hq","squads")=="supportSquadLimit"&&ha.Setting("hq","radius")=="responseRadiusMetres");
|
||||||
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="stingerChanceOneIn");
|
assert(ha.Setting("hq","aa")=="antiAirChancePercent"&&ha.Setting("hq","mines")=="antiPersonnelMineChancePercent");
|
||||||
|
assert(ha.Setting("intel","carrying").empty()&&ha.Setting("hq","stinger")=="antiAirChancePercent");
|
||||||
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
|
assert(ia.Setting("intel","chance")=="aiIntelChancePercent"&&ia.Setting("intel","carrying")=="aiIntelChancePercent");
|
||||||
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
|
assert(ia.Setting("hq","chance").empty()&&ia.Setting("intel","spawn").empty());
|
||||||
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
|
HQHelp hh;IntelHelp ih;lines.clear();assert(hh.Help("hq",lines)&&lines.size()<=5);lines.clear();assert(ih.Help("intel",lines)&&lines.size()<=4);
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ run(r'''
|
|||||||
using string=std::string;
|
using string=std::string;
|
||||||
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T x){this->push_back(x);}};
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T x){this->push_back(x);}};
|
||||||
'''+valid+integer+r'''
|
'''+valid+integer+r'''
|
||||||
struct Settings{int chance=15,writes=0;bool fail=false;
|
struct Settings{int chance=7,writes=0;bool fail=false;
|
||||||
int Get(string key){assert(key=="stingerChanceOneIn");return chance;}
|
int Get(string key){assert(key=="antiAirChancePercent");return chance;}
|
||||||
bool Set(string key,int value){writes++;if(fail||!Valid(key,value))return false;chance=value;return true;}};
|
bool Set(string key,int value){writes++;if(fail||!Valid(key,value))return false;chance=value;return true;}};
|
||||||
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
|
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
|
||||||
struct Provider{
|
struct Provider{
|
||||||
@@ -27,20 +27,20 @@ void Effect(const Array<string>& r){assert(r.back().find("have not rolled yet")!
|
|||||||
int main(){
|
int main(){
|
||||||
ERR_GameMode mode;Provider p;Array<string> replies;
|
ERR_GameMode mode;Provider p;Array<string> replies;
|
||||||
Array<string> show{"hq","stinger","show"};p.StingerSetting(&mode,show,replies);
|
Array<string> show{"hq","stinger","show"};p.StingerSetting(&mode,show,replies);
|
||||||
assert(mode.settings.writes==0&&replies.back().find("stingerChanceOneIn=15")!=string::npos);Effect(replies);
|
assert(mode.settings.writes==0&&replies.back().find("antiAirChancePercent=7")!=string::npos);Effect(replies);
|
||||||
Array<string> help{"hq","stinger","help"};p.StingerSetting(&mode,help,replies);
|
Array<string> help{"hq","stinger","help"};p.StingerSetting(&mode,help,replies);
|
||||||
assert(mode.settings.writes==0&&replies.back().find("0 off, 1 every eligible soldier, 2..10000 one in N")!=string::npos);Effect(replies);
|
assert(mode.settings.writes==0&&replies.back().find("0..100 percent")!=string::npos);Effect(replies);
|
||||||
Array<string> setting{"hq","stinger","set","0"};
|
Array<string> setting{"hq","stinger","set","0"};
|
||||||
for(int n:{0,1,2,15,10000}){
|
for(int n:{0,1,2,15,100}){
|
||||||
setting[3]=std::to_string(n);p.StingerSetting(&mode,setting,replies);
|
setting[3]=std::to_string(n);p.StingerSetting(&mode,setting,replies);
|
||||||
assert(mode.settings.chance==n&&replies.back().find(" saved.")!=string::npos);Effect(replies);
|
assert(mode.settings.chance==n&&replies.back().find(" saved.")!=string::npos);Effect(replies);
|
||||||
}
|
}
|
||||||
for(string invalid:{"-1","1.0","1junk","10000000","10001","1e1","+1"}){
|
for(string invalid:{"-1","1.0","1junk","10000000","101","1e1","+1"}){
|
||||||
setting[3]=invalid;p.StingerSetting(&mode,setting,replies);
|
setting[3]=invalid;p.StingerSetting(&mode,setting,replies);
|
||||||
assert(mode.settings.chance==10000&&replies.back().find("previous setting retained")!=string::npos&&replies.back().find(" saved.")==string::npos);Effect(replies);
|
assert(mode.settings.chance==100&&replies.back().find("previous setting retained")!=string::npos&&replies.back().find(" saved.")==string::npos);Effect(replies);
|
||||||
}
|
}
|
||||||
setting[3]="0";mode.settings.fail=true;p.StingerSetting(&mode,setting,replies);
|
setting[3]="0";mode.settings.fail=true;p.StingerSetting(&mode,setting,replies);
|
||||||
assert(mode.settings.chance==10000&&replies.back().find("previous setting retained")!=string::npos);Effect(replies);
|
assert(mode.settings.chance==100&&replies.back().find("previous setting retained")!=string::npos);Effect(replies);
|
||||||
int writes=mode.settings.writes;
|
int writes=mode.settings.writes;
|
||||||
for(Array<string> malformed: {Array<string>{"hq","stinger"},Array<string>{"hq","stinger","set"},Array<string>{"hq","stinger","show","extra"}}){
|
for(Array<string> malformed: {Array<string>{"hq","stinger"},Array<string>{"hq","stinger","set"},Array<string>{"hq","stinger","show","extra"}}){
|
||||||
p.StingerSetting(&mode,malformed,replies);assert(mode.settings.writes==writes&&replies.back().find("previous setting retained")!=string::npos);
|
p.StingerSetting(&mode,malformed,replies);assert(mode.settings.writes==writes&&replies.back().find("previous setting retained")!=string::npos);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ MODE=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
|||||||
ACTORS=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c'
|
ACTORS=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Actors.c'
|
||||||
|
|
||||||
def cpp(s):
|
def cpp(s):
|
||||||
s=s.replace('protected ','').replace('Replication.IsServer()','server').replace('Replication.BumpMe()','Bump()')
|
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('protected ','').replace('Replication.IsServer()','server').replace('Replication.BumpMe()','Bump()')
|
||||||
s=s.replace('ERR_Phase.DEFEAT','5').replace('ERR_Phase.PRISON','1').replace('ERR_Phase.RADIO_SEARCH','2').replace('ERR_MissionRules.Terminal(m_Phase)','(m_Phase>=5)')
|
s=s.replace('ERR_Phase.DEFEAT','5').replace('ERR_Phase.PRISON','1').replace('ERR_Phase.RADIO_SEARCH','2').replace('ERR_MissionRules.Terminal(m_Phase)','(m_Phase>=5)')
|
||||||
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('foreach (int id : connected)','for (int id : connected)')
|
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('foreach (int id : connected)','for (int id : connected)')
|
||||||
s=s.replace('SCR_PlayerController controller = SCR_PlayerController.Cast(', 'Controller* controller = Controller::Cast(')
|
s=s.replace('SCR_PlayerController controller = SCR_PlayerController.Cast(', 'Controller* controller = Controller::Cast(')
|
||||||
@@ -32,7 +32,8 @@ struct Players {int count=0;Controller controller;int GetPlayerCount(){return co
|
|||||||
struct Queue {std::vector<std::string> removed;void Remove(std::string s){removed.push_back(s);}}queue;
|
struct Queue {std::vector<std::string> removed;void Remove(std::string s){removed.push_back(s);}}queue;
|
||||||
struct Game {Players* GetPlayerManager(){return &players;}Queue* GetCallqueue(){return &queue;}}game;Game* GetGame(){return &game;}
|
struct Game {Players* GetPlayerManager(){return &players;}Queue* GetCallqueue(){return &queue;}}game;Game* GetGame(){return &game;}
|
||||||
struct Hook {int calls=0;std::function<void()> callback;std::string name;void Invoke(float=0){calls++;events.push_back(name);if(callback)callback();}};
|
struct Hook {int calls=0;std::function<void()> callback;std::string name;void Invoke(float=0){calls++;events.push_back(name);if(callback)callback();}};
|
||||||
struct Work {int calls=0;std::string name;std::function<void()> callback;void Tick(float=0){calls++;events.push_back(name);if(callback)callback();}};
|
struct ERR_HQ {static void Event(std::string,std::string){}};
|
||||||
|
struct Work {bool DefeatDue(float){return false;}int calls=0;std::string name;std::function<void()> callback;void Tick(float=0){calls++;events.push_back(name);if(callback)callback();}};
|
||||||
struct Roster {bool survivor=true;bool HasSurvivor(){return survivor;}}roster;
|
struct Roster {bool survivor=true;bool HasSurvivor(){return survivor;}}roster;
|
||||||
struct Entity {bool group=false;};int deleted=0;void Delete(Entity*){deleted++;}
|
struct Entity {bool group=false;};int deleted=0;void Delete(Entity*){deleted++;}
|
||||||
struct Command {
|
struct Command {
|
||||||
|
|||||||
@@ -33,13 +33,20 @@ int main(){
|
|||||||
files["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},
|
files["old"]={{"reportIntervalSeconds",10},{"supportSquadLimit",4},{"responseRadiusMetres",2000},
|
||||||
{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
|
{"aiIntelChancePercent",60},{"intelRevealChancePercent",15},{"radioDefenceSeconds",180}};
|
||||||
Settings s;Map* loaded=nullptr;assert(s.Read("old",loaded)&&loaded->Get("automaticConvoyEnabled")==1);
|
Settings s;Map* loaded=nullptr;assert(s.Read("old",loaded)&&loaded->Get("automaticConvoyEnabled")==1);
|
||||||
assert(loaded->Get("reportIntervalSeconds")==10&&loaded->Get("stingerChanceOneIn")==15);
|
assert(loaded->Get("reportIntervalSeconds")==10&&loaded->Get("antiAirChancePercent")==7);
|
||||||
files["disabled"]=files["old"];files["disabled"]["automaticConvoyEnabled"]=0;
|
files["disabled"]=files["old"];files["disabled"]["automaticConvoyEnabled"]=0;
|
||||||
assert(s.Read("disabled",loaded)&&loaded->Get("automaticConvoyEnabled")==0);
|
assert(s.Read("disabled",loaded)&&loaded->Get("automaticConvoyEnabled")==0);
|
||||||
files["enabled"]=files["old"];files["enabled"]["automaticConvoyEnabled"]=1;
|
files["enabled"]=files["old"];files["enabled"]["automaticConvoyEnabled"]=1;
|
||||||
assert(s.Read("enabled",loaded)&&loaded->Get("automaticConvoyEnabled")==1);
|
assert(s.Read("enabled",loaded)&&loaded->Get("automaticConvoyEnabled")==1);
|
||||||
for(float bad:{-1.0f,2.0f,0.5f}){files["invalid"]=files["old"];files["invalid"]["automaticConvoyEnabled"]=bad;assert(!s.Read("invalid",loaded));}
|
for(float bad:{-1.0f,2.0f,0.5f}){files["invalid"]=files["old"];files["invalid"]["automaticConvoyEnabled"]=bad;assert(!s.Read("invalid",loaded));}
|
||||||
files["incomplete"]=files["old"];files["incomplete"].erase("reportIntervalSeconds");assert(!s.Read("incomplete",loaded));
|
files["incomplete"]=files["old"];files["incomplete"].erase("reportIntervalSeconds");assert(!s.Read("incomplete",loaded));
|
||||||
|
for(auto pair:std::map<int,int>{{0,0},{1,100},{15,7},{37,3},{10000,0}}){
|
||||||
|
files["legacy"]=files["old"];files["legacy"]["stingerChanceOneIn"]=pair.first;
|
||||||
|
assert(s.Read("legacy",loaded)&&loaded->Get("antiAirChancePercent")==pair.second);
|
||||||
|
}
|
||||||
|
for(float bad:{-1.0f,10001.0f,0.5f}){files["legacy"]=files["old"];files["legacy"]["stingerChanceOneIn"]=bad;assert(!s.Read("legacy",loaded));}
|
||||||
|
files["new"]=files["old"];files["new"]["stingerChanceOneIn"]=15;files["new"]["antiAirChancePercent"]=100;
|
||||||
|
assert(s.Read("new",loaded)&&loaded->Get("antiAirChancePercent")==100&&loaded->Get("antiPersonnelMineChancePercent")==60);
|
||||||
assert(!s.Read("missing",loaded));
|
assert(!s.Read("missing",loaded));
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
|
|||||||
@@ -1,139 +1,8 @@
|
|||||||
"""Execute actual scoped skill and finite native-equipment policy with API doubles."""
|
"""Execute actual finite equipment coordinator; individual grant policies have separate tests."""
|
||||||
import os,re
|
import os,re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from policy_harness import ROOT,run
|
from policy_harness import ROOT,run
|
||||||
source=Path(os.environ.get('ERR_EQUIPMENT_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c'))).read_text()
|
source=Path(os.environ.get('ERR_EQUIPMENT_SOURCE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyEquipment.c'))).read_text()
|
||||||
code=source[:source.index('class ERR_EnemyEquipmentGroup')]
|
|
||||||
code=re.sub(r'//[^\n]*','',code)
|
|
||||||
code=code.replace('modded class SCR_AICombatComponent','struct SCR_AICombatComponent : NativeCombat')
|
|
||||||
code=code.replace('class ERR_EnemyEquipmentCallback : ScriptedInventoryOperationCallback','struct ERR_EnemyEquipmentCallback')
|
|
||||||
code=code.replace('class ERR_EnemyEquipmentRules','struct ERR_EnemyEquipmentRules').replace('class ERR_EnemyEquipmentEntry','struct ERR_EnemyEquipmentEntry')
|
|
||||||
code=code.replace('protected ','').replace('override ','').replace('ref ','')
|
|
||||||
code=code.replace('static const ResourceName','static constexpr const char*').replace('static const int','static constexpr int')
|
|
||||||
code=code.replace('string result','std::string result').replace('array<IEntity>','std::vector<IEntity*>')
|
|
||||||
code=code.replace('BaseWeaponManagerComponent.Cast(Actor.FindComponent(BaseWeaponManagerComponent))','Actor->weapons')
|
|
||||||
code=code.replace('InventoryStorageManagerComponent.Cast(Actor.FindComponent(InventoryStorageManagerComponent))','Actor->inventory')
|
|
||||||
for cls in ('ERR_GameMode','SCR_AIGroup','SCR_AIGroupUtilityComponent','IEntity','SCR_AICombatComponent','ERR_EnemyEquipmentCallback','ERR_EnemyOptics','BaseWeaponManagerComponent','InventoryStorageManagerComponent'):
|
|
||||||
code=re.sub(r'\b'+cls+r' ([a-zA-Z_]\w*)',cls+r'* \1',code)
|
|
||||||
for obj in ('mode','group','utility','Actor','Group','Combat','Optics','Operation','item','weapons','inventory'):
|
|
||||||
code=code.replace(obj+'.',obj+'->')
|
|
||||||
for cls in ('Replication','SCR_Global','ERR_GameMode','ERR_Actors','ERR_EnemyEquipmentRules','ERR_EnemyGunnerSkill','EAISkill','ERR_HQ','Math','EStoragePurpose'):
|
|
||||||
code=code.replace(cls+'.',cls+'::')
|
|
||||||
code=code.replace('GetPrefabData().','GetPrefabData()->').replace('Settings().','Settings()->').replace('foreach (','for (').replace('null','nullptr').replace('super.','NativeCombat::')
|
|
||||||
code=code.replace('new ERR_EnemyEquipmentCallback()','NewOperation()')
|
|
||||||
code=re.sub(r'\b(bool|int|EAISkill|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',code)
|
|
||||||
code=code.replace('\n}\n','\n};\n')
|
|
||||||
# Forward factory declaration permits bounded managed operation storage below.
|
|
||||||
code=code.replace('struct ERR_EnemyEquipmentEntry','ERR_EnemyEquipmentCallback* NewOperation();\nstruct ERR_EnemyEquipmentEntry')
|
|
||||||
run(r'''
|
|
||||||
#include <cassert>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
#include <memory>
|
|
||||||
enum class EAISkill {ROOKIE,REGULAR,EXPERT,CYLON};
|
|
||||||
namespace Replication {bool server=true;bool IsServer(){return server;}}
|
|
||||||
namespace SCR_Global {bool editor=false;bool IsEditMode(){return editor;}}
|
|
||||||
namespace EStoragePurpose {constexpr int PURPOSE_WEAPON_PROXY=7;}
|
|
||||||
namespace ERR_HQ {void Event(std::string,std::string){}}
|
|
||||||
struct SCR_AIGroupUtilityComponent {bool military=true;bool IsMilitary(){return military;}};
|
|
||||||
struct SCR_AIGroup {
|
|
||||||
SCR_AIGroupUtilityComponent utility;int players=0,faction=1;bool ready=true;
|
|
||||||
SCR_AIGroupUtilityComponent* GetGroupUtilityComponent(){return ready?&utility:nullptr;}
|
|
||||||
int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}
|
|
||||||
};
|
|
||||||
struct Settings {int oneIn=15;int Get(std::string key){assert(key=="stingerChanceOneIn");return oneIn;}};
|
|
||||||
struct ERR_GameMode {
|
|
||||||
static inline ERR_GameMode* current=nullptr;bool active=true;struct Settings values;
|
|
||||||
static ERR_GameMode* Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==1;}
|
|
||||||
struct Settings* Settings(){return &values;}
|
|
||||||
};
|
|
||||||
namespace Math {int draw=0,calls=0;int RandomInt(int lo,int hi){assert(lo==0&&hi>0);calls++;return draw;}}
|
|
||||||
struct Prefab {std::string name;std::string GetPrefabName(){return name;}};
|
|
||||||
struct ERR_EnemyEquipmentCallback;
|
|
||||||
struct InventoryStorageManagerComponent {
|
|
||||||
bool accept=true;int requests=0;ERR_EnemyEquipmentCallback* callback=nullptr;
|
|
||||||
bool TrySpawnPrefabToStorage(const char* prefab,void* target,int slot,int purpose,ERR_EnemyEquipmentCallback* op){assert(std::string(prefab).find("3DF204EEE0534EF2")!=std::string::npos);assert(!target&&slot==-1&&purpose==7);requests++;callback=op;return accept;}
|
|
||||||
};
|
|
||||||
struct IEntity;
|
|
||||||
struct BaseWeaponManagerComponent {std::vector<IEntity*> items;void GetWeaponsList(std::vector<IEntity*>& out){out=items;}};
|
|
||||||
struct IEntity {
|
|
||||||
bool player=false,alive=true;SCR_AIGroup* group=nullptr;Prefab* prefab=nullptr;
|
|
||||||
BaseWeaponManagerComponent* weapons=nullptr;InventoryStorageManagerComponent* inventory=nullptr;
|
|
||||||
Prefab* GetPrefabData(){return prefab;}
|
|
||||||
};
|
|
||||||
namespace ERR_Actors {bool CanFight(IEntity* actor){return actor->alive;}bool Player(IEntity* actor){return actor->player;}SCR_AIGroup* Group(IEntity* actor){return actor->group;}}
|
|
||||||
struct NativeCombat {
|
|
||||||
IEntity* owner=nullptr;EAISkill skill=EAISkill::REGULAR;
|
|
||||||
IEntity* GetOwner(){return owner;}EAISkill GetAISkill(){return skill;}
|
|
||||||
virtual void SetAISkill(EAISkill value){skill=value;}virtual void ResetAISkill(){skill=EAISkill::ROOKIE;}
|
|
||||||
};
|
|
||||||
namespace ERR_EnemyGunnerSkill {
|
|
||||||
bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*){return false;}
|
|
||||||
bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*){return false;}
|
|
||||||
EAISkill Profile(EAISkill requested,bool){return EAISkill::CYLON;}
|
|
||||||
}
|
|
||||||
struct ERR_EnemyOptics{int stops=0;void Stop(){stops++;}};
|
|
||||||
''' + code + r'''
|
|
||||||
std::vector<std::unique_ptr<ERR_EnemyEquipmentCallback>> operations;
|
|
||||||
ERR_EnemyEquipmentCallback* NewOperation(){operations.emplace_back(new ERR_EnemyEquipmentCallback());return operations.back().get();}
|
|
||||||
struct Soldier {
|
|
||||||
SCR_AIGroup group;BaseWeaponManagerComponent weapons;InventoryStorageManagerComponent inventory;IEntity actor;SCR_AICombatComponent combat;ERR_EnemyOptics optics;
|
|
||||||
Soldier(){actor.group=&group;actor.weapons=&weapons;actor.inventory=&inventory;combat.owner=&actor;}
|
|
||||||
ERR_EnemyEquipmentEntry Entry(){ERR_EnemyEquipmentEntry e;e.Actor=&actor;e.Group=&group;e.Combat=&combat;e.Optics=&optics;return e;}
|
|
||||||
};
|
|
||||||
int main(){
|
|
||||||
ERR_GameMode mode;ERR_GameMode::current=&mode;
|
|
||||||
// One draw per exact actor; accepted insertion cannot be retried, even after timeout.
|
|
||||||
for(int draw=0;draw<15;draw++){
|
|
||||||
Math::draw=draw;Math::calls=0;Soldier soldier;auto job=soldier.Entry();
|
|
||||||
for(int n=0;n<20;n++){job.Check(&mode);}
|
|
||||||
assert(job.Complete&&Math::calls==1&&soldier.inventory.requests==(draw==0?1:0));
|
|
||||||
}
|
|
||||||
Math::draw=0;mode.values.oneIn=0;Soldier off;auto disabled=off.Entry();Math::calls=0;
|
|
||||||
assert(disabled.Check(&mode)&&Math::calls==0&&off.inventory.requests==0);
|
|
||||||
mode.values.oneIn=1;
|
|
||||||
Soldier success;auto finished=success.Entry();assert(!finished.Check(&mode));
|
|
||||||
success.inventory.callback->OnComplete();success.inventory.callback->OnAllFailed();
|
|
||||||
assert(finished.Check(&mode)&&success.inventory.requests==1&&success.inventory.callback->Succeeded);
|
|
||||||
// A completed then removed launcher is never replenished.
|
|
||||||
for(int n=0;n<10;n++){finished.Check(&mode);}assert(success.inventory.requests==1);
|
|
||||||
Soldier rejected;rejected.inventory.accept=false;auto retries=rejected.Entry();
|
|
||||||
for(int n=0;n<10;n++){retries.Check(&mode);}assert(rejected.inventory.requests==4&&retries.Complete);
|
|
||||||
Soldier failed;auto again=failed.Entry();again.Check(&mode);failed.inventory.callback->OnFailed();again.Check(&mode);assert(failed.inventory.requests==2);
|
|
||||||
for(int n=0;n<10;n++){again.Check(&mode);}assert(failed.inventory.requests==2);
|
|
||||||
Soldier changed;auto captured=changed.Entry();captured.Check(&mode);changed.actor.player=true;assert(captured.Check(&mode));changed.actor.player=false;
|
|
||||||
for(int n=0;n<10;n++){captured.Check(&mode);}assert(changed.inventory.requests==1);
|
|
||||||
Soldier absent;absent.actor.inventory=nullptr;auto missing=absent.Entry();for(int n=0;n<8;n++){missing.Check(&mode);}assert(missing.Complete&&absent.inventory.requests==0);
|
|
||||||
Soldier equipped;Prefab prefab;prefab.name=ERR_EnemyEquipmentEntry::LAUNCHER;IEntity item;item.prefab=&prefab;equipped.weapons.items.push_back(&item);auto existing=equipped.Entry();assert(existing.Check(&mode)&&equipped.inventory.requests==0);
|
|
||||||
// The exact same eligibility policy protects skill binding and item submission.
|
|
||||||
for(int variant=0;variant<9;variant++){
|
|
||||||
Soldier soldier;ERR_GameMode other;auto job=soldier.Entry();Math::calls=0;
|
|
||||||
if(variant==0)Replication::server=false;
|
|
||||||
if(variant==1)SCR_Global::editor=true;
|
|
||||||
if(variant==2)mode.active=false;
|
|
||||||
if(variant==3)ERR_GameMode::current=&other;
|
|
||||||
if(variant==4)soldier.actor.player=true;
|
|
||||||
if(variant==5)soldier.actor.alive=false;
|
|
||||||
if(variant==6)soldier.group.players=1;
|
|
||||||
if(variant==7)soldier.group.faction=2;
|
|
||||||
if(variant==8)soldier.group.utility.military=false;
|
|
||||||
assert(job.Check(&mode)&&Math::calls==0&&soldier.inventory.requests==0);
|
|
||||||
soldier.combat.ERR_BindEnemySkill(&mode,&soldier.group);assert(soldier.combat.GetAISkill()==EAISkill::REGULAR);
|
|
||||||
Replication::server=true;SCR_Global::editor=false;mode.active=true;ERR_GameMode::current=&mode;
|
|
||||||
}
|
|
||||||
Soldier skill;skill.combat.ERR_BindEnemySkill(&mode,&skill.group);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
|
|
||||||
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
|
|
||||||
skill.combat.ResetAISkill();assert(skill.combat.GetAISkill()==EAISkill::CYLON);
|
|
||||||
skill.combat.SetAISkill(EAISkill::CYLON);assert(skill.combat.GetAISkill()==EAISkill::CYLON);
|
|
||||||
skill.combat.ERR_BindEnemySkill(&mode,&skill.group);skill.combat.ERR_ReleaseEnemySkill(&mode);assert(skill.combat.GetAISkill()==EAISkill::REGULAR);
|
|
||||||
skill.combat.SetAISkill(EAISkill::ROOKIE);assert(skill.combat.GetAISkill()==EAISkill::ROOKIE);
|
|
||||||
Soldier moved;SCR_AIGroup replacement;moved.combat.ERR_BindEnemySkill(&mode,&moved.group);moved.actor.group=&replacement;
|
|
||||||
moved.combat.SetAISkill(EAISkill::ROOKIE);assert(moved.combat.GetAISkill()==EAISkill::ROOKIE);
|
|
||||||
Soldier stopped;auto terminal=stopped.Entry();terminal.Check(&mode);terminal.Stop(&mode);assert(stopped.optics.stops==1);stopped.inventory.callback->OnComplete();
|
|
||||||
for(int n=0;n<10;n++){terminal.Check(&mode);}assert(stopped.inventory.requests==1&&terminal.Complete);
|
|
||||||
}
|
|
||||||
''')
|
|
||||||
print('PASS: actual enemy eligibility, scoped native skill floor/reset/restoration, one-roll inventory issuance, completion/failure, timeout quarantine, actor capture and terminal stop; native inventory runtime unproven')
|
|
||||||
coordinator=source[source.index('class ERR_EnemyEquipmentGroup'):]
|
coordinator=source[source.index('class ERR_EnemyEquipmentGroup'):]
|
||||||
coordinator=re.sub(r'//[^\n]*','',coordinator).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
coordinator=re.sub(r'//[^\n]*','',coordinator).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||||
coordinator=coordinator.replace('array<ERR_EnemyEquipmentGroup>','Array<ERR_EnemyEquipmentGroup*>').replace('array<ERR_EnemyEquipmentEntry>','Array<ERR_EnemyEquipmentEntry*>').replace('array<AIAgent>','Array<AIAgent*>')
|
coordinator=coordinator.replace('array<ERR_EnemyEquipmentGroup>','Array<ERR_EnemyEquipmentGroup*>').replace('array<ERR_EnemyEquipmentEntry>','Array<ERR_EnemyEquipmentEntry*>').replace('array<AIAgent>','Array<AIAgent*>')
|
||||||
@@ -157,10 +26,10 @@ template<typename T> struct Array:std::vector<T>{int Count(){return this->size()
|
|||||||
struct SCR_AIGroup;
|
struct SCR_AIGroup;
|
||||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;bool Active(){return active;}static ERR_GameMode* Get(){return current;}};
|
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;bool Active(){return active;}static ERR_GameMode* Get(){return current;}};
|
||||||
struct SCR_AICombatComponent{int binds=0,refreshes=0;void ERR_BindEnemySkill(ERR_GameMode*,SCR_AIGroup*){binds++;}void ERR_RefreshEnemySkill(){refreshes++;}};
|
struct SCR_AICombatComponent{int binds=0,refreshes=0;void ERR_BindEnemySkill(ERR_GameMode*,SCR_AIGroup*){binds++;}void ERR_RefreshEnemySkill(){refreshes++;}};
|
||||||
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true;};
|
struct IEntity{SCR_AICombatComponent* combat=nullptr;bool eligible=true,unconscious=false;};
|
||||||
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
|
struct AIAgent{IEntity* actor=nullptr;IEntity* GetControlledEntity(){return actor;}};
|
||||||
struct SCR_AIGroup{Array<AIAgent*> agents;void GetAgents(Array<AIAgent*>& out){out=agents;}};
|
struct SCR_AIGroup{Array<AIAgent*> agents;void GetAgents(Array<AIAgent*>& out){out=agents;}};
|
||||||
namespace ERR_EnemyEquipmentRules{bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}}
|
namespace ERR_EnemyEquipmentRules{bool Owned(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* actor){return Owned(m,g,actor)&&!actor->unconscious;}}
|
||||||
struct ERR_EnemyOptics{
|
struct ERR_EnemyOptics{
|
||||||
IEntity* actor;SCR_AIGroup* group;int checks=0,stops=0;
|
IEntity* actor;SCR_AIGroup* group;int checks=0,stops=0;
|
||||||
ERR_EnemyOptics(IEntity* a,SCR_AIGroup* g):actor(a),group(g){}
|
ERR_EnemyOptics(IEntity* a,SCR_AIGroup* g):actor(a),group(g){}
|
||||||
@@ -192,6 +61,12 @@ int main(){
|
|||||||
for(int i=1;i<=12;i++){incomplete.Tick(i*3000);}assert(incomplete.m_Entries.Count()==1&&incomplete.m_Entries[0]->SkillChecks==5);
|
for(int i=1;i<=12;i++){incomplete.Tick(i*3000);}assert(incomplete.m_Entries.Count()==1&&incomplete.m_Entries[0]->SkillChecks==5);
|
||||||
auto incompleteOptics=incomplete.m_Entries[0]->Optics;int checksBeforeStop=incompleteOptics->checks;
|
auto incompleteOptics=incomplete.m_Entries[0]->Optics;int checksBeforeStop=incompleteOptics->checks;
|
||||||
incomplete.Stop();incomplete.Tick(60000);assert(incompleteOptics->stops==1&&incompleteOptics->checks==checksBeforeStop);
|
incomplete.Stop();incomplete.Tick(60000);assert(incompleteOptics->stops==1&&incompleteOptics->checks==checksBeforeStop);
|
||||||
|
// Temporary incapacitation preserves the same finite entry and its skill binding.
|
||||||
|
ERR_EnemyEquipment recovery(&mode);SCR_AIGroup recoveringGroup;IEntity recovering;SCR_AICombatComponent recoveredCombat;
|
||||||
|
recovering.combat=&recoveredCombat;AIAgent recoveringAgent;recoveringAgent.actor=&recovering;recoveringGroup.agents.Insert(&recoveringAgent);
|
||||||
|
recovery.RegisterGroup(&recoveringGroup);recovery.Tick(3000);auto retained=recovery.m_Entries[0];
|
||||||
|
recovering.unconscious=true;recovery.Tick(6000);assert(!retained->Released&&retained->stops==0&&retained->checks==1&&recoveredCombat.refreshes==1);
|
||||||
|
recovering.unconscious=false;recovery.Tick(9000);assert(recovery.m_Entries.Count()==1&&recovery.m_Entries[0]==retained&&recoveredCombat.binds==1&&recoveredCombat.refreshes==2&&retained->checks==2);
|
||||||
ERR_EnemyEquipment capture(&mode);SCR_AIGroup capturedGroup;IEntity capturedBody;AIAgent capturedAgent;capturedAgent.actor=&capturedBody;
|
ERR_EnemyEquipment capture(&mode);SCR_AIGroup capturedGroup;IEntity capturedBody;AIAgent capturedAgent;capturedAgent.actor=&capturedBody;
|
||||||
capturedGroup.agents.Insert(&capturedAgent);capture.RegisterGroup(&capturedGroup);capture.Tick(3000);
|
capturedGroup.agents.Insert(&capturedAgent);capture.RegisterGroup(&capturedGroup);capture.Tick(3000);
|
||||||
auto capturedOptics=capture.m_Entries[0]->Optics;capturedBody.eligible=false;capture.Tick(6000);
|
auto capturedOptics=capture.m_Entries[0]->Optics;capturedBody.eligible=false;capture.Tick(6000);
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace Replication{bool server=true;bool IsServer(){return server;}}
|
|||||||
namespace SCR_Global{bool edit=false;bool IsEditMode(){return edit;}}
|
namespace SCR_Global{bool edit=false;bool IsEditMode(){return edit;}}
|
||||||
struct SCR_AIGroupUtilityComponent{bool military=true;bool IsMilitary(){return military;}};
|
struct SCR_AIGroupUtilityComponent{bool military=true;bool IsMilitary(){return military;}};
|
||||||
struct SCR_AIGroup{SCR_AIGroupUtilityComponent utility;int players=0,faction=1;auto GetGroupUtilityComponent(){return &utility;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}};
|
struct SCR_AIGroup{SCR_AIGroupUtilityComponent utility;int players=0,faction=1;auto GetGroupUtilityComponent(){return &utility;}int GetPlayerCount(bool){return players;}int GetFaction(){return faction;}};
|
||||||
struct IEntity{virtual ~IEntity(){}bool alive=true,player=false;SCR_AIGroup* group=nullptr;IEntity* vehicle=nullptr;};
|
struct IEntity{virtual ~IEntity(){}bool alive=true,player=false,unconscious=false;SCR_AIGroup* group=nullptr;IEntity* vehicle=nullptr;};
|
||||||
using Vehicle=IEntity;
|
using Vehicle=IEntity;
|
||||||
struct BaseCompartmentSlot{virtual ~BaseCompartmentSlot(){}IEntity* occupant=nullptr;IEntity* vehicle=nullptr;ECompartmentType type=ECompartmentType::TURRET;auto GetType(){return type;}auto GetOccupant(){return occupant;}auto GetVehicle(){return vehicle;}};
|
struct BaseCompartmentSlot{virtual ~BaseCompartmentSlot(){}IEntity* occupant=nullptr;IEntity* vehicle=nullptr;ECompartmentType type=ECompartmentType::TURRET;auto GetType(){return type;}auto GetOccupant(){return occupant;}auto GetVehicle(){return vehicle;}};
|
||||||
struct TurretCompartmentSlot:BaseCompartmentSlot{};
|
struct TurretCompartmentSlot:BaseCompartmentSlot{};
|
||||||
@@ -46,7 +46,7 @@ struct ERR_ConvoyVehicle{SCR_AIGroup* Group=nullptr;Vehicle* Entity=nullptr;bool
|
|||||||
struct ERR_ConvoyTask{bool Ready=true,Stopped=false;ERR_ConvoyReceipt* Receipt=nullptr;Array<ERR_ConvoyVehicle*> Vehicles;};
|
struct ERR_ConvoyTask{bool Ready=true,Stopped=false;ERR_ConvoyReceipt* Receipt=nullptr;Array<ERR_ConvoyVehicle*> Vehicles;};
|
||||||
struct ERR_HQ{ERR_ConvoyTask* task=nullptr;ERR_ConvoyTask* extra=nullptr;auto Convoys(){return this;}int Count(){return extra?2:1;}auto At(int index){return index==0?task:extra;}auto ConvoyTask(){return task;}};
|
struct ERR_HQ{ERR_ConvoyTask* task=nullptr;ERR_ConvoyTask* extra=nullptr;auto Convoys(){return this;}int Count(){return extra?2:1;}auto At(int index){return index==0?task:extra;}auto ConvoyTask(){return task;}};
|
||||||
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;ERR_HQ* hq=nullptr;Ledger* ledger=nullptr;static auto Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==1;}auto Command(){return hq;}auto Forces(){return ledger;}};
|
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;bool active=true;ERR_HQ* hq=nullptr;Ledger* ledger=nullptr;static auto Get(){return current;}bool Active(){return active;}int EnemyFaction(){return 1;}bool IsMissionEnemy(int f){return f==1;}auto Command(){return hq;}auto Forces(){return ledger;}};
|
||||||
namespace ERR_Actors{bool CanFight(IEntity* a){return a->alive;}bool Alive(IEntity* a){return a->alive;}bool Player(IEntity* a){return a->player;}auto Group(IEntity* a){return a->group;}}
|
namespace ERR_Actors{bool CanFight(IEntity* a){return a->alive&&!a->unconscious;}bool Alive(IEntity* a){return a->alive;}bool Player(IEntity* a){return a->player;}auto Group(IEntity* a){return a->group;}}
|
||||||
namespace ERR_ConvoyRules{bool capture=false;bool Captured(Vehicle*,SCR_AIGroup* group){return capture||group->players>0;}bool PartOf(IEntity* a,IEntity* b){return a==b;}}
|
namespace ERR_ConvoyRules{bool capture=false;bool Captured(Vehicle*,SCR_AIGroup* group){return capture||group->players>0;}bool PartOf(IEntity* a,IEntity* b){return a==b;}}
|
||||||
namespace CompartmentAccessComponent{auto GetVehicleIn(IEntity* actor){return actor->vehicle;}}
|
namespace CompartmentAccessComponent{auto GetVehicleIn(IEntity* actor){return actor->vehicle;}}
|
||||||
struct ERR_EnemyGunnerSkill{static ERR_ConvoyTask* TaskForGroup(ERR_HQ*,SCR_AIGroup*);static bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*);static bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*);static EAISkill Profile(EAISkill,bool);};
|
struct ERR_EnemyGunnerSkill{static ERR_ConvoyTask* TaskForGroup(ERR_HQ*,SCR_AIGroup*);static bool CapturedGroup(ERR_GameMode*,SCR_AIGroup*);static bool OriginalGunner(ERR_GameMode*,SCR_AIGroup*,IEntity*);static EAISkill Profile(EAISkill,bool);};
|
||||||
@@ -60,6 +60,9 @@ int main(){
|
|||||||
ERR_ForceRecord record;Ledger ledger;ledger.record=&record;mode.ledger=&ledger;
|
ERR_ForceRecord record;Ledger ledger;ledger.record=&record;mode.ledger=&ledger;
|
||||||
assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));
|
assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));
|
||||||
ERR_ConvoyTask empty;hq.task=∅hq.extra=&task;assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));hq.task=&task;hq.extra=nullptr;
|
ERR_ConvoyTask empty;hq.task=∅hq.extra=&task;assert(ERR_EnemyGunnerSkill::OriginalGunner(&mode,&group,&actor));hq.task=&task;hq.extra=nullptr;
|
||||||
|
actor.unconscious=true;
|
||||||
|
assert(ERR_EnemyEquipmentRules::Owned(&mode,&group,&actor)&&!ERR_EnemyEquipmentRules::Eligible(&mode,&group,&actor));
|
||||||
|
actor.unconscious=false;
|
||||||
// Every native/canonical ownership fact independently matters.
|
// Every native/canonical ownership fact independently matters.
|
||||||
for(int i=0;i<25;i++){
|
for(int i=0;i<25;i++){
|
||||||
auto savedActor=actor;auto savedVehicle=vehicle;auto savedGroup=group;auto savedUnit=unit;auto savedTask=task;auto savedReceipt=receipt;auto savedRecord=record;auto savedSlot=slot;
|
auto savedActor=actor;auto savedVehicle=vehicle;auto savedGroup=group;auto savedUnit=unit;auto savedTask=task;auto savedReceipt=receipt;auto savedRecord=record;auto savedSlot=slot;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""Execute production finite launcher/fallback/mine controllers with native boundaries."""
|
||||||
|
from policy_harness import ROOT, run, body
|
||||||
|
import re
|
||||||
|
source=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_EnemyLoadout.c'
|
||||||
|
s=source.read_text();s=s[s.index('class ERR_EnemyLauncherGrant'):]
|
||||||
|
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
|
||||||
|
s=s.replace('ResourceName','string').replace('array<IEntity>','Array<IEntity*>')
|
||||||
|
s=s.replace('InventoryStorageManagerComponent.Cast(actor.FindComponent(InventoryStorageManagerComponent))','actor->inventory').replace('BaseWeaponManagerComponent.Cast(actor.FindComponent(BaseWeaponManagerComponent))','actor->manager')
|
||||||
|
s=s.replace('item.FindComponent(SCR_AntiPersonnelMineCollisionHandlerComponent)','item->mine')
|
||||||
|
for typ in ('ERR_GameMode','SCR_AIGroup','IEntity','InventoryStorageManagerComponent','BaseWeaponManagerComponent','ERR_EnemyEquipmentCallback'):
|
||||||
|
s=re.sub(r'\b'+typ+r' (\w+)',typ+r'* \1',s)
|
||||||
|
for obj in ('actor','group','mode','inventory','manager','m_Operation','existing','item'):s=s.replace(obj+'.',obj+'->')
|
||||||
|
s=s.replace('Settings().','Settings()->').replace('GetPrefabData().','GetPrefabData()->')
|
||||||
|
for obj in ('ERR_HQ','Math','ERR_EnemyLoadoutRules','ERR_EnemyEquipmentRules','ERR_EnemyLoadoutCatalog','EStoragePurpose','string'):s=s.replace(obj+'.',obj+'::')
|
||||||
|
s=s.replace('string::Format(', 'Format(').replace('foreach (','for (').replace('null','nullptr')
|
||||||
|
s=re.sub(r'\b(bool|int|\w+\*) (\w+);',r'\1 \2 {};',s).replace('\n}\n','\n};\n')
|
||||||
|
selected=body(source,'static bool Selected(')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
using string=std::string;
|
||||||
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}};
|
||||||
|
struct ERR_EnemyEquipmentCallback{bool Finished=false,Succeeded=false;};
|
||||||
|
struct InventoryStorageManagerComponent;struct BaseWeaponManagerComponent;
|
||||||
|
struct IEntity{string prefab;bool mine=false,loaded=true,eligible=true;InventoryStorageManagerComponent* inventory=nullptr;BaseWeaponManagerComponent* manager=nullptr;
|
||||||
|
IEntity* GetPrefabData(){return this;}string GetPrefabName(){return prefab;}int GetOrigin(){return 0;}};
|
||||||
|
struct BaseWeaponManagerComponent{Array<IEntity*> weapons;void GetWeaponsList(Array<IEntity*>& out){out=weapons;}};
|
||||||
|
namespace EStoragePurpose{const int PURPOSE_WEAPON_PROXY=1,PURPOSE_DEPOSIT=2;}
|
||||||
|
struct InventoryStorageManagerComponent{Array<IEntity*> items;ERR_EnemyEquipmentCallback* pending=nullptr;bool accept=true,deleteAccept=true;int requests=0,deletes=0;string last;IEntity* deleting=nullptr;
|
||||||
|
void GetItems(Array<IEntity*>& out){out=items;}
|
||||||
|
bool TrySpawnPrefabToStorage(string prefab,void*,int,int,ERR_EnemyEquipmentCallback* op){requests++;last=prefab;pending=op;return accept;}
|
||||||
|
bool TryDeleteItem(IEntity* item,ERR_EnemyEquipmentCallback* op){assert(items.Contains(item));deletes++;deleting=item;pending=op;return deleteAccept;}
|
||||||
|
void Complete(bool ok){pending->Finished=true;pending->Succeeded=ok;}
|
||||||
|
};
|
||||||
|
struct Settings{int aa=100,mines=100;int Runtime(string key){return key=="antiAirChancePercent"?aa:mines;}};
|
||||||
|
struct ERR_GameMode{struct Settings settings;struct Settings* Settings(){return &settings;}};
|
||||||
|
struct SCR_AIGroup{int GetFaction(){return 1;}};
|
||||||
|
namespace ERR_EnemyEquipmentRules{bool Eligible(ERR_GameMode*,SCR_AIGroup*,IEntity* actor){return actor&&actor->eligible;}}
|
||||||
|
namespace Math{int roll=0;int RandomInt(int,int){return roll;}int Min(int a,int b){return std::min(a,b);}}
|
||||||
|
namespace ERR_HQ{void Event(string,string){}}
|
||||||
|
template<class... Args>string Format(string,Args...){return "audit";}
|
||||||
|
struct ERR_EnemyLoadoutRules{
|
||||||
|
'''+selected+r'''
|
||||||
|
static bool AntiAirName(string prefab){return prefab=="IGLA"||prefab=="FIM";}
|
||||||
|
};
|
||||||
|
struct ERR_EnemyLoadoutCatalog{
|
||||||
|
static inline string FIM="FIM",MINE="MINE",preferred="IGLA";static inline bool available=true;
|
||||||
|
static string Launcher(ERR_GameMode*,int){return preferred;}static bool Available(string){return available;}
|
||||||
|
static bool Loaded(IEntity* item){return item&&item->loaded;}
|
||||||
|
static IEntity* Find(string name,Array<IEntity*>& items,Array<IEntity*>& excluded){for(auto* i:items)if(i->prefab==name&&!excluded.Contains(i))return i;return nullptr;}
|
||||||
|
};
|
||||||
|
'''+s+r'''
|
||||||
|
struct Soldier{IEntity actor,rifle;InventoryStorageManagerComponent inv;BaseWeaponManagerComponent mgr;SCR_AIGroup group;
|
||||||
|
Soldier(){actor.inventory=&inv;actor.manager=&mgr;rifle.prefab="RIFLE";inv.items={&rifle};mgr.weapons={&rifle};}
|
||||||
|
void Add(IEntity& item,bool weapon){inv.items.Insert(&item);if(weapon)mgr.weapons.Insert(&item);}
|
||||||
|
};
|
||||||
|
int main(){
|
||||||
|
for(int percent=0;percent<=100;percent++){int chosen=0;for(int roll=0;roll<100;roll++)chosen+=ERR_EnemyLoadoutRules::Selected(percent,roll);assert(chosen==percent);}
|
||||||
|
assert(!ERR_EnemyLoadoutRules::Selected(-1,0)&&!ERR_EnemyLoadoutRules::Selected(101,0));
|
||||||
|
ERR_GameMode mode;Math::roll=99;
|
||||||
|
Soldier off;ERR_EnemyLauncherGrant noaa;ERR_EnemyMineGrant nomines;mode.settings.aa=mode.settings.mines=0;
|
||||||
|
noaa.Check(&mode,&off.group,&off.actor);nomines.Check(&mode,&off.group,&off.actor);assert(off.inv.requests==0);
|
||||||
|
mode.settings.aa=mode.settings.mines=100;
|
||||||
|
Soldier good;ERR_EnemyLauncherGrant aa;aa.Check(&mode,&good.group,&good.actor);assert(good.inv.last=="IGLA");
|
||||||
|
IEntity igla;igla.prefab="IGLA";good.Add(igla,true);good.inv.Complete(true);aa.Check(&mode,&good.group,&good.actor);assert(aa.m_Finished&&good.inv.requests==1);
|
||||||
|
good.mgr.weapons.clear();good.inv.items.clear();for(int n=0;n<20;n++)aa.Check(&mode,&good.group,&good.actor);assert(good.inv.requests==1);
|
||||||
|
Soldier reject;ERR_EnemyLauncherGrant fallback;reject.inv.accept=false;fallback.Check(&mode,&reject.group,&reject.actor);assert(fallback.m_Fallback);
|
||||||
|
reject.inv.accept=true;fallback.Check(&mode,&reject.group,&reject.actor);assert(reject.inv.last=="FIM"&&reject.inv.requests==2);
|
||||||
|
IEntity fim;fim.prefab="FIM";reject.Add(fim,true);reject.inv.Complete(true);fallback.Check(&mode,&reject.group,&reject.actor);assert(fallback.m_Finished);
|
||||||
|
Soldier broken;ERR_EnemyLauncherGrant bad;bad.Check(&mode,&broken.group,&broken.actor);IEntity empty;empty.prefab="IGLA";empty.loaded=false;broken.Add(empty,true);broken.inv.Complete(true);
|
||||||
|
bad.Check(&mode,&broken.group,&broken.actor);assert(broken.inv.deletes==1&&broken.inv.deleting==&empty&&broken.inv.items.Contains(&broken.rifle));
|
||||||
|
broken.inv.items={&broken.rifle};broken.mgr.weapons={&broken.rifle};broken.inv.Complete(true);bad.Check(&mode,&broken.group,&broken.actor);assert(bad.m_Fallback);
|
||||||
|
bad.Check(&mode,&broken.group,&broken.actor);assert(broken.inv.requests==2&&broken.inv.last=="FIM");
|
||||||
|
Soldier pending;ERR_EnemyLauncherGrant wait;for(int n=0;n<30;n++)wait.Check(&mode,&pending.group,&pending.actor);assert(wait.m_Finished&&pending.inv.requests==1);pending.inv.Complete(true);wait.Check(&mode,&pending.group,&pending.actor);assert(pending.inv.requests==1);
|
||||||
|
Soldier looted;ERR_EnemyLauncherGrant lost;lost.Check(&mode,&looted.group,&looted.actor);looted.inv.Complete(true);lost.Check(&mode,&looted.group,&looted.actor);assert(lost.m_Finished&&!lost.m_Fallback&&looted.inv.requests==1);
|
||||||
|
Soldier captured;ERR_EnemyLauncherGrant ownership;ownership.Check(&mode,&captured.group,&captured.actor);captured.actor.eligible=false;ownership.Check(&mode,&captured.group,&captured.actor);assert(ownership.m_Finished&&captured.inv.requests==1);
|
||||||
|
Soldier miner;ERR_EnemyMineGrant mines;mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==1);IEntity mine1,mine2;mine1.mine=mine2.mine=true;miner.Add(mine1,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);miner.Add(mine2,false);miner.inv.Complete(true);mines.Check(&mode,&miner.group,&miner.actor);assert(mines.m_Finished);
|
||||||
|
miner.inv.items.clear();for(int n=0;n<20;n++)mines.Check(&mode,&miner.group,&miner.actor);assert(miner.inv.requests==2);
|
||||||
|
Soldier full;ERR_EnemyMineGrant existing;full.Add(mine1,false);full.Add(mine2,false);existing.Check(&mode,&full.group,&full.actor);assert(existing.m_Finished&&full.inv.requests==0);
|
||||||
|
Soldier dropped;ERR_EnemyMineGrant finite;finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);dropped.inv.Complete(true);finite.Check(&mode,&dropped.group,&dropped.actor);assert(finite.m_Finished&&dropped.inv.requests==2);
|
||||||
|
Soldier noSpace;noSpace.inv.accept=false;ERR_EnemyMineGrant noLoose;noLoose.Check(&mode,&noSpace.group,&noSpace.actor);assert(noLoose.m_Finished&&noSpace.inv.requests==1);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: actual percentage selection, native/preferred launcher preservation, failure/removal fallback, pending-operation quarantine, capture, finite two-mine grants and no replenishment')
|
||||||
@@ -62,7 +62,7 @@ struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;bool active=true;
|
|||||||
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
struct Replication{inline static bool server=true;static bool IsServer(){return server;}};
|
||||||
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
|
struct SCR_Global{inline static bool editor=false;static bool IsEditMode(){return editor;}};
|
||||||
struct ERR_HQ{inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
|
struct ERR_HQ{inline static Array<string>events;static void Event(string,string message){events.Insert(message);}};
|
||||||
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}};
|
struct ERR_EnemyEquipmentRules{static bool Owned(ERR_GameMode*m,Group*g,IEntity a){return m&&g&&a&&a->eligible&&a->group==g;}static bool Eligible(ERR_GameMode*m,Group*g,IEntity a){return Owned(m,g,a);}};
|
||||||
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
|
struct Inventory{Array<IEntity>items;Array<int>requests;bool basicFits=true,manpackFits=true,accept=true;int callback=0;ScriptedInventoryOperationCallback*last=nullptr;
|
||||||
void GetItems(Array<IEntity>&out){out=items;}
|
void GetItems(Array<IEntity>&out){out=items;}
|
||||||
int Kind(ResourceName p){return p.find("R107M")!=string::npos?2:1;}
|
int Kind(ResourceName p){return p.find("R107M")!=string::npos?2:1;}
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
"""Execute the actual faction button handler, including Random-all submission."""
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
p = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_FactionMenu.c'
|
||||||
|
code = body(p, 'protected void OnFactionButton(')
|
||||||
|
code = (code.replace('protected ', '').replace('SCR_ButtonBaseComponent button', 'SCR_ButtonBaseComponent* button')
|
||||||
|
.replace('button.GetRootWidget().GetName()', 'button->name')
|
||||||
|
.replace('foreach (', 'for (').replace('ERR_FactionChoices.Roles()', 'Roles()')
|
||||||
|
.replace('SCR_ComboBoxComponent.GetComboBoxComponent(', 'GetCombo(')
|
||||||
|
.replace(').SetCurrentItem(', ')->SetCurrentItem(').replace(').GetCurrentIndex()', ')->GetCurrentIndex()')
|
||||||
|
.replace('m_Controller.ERR_AdminSubmit', 'm_Controller->ERR_AdminSubmit'))
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
using string=std::string;
|
||||||
|
struct IDs:std::vector<string>{using std::vector<string>::vector;int Count(){return size();}};
|
||||||
|
struct Combo{int index=1;void SetCurrentItem(int i){index=i;}int GetCurrentIndex(){return index;}};
|
||||||
|
struct SCR_ButtonBaseComponent{string name;};
|
||||||
|
struct Controller{std::vector<string> commands;void ERR_AdminSubmit(string cmd,bool snapshot){assert(snapshot);commands.push_back(cmd);}};
|
||||||
|
struct Menu {
|
||||||
|
bool m_Ready=true,focused=true,m_FactionFieldsInitialized=true;
|
||||||
|
float m_Age=0,m_ClickDelay=0;
|
||||||
|
Controller controller;Controller* m_Controller=&controller;
|
||||||
|
IDs m_FactionIds={"random","us","ussr","fia"};
|
||||||
|
std::map<string,Combo> combos;
|
||||||
|
string feedback;
|
||||||
|
static IDs Roles(){return {"escapists","enemy","independent"};}
|
||||||
|
bool IsFocused(){return focused;}int GetRootWidget(){return 0;}
|
||||||
|
Combo* GetCombo(string name,int){return &combos[name];}
|
||||||
|
void SetText(string field,string value){assert(field=="Feedback");feedback=value;}
|
||||||
|
'''+code+r'''
|
||||||
|
};
|
||||||
|
int main(){
|
||||||
|
SCR_ButtonBaseComponent random{"FactionsRandomAll"}, apply{"FactionsApply"}, other{"Other"};
|
||||||
|
Menu m;
|
||||||
|
m.OnFactionButton(&random);
|
||||||
|
assert(m.controller.commands.size()==1&&m.controller.commands[0]=="factions set random random random");
|
||||||
|
for(auto role:Menu::Roles())assert(m.combos["FactionSelect_"+role].index==0);
|
||||||
|
assert(m.feedback=="Saving faction selections..."&&m.m_ClickDelay>0);
|
||||||
|
m.OnFactionButton(&random);assert(m.controller.commands.size()==1); // debounce
|
||||||
|
for(int guard=0;guard<6;guard++){
|
||||||
|
Menu refused;
|
||||||
|
if(guard==0)refused.m_Ready=false;
|
||||||
|
if(guard==1)refused.focused=false;
|
||||||
|
if(guard==2)refused.m_Controller=nullptr;
|
||||||
|
if(guard==3)refused.m_Age=7;
|
||||||
|
if(guard==4)refused.m_ClickDelay=1;
|
||||||
|
if(guard==5)refused.m_FactionFieldsInitialized=false;
|
||||||
|
refused.OnFactionButton(&random);assert(refused.controller.commands.empty()&&refused.combos.empty());
|
||||||
|
}
|
||||||
|
Menu manual;
|
||||||
|
manual.combos["FactionSelect_escapists"].index=1;
|
||||||
|
manual.combos["FactionSelect_enemy"].index=0;
|
||||||
|
manual.combos["FactionSelect_independent"].index=3;
|
||||||
|
manual.OnFactionButton(&apply);
|
||||||
|
assert(manual.controller.commands.size()==1&&manual.controller.commands[0]=="factions set us random fia");
|
||||||
|
Menu invalid;invalid.combos["FactionSelect_enemy"].index=99;
|
||||||
|
invalid.OnFactionButton(&apply);invalid.OnFactionButton(&other);invalid.OnFactionButton(nullptr);
|
||||||
|
assert(invalid.controller.commands.empty());
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: Random all submits persistent random roles immediately; manual mixed choices, stale/unauthorized UI guards and debounce preserved')
|
||||||
@@ -32,6 +32,7 @@ int main(){
|
|||||||
for(int n=0;n<1000;n++){
|
for(int n=0;n<1000;n++){
|
||||||
assert(Rules::Resolve({"us","ussr","fia","rhs"},saved,resolved));
|
assert(Rules::Resolve({"us","ussr","fia","rhs"},saved,resolved));
|
||||||
assert(Rules::Distinct(resolved)&&resolved.size()==3);
|
assert(Rules::Distinct(resolved)&&resolved.size()==3);
|
||||||
|
for(auto role:Rules::Roles())assert(saved.Get(role)=="random");
|
||||||
}
|
}
|
||||||
saved.Set("enemy","us");saved.Set("independent","fia");
|
saved.Set("enemy","us");saved.Set("independent","fia");
|
||||||
for(int n=0;n<100;n++){
|
for(int n=0;n<100;n++){
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Execute finite site-roster and committed-location spacing policy bodies."""
|
||||||
|
import re
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
core=ROOT/'addons/core/Scripts/Game/ERR/00_Core'
|
||||||
|
roster=body(core/'ERR_SiteSquads.c','static array<ResourceName> Roster(')
|
||||||
|
roster=roster.replace('static array<ResourceName>', 'Array<Text>').replace('array<ResourceName> source','Array<Text>* source').replace('array<ResourceName>','Array<Text>').replace('ResourceName member','Text member').replace('source.', 'source->').replace('source[index % source->Count()]','(*source)[index % source->Count()]').replace('foreach (','for (').replace('member : source','member : *source')
|
||||||
|
spacing=body(core/'ERR_GeneratedSites.c','protected bool Separated(').replace('protected ', '').replace('ERR_SiteDefinition candidate','Site* candidate').replace('ERR_SiteDefinition other','Site* other').replace('vector position','Vec position').replace('foreach (','for (')
|
||||||
|
for name in ('candidate','other','m_Definition'):
|
||||||
|
spacing=spacing.replace(name+'.',name+'->')
|
||||||
|
spacing=spacing.replace('ERR_LocationKind.','Kind::').replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_GameMode.Get().EscapeRadius()','20000')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <cmath>
|
||||||
|
struct Text:std::string {using std::string::string;bool IsEmpty(){return empty();}};
|
||||||
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Insert(T x){this->push_back(x);}};
|
||||||
|
'''+roster+r'''
|
||||||
|
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
||||||
|
struct Vec{float x,z;static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||||
|
struct Site{int Kind=Kind::HOSPITAL;bool RandomClear=true,PlacementResolved=false;Vec Position{0,0};};
|
||||||
|
struct Definition{Array<Site*>Sites;};
|
||||||
|
struct Generator{Definition* m_Definition;
|
||||||
|
'''+spacing+r'''
|
||||||
|
};
|
||||||
|
int main(){
|
||||||
|
Array<Text> source={"medic","rifleman","radioman"},empty,invalid={"rifleman",""};
|
||||||
|
assert(Roster(nullptr,4).IsEmpty()&&Roster(&empty,4).IsEmpty()&&Roster(&invalid,4).IsEmpty());
|
||||||
|
assert(Roster(&source,0).IsEmpty()&&Roster(&source,9).IsEmpty());
|
||||||
|
auto defenders=Roster(&source,4),mobile=Roster(&source,2);
|
||||||
|
assert(defenders.Count()==4&&mobile.Count()==2&&defenders[3]=="medic"&&mobile[1]=="rifleman"&&source.Count()==3);
|
||||||
|
Site a,b;Definition definition{{&a,&b}};Generator generator{&definition};
|
||||||
|
assert(generator.Separated(&a,{0,0},false)); // unresolved seeds do not reserve the map
|
||||||
|
b.PlacementResolved=true;assert(!generator.Separated(&a,{599,0},false));assert(generator.Separated(&a,{600,0},false));
|
||||||
|
b.Kind=Kind::PRISON;assert(!generator.Separated(&a,{599,0},true)); // fallback must pass the same separation check
|
||||||
|
a.Kind=Kind::RADIO;assert(!generator.Separated(&a,{999,0},false));assert(generator.Separated(&a,{1000,0},false));
|
||||||
|
a.Kind=Kind::MORTAR;b.Kind=Kind::MORTAR;assert(!generator.Separated(&a,{999,0},false));assert(generator.Separated(&a,{1000,0},false));
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: exact finite site squads, invalid roster/count rejection, committed-site spacing and safe fallback boundary')
|
||||||
|
|
||||||
|
courier=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_MortarLogistics.c','static ERR_SiteRuntime CourierSource(')
|
||||||
|
courier=courier.replace('static ERR_SiteRuntime', 'Site*').replace('ERR_SiteRuntime site','Site* site').replace('site.', 'site->').replace('null','nullptr')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
struct Site {Site* Responders=nullptr;bool PatrolCycle=false;};
|
||||||
|
'''+courier+r'''
|
||||||
|
int main(){
|
||||||
|
Site fixed,mobile,legacy;fixed.Responders=&mobile;legacy.PatrolCycle=true;
|
||||||
|
assert(CourierSource(nullptr)==nullptr&&CourierSource(&fixed)==&mobile);
|
||||||
|
fixed.Responders=nullptr;assert(CourierSource(&fixed)==nullptr); // never borrow permanent guards
|
||||||
|
assert(CourierSource(&legacy)==&legacy); // retain explicit old roaming patrol ownership
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: mortar couriers use the mobile site team and refuse permanent-only garrisons')
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""Execute production gates behind the RKS2 delayed-start and hunter failures."""
|
||||||
|
import re
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
r = ROOT/'addons/core/Scripts/Game/ERR/10_AI'
|
||||||
|
# Use the actual stateful deadline block, including first-poll initialization.
|
||||||
|
poll = body(r/'ERR_MortarReadiness.c', 'ERR_MortarSupplyState Poll(')
|
||||||
|
clock = poll[poll.index(' if (m_StartedAt < 0)'):poll.index(' array<AIAgent> agents')]
|
||||||
|
admission = body(r/'ERR_HQ.c', 'protected bool ConvoyAdmissionReady(').replace('protected ', '')
|
||||||
|
admission = admission.replace('array<ref ERR_MortarBattery> batteries = {};', 'Array<ERR_MortarBattery*> batteries;')
|
||||||
|
admission = admission.replace('foreach (', 'for (').replace('ERR_HQUnit unit', 'ERR_HQUnit* unit').replace('ERR_MortarBattery support', 'ERR_MortarBattery* support').replace('ERR_MortarBattery battery', 'ERR_MortarBattery* battery')
|
||||||
|
for name in ('m_Mode', 'm_Mortars', 'unit', 'support', 'battery'):
|
||||||
|
admission = admission.replace(name+'.', name+'->')
|
||||||
|
admission = admission.replace('Forces().', 'Forces()->')
|
||||||
|
crew = body(r/'ERR_HunterService.c', 'override bool CrewValid(').replace('override ', '').replace('ERR_GameMode mode','Mode* mode')
|
||||||
|
crew = crew.replace('ERR_Actors.', 'Actors::').replace('mode.', 'mode->').replace('Group.', 'Group->').replace('Seats[0].','Seats[0]->')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
struct Text { bool empty=true; bool IsEmpty(){return empty;} };
|
||||||
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}};
|
||||||
|
struct GroupType {int faction=1;int GetFaction(){return faction;}};
|
||||||
|
struct Actor {bool alive=true,fit=true,player=false;GroupType* group=nullptr;};
|
||||||
|
struct Seat {Actor* occupant=nullptr;Actor* GetOccupant(){return occupant;}};
|
||||||
|
namespace Actors {bool Alive(Actor* x){return x&&x->alive;}bool CanFight(Actor* x){return x&&x->alive&&x->fit;}bool Player(Actor*x){return x&&x->player;}GroupType* Group(Actor*x){return x?x->group:nullptr;}}
|
||||||
|
struct Clock {float m_StartedAt=-1; bool Poll(float now){
|
||||||
|
'''+clock+r'''
|
||||||
|
return expired;
|
||||||
|
}};
|
||||||
|
struct Ledger {std::vector<GroupType*> registered;bool FindGroup(GroupType*g){return std::find(registered.begin(),registered.end(),g)!=registered.end();}};
|
||||||
|
struct Mode {Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
|
||||||
|
struct ERR_HQUnit {GroupType* Group=nullptr;};
|
||||||
|
struct ERR_MortarBattery {GroupType* group=nullptr;bool stopped=false,StartupActivated=false;Text SpawnFailure;bool Stopped(){return stopped;}};
|
||||||
|
struct Mortars {Array<ERR_MortarBattery*> all;ERR_MortarBattery* FindGroup(GroupType*g){for(auto*b:all)if(b->group==g)return b;return nullptr;}void Batteries(Array<ERR_MortarBattery*>&out){out=all;}};
|
||||||
|
struct HQ {Array<ERR_HQUnit*> m_Units;Mode* m_Mode;Mortars* m_Mortars;
|
||||||
|
'''+admission+r'''
|
||||||
|
};
|
||||||
|
struct Hunter {GroupType* Group;Actor* Entity;Actor* Driver;Array<Actor*> Members;Array<Seat*> Seats;
|
||||||
|
'''+crew+r'''
|
||||||
|
};
|
||||||
|
int main(){
|
||||||
|
Clock c;assert(!c.Poll(150000));assert(!c.Poll(239999));assert(c.Poll(240000)); // 150 seconds in prison does not spend the initialization budget
|
||||||
|
Clock early;assert(!early.Poll(1000));assert(!early.Poll(90999));assert(early.Poll(91000));assert(early.Poll(999)); // finite deadline and clock reversal remain enforced
|
||||||
|
Mode mode;GroupType ordinary,mortar;ERR_HQUnit u{&ordinary},m{&mortar};ERR_MortarBattery battery{&mortar};Mortars support;support.all={&battery};HQ hq{{&u,&m},&mode,&support};
|
||||||
|
mode.ledger.registered={&ordinary};assert(!hq.ConvoyAdmissionReady()); // unfinished mortar waits
|
||||||
|
battery.SpawnFailure.empty=false;assert(hq.ConvoyAdmissionReady()); // failed optional mortar cannot block convoy admission
|
||||||
|
battery.SpawnFailure.empty=true;battery.stopped=true;assert(hq.ConvoyAdmissionReady());
|
||||||
|
battery.stopped=false;battery.StartupActivated=true;assert(!hq.ConvoyAdmissionReady()); // success still requires ledger registration
|
||||||
|
mode.ledger.registered.push_back(&mortar);assert(hq.ConvoyAdmissionReady());
|
||||||
|
mode.ledger.registered={&mortar};battery.SpawnFailure.empty=false;assert(!hq.ConvoyAdmissionReady()); // unrelated pending group is not bypassed
|
||||||
|
Actor vehicle,driver,gunner;driver.group=&ordinary;gunner.group=&ordinary;Seat seat{&driver};Hunter hunter{&ordinary,&vehicle,&driver,{&driver,&gunner},{&seat}};
|
||||||
|
assert(hunter.CrewValid(&mode));gunner.alive=false;assert(hunter.CrewValid(&mode)); // loss of gunner does not discard a working driver
|
||||||
|
driver.fit=false;assert(!hunter.CrewValid(&mode));driver.fit=true;assert(hunter.CrewValid(&mode)); // temporary incapacitation can resume
|
||||||
|
seat.occupant=nullptr;assert(!hunter.CrewValid(&mode));seat.occupant=&driver;assert(hunter.CrewValid(&mode));
|
||||||
|
driver.player=true;assert(!hunter.CrewValid(&mode));driver.player=false;vehicle.alive=false;assert(!hunter.CrewValid(&mode));
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: real-session mortar deadline, failed-support convoy admission and hunter survivor/recovery gates')
|
||||||
|
|
||||||
|
# Execute the real retry method with a newer order still pending: the physical
|
||||||
|
# waypoint, not the unsent destination, must be recreated. Exercise retry limits
|
||||||
|
# and combat suppression too.
|
||||||
|
movement = body(r/'ERR_HunterService.c', 'protected void ObserveMovement(')
|
||||||
|
movement = movement.replace('protected ', '').replace('ERR_HunterVehicle unit', 'MovingHunter* unit')
|
||||||
|
movement = movement.replace('SCR_AIGroupUtilityComponent utility', 'Utility* utility').replace('vector ', 'Vec ').replace('vector.DistanceXZ', 'Vec::DistanceXZ')
|
||||||
|
movement = movement.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination))', 'Spawn(retryDestination)')
|
||||||
|
for name in ('unit', 'utility'):
|
||||||
|
movement = movement.replace(name+'.', name+'->')
|
||||||
|
for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'):
|
||||||
|
movement = movement.replace(name+'.', name+'->')
|
||||||
|
movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
|
||||||
|
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event')
|
||||||
|
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
struct Vec {float x=0,z=0;float Length(){return 0;}static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
|
||||||
|
struct Targets {bool empty=true;bool IsEmpty(){return empty;}};
|
||||||
|
struct Perception {Targets m_aTargets;};
|
||||||
|
struct Utility {Perception* m_Perception;};
|
||||||
|
struct Physics {Vec GetVelocity(){return {};}};
|
||||||
|
struct Vehicle {Vec pos;Physics physics;Vec GetOrigin(){return pos;}Physics* GetPhysics(){return &physics;}};
|
||||||
|
struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}void AddWaypoint(void*){}};
|
||||||
|
struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}};
|
||||||
|
struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}};
|
||||||
|
Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;}
|
||||||
|
template<class... Args> std::string Format(Args...){return "log";}
|
||||||
|
void Event(std::string,std::string){}
|
||||||
|
'''+movement+r'''
|
||||||
|
int main(){
|
||||||
|
Perception perception;Group group{{&perception}};Vehicle vehicle;Waypoint delivered{{100,200}};
|
||||||
|
MovingHunter hunter{&vehicle,&group,&delivered,{900,900}};
|
||||||
|
ObserveMovement(&hunter,60000);assert(hunter.Waypoint==&delivered&&hunter.Repaths==0);
|
||||||
|
ObserveMovement(&hunter,60001);assert(hunter.Repaths==1&&hunter.Waypoint->pos.x==100&&hunter.Waypoint->pos.z==200);assert(hunter.Destination.x==900);
|
||||||
|
ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3);
|
||||||
|
ObserveMovement(&hunter,240001);assert(hunter.Repaths==3);
|
||||||
|
vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0);
|
||||||
|
perception.m_aTargets.empty=false;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
|
||||||
|
perception.m_aTargets.empty=true;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
|
||||||
|
ObserveMovement(&hunter,460000);assert(hunter.Repaths==1);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: native-route retry preserves delivered target, limits attempts and suspends during combat')
|
||||||
@@ -6,7 +6,11 @@ for old,new in [
|
|||||||
('GetGame().','GetGame()->'),
|
('GetGame().','GetGame()->'),
|
||||||
('ChimeraAIWorld world', 'ChimeraAIWorld* world'),
|
('ChimeraAIWorld world', 'ChimeraAIWorld* world'),
|
||||||
('ChimeraAIWorld.Cast', 'ChimeraAIWorld::Cast'),
|
('ChimeraAIWorld.Cast', 'ChimeraAIWorld::Cast'),
|
||||||
('foreach (ERR_SiteRuntime site : m_Sites)', 'for (ERR_SiteRuntime* site : m_Sites)'),
|
('array<ERR_SiteRuntime> pendingGroups = {};', 'Array<ERR_SiteRuntime*> pendingGroups;'),
|
||||||
|
('foreach (ERR_SiteRuntime owner : m_Sites)', 'for (ERR_SiteRuntime* owner : m_Sites)'),
|
||||||
|
('foreach (ERR_SiteRuntime site : pendingGroups)', 'for (ERR_SiteRuntime* site : pendingGroups)'),
|
||||||
|
('pendingGroups.Insert', 'pendingGroups.push_back'),
|
||||||
|
('owner.Responders', 'owner->Responders'),
|
||||||
('site.Group', 'site->Group'),
|
('site.Group', 'site->Group'),
|
||||||
('site.InitialGuardCount', 'site->InitialGuardCount'),
|
('site.InitialGuardCount', 'site->InitialGuardCount'),
|
||||||
('site->Group.', 'site->Group->'),
|
('site->Group.', 'site->Group->'),
|
||||||
@@ -22,7 +26,7 @@ run('''#include <cassert>
|
|||||||
template<typename T> using Array=std::vector<T>;
|
template<typename T> using Array=std::vector<T>;
|
||||||
enum class ERR_PrepareResult {FAILED=-1,PENDING,READY};
|
enum class ERR_PrepareResult {FAILED=-1,PENDING,READY};
|
||||||
struct Group {bool immediate=true; std::shared_ptr<Array<std::string>> m_aUnitPrefabSlots; int physical=0; void SetSpawnImmediately(bool b){immediate=b;} };
|
struct Group {bool immediate=true; std::shared_ptr<Array<std::string>> m_aUnitPrefabSlots; int physical=0; void SetSpawnImmediately(bool b){immediate=b;} };
|
||||||
struct ERR_SiteRuntime {struct Group* Group; int InitialGuardCount;};
|
struct ERR_SiteRuntime {struct Group* Group; int InitialGuardCount;ERR_SiteRuntime* Responders=nullptr;};
|
||||||
struct ChimeraAIWorld {
|
struct ChimeraAIWorld {
|
||||||
Array<Group*> purged;
|
Array<Group*> purged;
|
||||||
static ChimeraAIWorld* Cast(ChimeraAIWorld* p){return p;}
|
static ChimeraAIWorld* Cast(ChimeraAIWorld* p){return p;}
|
||||||
@@ -47,19 +51,21 @@ struct Owner {
|
|||||||
int main(){
|
int main(){
|
||||||
ChimeraAIWorld world; Game instance{&world}; game=&instance;
|
ChimeraAIWorld world; Game instance{&world}; game=&instance;
|
||||||
auto authored=std::make_shared<Array<std::string>>(6,"soldier");
|
auto authored=std::make_shared<Array<std::string>>(6,"soldier");
|
||||||
Group empty{true,authored,0}, partial{true,authored,2}, control{true,authored,6}, unrequested{true,authored,0};
|
Group empty{true,authored,0}, partial{true,authored,2}, control{true,authored,6}, unrequested{true,authored,0}, mobile{true,authored,1};
|
||||||
ERR_SiteRuntime a{&empty,6},b{&partial,6},c{&control,6},ignored{&unrequested,0},missing{nullptr,6};
|
ERR_SiteRuntime a{&empty,6},b{&partial,6},c{&control,6},ignored{&unrequested,0},missing{nullptr,6};
|
||||||
|
ERR_SiteRuntime responders{&mobile,2};a.Responders=&responders;
|
||||||
Owner owner; owner.m_ExtractionPlacement=std::make_shared<Placement>(); owner.m_Sites={&a,&b,&ignored,&missing};
|
Owner owner; owner.m_ExtractionPlacement=std::make_shared<Placement>(); owner.m_Sites={&a,&b,&ignored,&missing};
|
||||||
owner.Stop(); owner.Stop();
|
owner.Stop(); owner.Stop();
|
||||||
assert(owner.m_Stopped && owner.routeStops==1 && world.purged.size()==2 && owner.m_ExtractionPlacement->stops==1);
|
assert(owner.m_Stopped && owner.routeStops==1 && world.purged.size()==3 && owner.m_ExtractionPlacement->stops==1);
|
||||||
assert(empty.m_aUnitPrefabSlots->empty() && partial.m_aUnitPrefabSlots->empty());
|
assert(empty.m_aUnitPrefabSlots->empty() && partial.m_aUnitPrefabSlots->empty());
|
||||||
assert(authored->size()==6 && control.m_aUnitPrefabSlots==authored && unrequested.m_aUnitPrefabSlots==authored);
|
assert(authored->size()==6 && control.m_aUnitPrefabSlots==authored && unrequested.m_aUnitPrefabSlots==authored);
|
||||||
|
assert(!mobile.immediate && mobile.m_aUnitPrefabSlots->empty() && mobile.physical==1);
|
||||||
assert(empty.physical==0 && partial.physical==2 && control.physical==6);
|
assert(empty.physical==0 && partial.physical==2 && control.physical==6);
|
||||||
assert(control.immediate && unrequested.immediate);
|
assert(control.immediate && unrequested.immediate);
|
||||||
Owner ready; ready.m_State=ERR_PrepareResult::READY; ready.m_Sites={&c}; ready.Stop();
|
Owner ready; ready.m_State=ERR_PrepareResult::READY; ready.m_Sites={&c}; ready.Stop();
|
||||||
assert(world.purged.size()==2 && control.immediate && control.m_aUnitPrefabSlots->size()==6);
|
assert(world.purged.size()==3 && control.immediate && control.m_aUnitPrefabSlots->size()==6);
|
||||||
game=nullptr; Owner deletedGame; deletedGame.m_Sites={&c}; deletedGame.Stop();
|
game=nullptr; Owner deletedGame; deletedGame.m_Sites={&c}; deletedGame.Stop();
|
||||||
assert(world.purged.size()==2);
|
assert(world.purged.size()==3);
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: pending-only cancellation retires copied instance intent, preserves physical/control actors, and stops once')
|
print('PASS: pending-only cancellation retires copied instance intent, preserves physical/control actors, and stops once')
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ GUIDE=Path(os.environ.get('ERR_GUIDE_SOURCE', ROOT/'addons/core/Scripts/Game/ERR
|
|||||||
OBSERVER=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
|
OBSERVER=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_Observer.c'
|
||||||
def cpp(text):
|
def cpp(text):
|
||||||
text=text.replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;')
|
text=text.replace('protected ', '').replace('override ', '').replace('super.OnControlledEntityChanged(from, to);', 'nativeChanges++;')
|
||||||
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','RplId'):
|
for name in ('ERR_GameMode','ERR_Phase','ERR_ObserverReason','ERR_Actors','ERR_ObjectiveText','SCR_HintManagerComponent','ERR_ObserverGuidance','ERR_ObserverRules','Resource','ETransformMode','SCR_ManualCamera','SCR_RespawnSystemComponent','RplId'):
|
||||||
text=text.replace(name+'.',name+'::')
|
text=text.replace(name+'.',name+'::')
|
||||||
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','CameraManager'):
|
for typename in ('ERR_GameMode','SCR_HintManagerComponent','Resource','SCR_RespawnSystemComponent','CameraManager'):
|
||||||
text=text.replace(typename+' ',typename+'* ')
|
text=text.replace(typename+' ',typename+'* ')
|
||||||
@@ -20,14 +20,17 @@ run(r'''#include <cassert>
|
|||||||
#include <string>
|
#include <string>
|
||||||
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
|
struct string:std::string {using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty()const{return empty();}};
|
||||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
|
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,RADIO_DEFENCE,EXTRACTION};}
|
||||||
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE};}
|
namespace ERR_ObserverReason {enum {NONE,SPENT_LIFE,LATE_EXTRACTION,SPAWN_UNAVAILABLE,UNCONSCIOUS};}
|
||||||
struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*;
|
struct Actor {bool alive=true;bool unconscious=false;} actor;using IEntity=Actor*;
|
||||||
struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}};
|
struct ERR_Actors {static bool Alive(Actor* a){return a&&a->alive;}static bool CanFight(Actor* a){return Alive(a)&&!a->unconscious;}};
|
||||||
struct ERR_GameMode {int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode;
|
struct ERR_GameMode {int phase=ERR_Phase::PRISON;bool active=true;string message="prison";static ERR_GameMode* current;static ERR_GameMode* Get(){return current;}int Phase(){return phase;}bool Active(){return active;}} mode;
|
||||||
ERR_GameMode* ERR_GameMode::current=&mode;
|
ERR_GameMode* ERR_GameMode::current=&mode;
|
||||||
struct ERR_ObjectiveText {static string Description(ERR_GameMode* m,bool){return m->message;}static string Title(int){return "objective";}};
|
struct ERR_ObjectiveText {static string Description(ERR_GameMode* m,bool){return m->message;}static string Title(int){return "objective";}};
|
||||||
struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int){attempts++;if(!accepts)return false;shown++;last=text;return true;}} hints;
|
struct SCR_HintManagerComponent {static SCR_HintManagerComponent* current;bool accepts=true;int attempts=0,shown=0;string last;static SCR_HintManagerComponent* GetInstance(){return current;}bool ShowCustom(string text,string,int){attempts++;if(!accepts)return false;shown++;last=text;return true;}} hints;
|
||||||
SCR_HintManagerComponent* SCR_HintManagerComponent::current=&hints;
|
SCR_HintManagerComponent* SCR_HintManagerComponent::current=&hints;
|
||||||
|
struct ERR_ObserverRules {
|
||||||
|
'''+cpp(body(OBSERVER,'static bool View('))+r'''
|
||||||
|
};
|
||||||
struct ERR_ObserverGuidance {
|
struct ERR_ObserverGuidance {
|
||||||
'''+guidance+r'''
|
'''+guidance+r'''
|
||||||
};
|
};
|
||||||
@@ -106,6 +109,12 @@ int main(){
|
|||||||
// A living body closes observation without attempting a spectator hint.
|
// A living body closes observation without attempting a spectator hint.
|
||||||
s.controlled=&actor;attempts=hints.attempts;s.ERR_UpdateObserverClient();assert(s.closes==1&&!s.m_ERRObserverReady&&hints.attempts==attempts);
|
s.controlled=&actor;attempts=hints.attempts;s.ERR_UpdateObserverClient();assert(s.closes==1&&!s.m_ERRObserverReady&&hints.attempts==attempts);
|
||||||
s.controlled=nullptr;s.ERR_UpdateObserverClient();assert(hints.shown==4&&s.m_ERRObserverReady);
|
s.controlled=nullptr;s.ERR_UpdateObserverClient();assert(hints.shown==4&&s.m_ERRObserverReady);
|
||||||
|
// Unconscious observation preserves the original body, delivers recovery wording,
|
||||||
|
// and closes as soon as the local body wakes even before server metadata clears.
|
||||||
|
reset();Controller casualty;actor.unconscious=true;casualty.m_ERRObserverReason=ERR_ObserverReason::UNCONSCIOUS;
|
||||||
|
casualty.ERR_UpdateObserverClient();assert(casualty.m_ERRObserverCamera==&camera&&casualty.controlled==&actor&&hints.last.find("You are unconscious")!=string::npos);
|
||||||
|
casualty.ERR_UpdateObserverClient();assert(hints.shown==1);
|
||||||
|
actor.unconscious=false;casualty.ERR_UpdateObserverClient();assert(!casualty.m_ERRObserverCamera&&casualty.closes==1&&hints.shown==1);
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):
|
for path,signature in ((GUIDE,'protected void ERR_UpdateGuide('),(OBSERVER,'protected void ERR_UpdateObserverClient(')):
|
||||||
|
|||||||
@@ -81,13 +81,13 @@ struct Sites{ERR_SiteRuntime* site=nullptr;int finds=0;string last;
|
|||||||
ERR_SiteRuntime* Find(string id){finds++;last=id;return site;}};
|
ERR_SiteRuntime* Find(string id){finds++;last=id;return site;}};
|
||||||
struct ERR_Settings{
|
struct ERR_Settings{
|
||||||
int automatic=1,stinger=15,convoy=3,writes=0;bool saveFails=false;
|
int automatic=1,stinger=15,convoy=3,writes=0;bool saveFails=false;
|
||||||
static Array<string> Keys(){return {}; }int Get(string key){if(key=="convoyCount")return convoy;return key=="stingerChanceOneIn"?stinger:automatic;}
|
static Array<string> Keys(){return {}; }int Get(string key){if(key=="convoyCount")return convoy;return key=="antiAirChancePercent"?stinger:automatic;}
|
||||||
'''+validator+r'''
|
'''+validator+r'''
|
||||||
static string SelectionText(int value){return value==-1?string("random"):string(std::to_string(value));}
|
static string SelectionText(int value){return value==-1?string("random"):string(std::to_string(value));}
|
||||||
bool Set(string key,int value){writes++;if(saveFails||(!Valid(key,value)&&!(key=="convoyCount"&&value==-1)))return false;
|
bool Set(string key,int value){writes++;if(saveFails||(!Valid(key,value)&&!(key=="convoyCount"&&value==-1)))return false;
|
||||||
if(key=="convoyCount"){convoy=value;return true;}
|
if(key=="convoyCount"){convoy=value;return true;}
|
||||||
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
|
if(key=="automaticConvoyEnabled"){automatic=value;return true;}
|
||||||
assert(key=="stingerChanceOneIn");stinger=value;return true;}
|
assert(key=="antiAirChancePercent");stinger=value;return true;}
|
||||||
};
|
};
|
||||||
struct ERR_GameMode{
|
struct ERR_GameMode{
|
||||||
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
|
inline static ERR_GameMode* current=nullptr;bool active=true,settingsReady=true;Ledger ledger;
|
||||||
@@ -180,9 +180,9 @@ int main(){
|
|||||||
assert(mode.settings.writes==1&&mode.settings.stinger==0&&pool.calls==0);
|
assert(mode.settings.writes==1&&mode.settings.stinger==0&&pool.calls==0);
|
||||||
assert(replies[0].find("have not rolled yet")!=string::npos&&replies[0].find("no reroll, removal or replenishment")!=string::npos);
|
assert(replies[0].find("have not rolled yet")!=string::npos&&replies[0].find("no reroll, removal or replenishment")!=string::npos);
|
||||||
admin.Execute(&mode,true,"hq stinger show",replies);
|
admin.Execute(&mode,true,"hq stinger show",replies);
|
||||||
assert(mode.settings.writes==1&&replies[0].find("stingerChanceOneIn=0")!=string::npos);
|
assert(mode.settings.writes==1&&replies[0].find("antiAirChancePercent=0")!=string::npos);
|
||||||
admin.Execute(&mode,true,"hq stinger help",replies);
|
admin.Execute(&mode,true,"hq stinger help",replies);
|
||||||
assert(mode.settings.writes==1&&replies[0].find("2..10000 one in N")!=string::npos);
|
assert(mode.settings.writes==1&&replies[0].find("0..100 percent")!=string::npos);
|
||||||
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 15",replies);
|
mode.settings.saveFails=true;admin.Execute(&mode,true,"hq stinger set 15",replies);
|
||||||
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
|
assert(mode.settings.stinger==0&&replies[0].find("previous setting retained")!=string::npos);mode.settings.saveFails=false;
|
||||||
int writes=mode.settings.writes;
|
int writes=mode.settings.writes;
|
||||||
|
|||||||
@@ -237,13 +237,13 @@ int main(){
|
|||||||
assert(!merged.IngestBody(&body,4002)&&merged.ConfirmedLosses()==4); // replay or different observer
|
assert(!merged.IngestBody(&body,4002)&&merged.ConfirmedLosses()==4); // replay or different observer
|
||||||
status.MessageId=3;status.ObservedAt=5000;assert(merged.Ingest(&status,5000)&&known->Closed);
|
status.MessageId=3;status.ObservedAt=5000;assert(merged.Ingest(&status,5000)&&known->Closed);
|
||||||
assert(!merged.RequestDue("patrol-1",999999));
|
assert(!merged.RequestDue("patrol-1",999999));
|
||||||
assert(merged.IdentifiedBodies()==4&&merged.Aggression()==1&&merged.HunterTarget()==1);
|
assert(merged.IdentifiedBodies()==4&&merged.Aggression()==1&&merged.HunterTarget()==4);
|
||||||
for(int bodies=0;bodies<=300;bodies++){
|
for(int bodies=0;bodies<=300;bodies++){
|
||||||
assert(ERR_HQWatchRules::Aggression(bodies)==std::min(30,bodies/3));
|
assert(ERR_HQWatchRules::Aggression(bodies)==std::min(30,bodies/3));
|
||||||
assert(ERR_HQWatchRules::Hunters(ERR_HQWatchRules::Aggression(bodies))==std::min(15,(bodies/3+1)/2));
|
assert(ERR_HQWatchRules::Hunters(ERR_HQWatchRules::Aggression(bodies))==3+(std::min(30,bodies/3)*12+29)/30);
|
||||||
}
|
}
|
||||||
assert(ERR_HQWatchRules::Aggression(-10)==0&&ERR_HQWatchRules::Hunters(-10)==0);
|
assert(ERR_HQWatchRules::Aggression(-10)==0&&ERR_HQWatchRules::Hunters(-10)==3);
|
||||||
merged.Clear();assert(merged.IdentifiedBodies()==0&&merged.HunterTarget()==0);
|
merged.Clear();assert(merged.IdentifiedBodies()==0&&merged.HunterTarget()==3);
|
||||||
// Exercise the actual HQ delivered-intake method and actual Report.Snapshot, including corpse deletion.
|
// Exercise the actual HQ delivered-intake method and actual Report.Snapshot, including corpse deletion.
|
||||||
HQDelivery intake;ERR_HQReport local;local.CasualtySourceId="deleted-during-uplink";
|
HQDelivery intake;ERR_HQReport local;local.CasualtySourceId="deleted-during-uplink";
|
||||||
local.CasualtyStrength=1;local.CasualtySlot=0;local.Target=&local;local.MessageId=501;
|
local.CasualtyStrength=1;local.CasualtySlot=0;local.Target=&local;local.MessageId=501;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ s=s.replace('override ','').replace('super.','Native::').replace('null','nullptr
|
|||||||
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
|
s=s.replace('ERR_GameMode mode','ERR_GameMode* mode').replace('ERR_ForceRecord record','ERR_ForceRecord* record')
|
||||||
s=s.replace('ERR_HQ hq = ERR_HQ.Cast(mode.Command())', 'ERR_HQ* hq = mode->Command()')
|
s=s.replace('ERR_HQ hq = ERR_HQ.Cast(mode.Command())', 'ERR_HQ* hq = mode->Command()')
|
||||||
s=s.replace('array<AIAgent> agents = {};','Array<AIAgent*> agents;').replace('foreach (AIAgent agent : agents)','for (AIAgent* agent : agents)')
|
s=s.replace('array<AIAgent> agents = {};','Array<AIAgent*> agents;').replace('foreach (AIAgent agent : agents)','for (AIAgent* agent : agents)')
|
||||||
for name in ('mode','record','Group','agent','hq'):s=s.replace(name+'.',name+'->')
|
for name in ('mode','record','Group','agent','hq','Recovery'):s=s.replace(name+'.',name+'->')
|
||||||
s=s.replace('Forces().','Forces()->').replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
|
s=s.replace('Forces().','Forces()->').replace('ERR_ConvoyRules.','ERR_ConvoyRules::')
|
||||||
objective=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c','static bool Objective(').replace('ERR_LocationKind.','ERR_LocationKind::')
|
objective=body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ObjectiveDefence.c','static bool Objective(').replace('ERR_LocationKind.','ERR_LocationKind::')
|
||||||
run(r'''
|
run(r'''
|
||||||
@@ -20,17 +20,19 @@ struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(GroupType*){retu
|
|||||||
struct ERR_HQ{int releases=0;void ReleaseRecovery(GroupType*){releases++;}};
|
struct ERR_HQ{int releases=0;void ReleaseRecovery(GroupType*){releases++;}};
|
||||||
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
|
struct ERR_GameMode{bool active=true;Ledger ledger;ERR_HQ hq;bool Active(){return active;}Ledger* Forces(){return &ledger;}ERR_HQ* Command(){return &hq;}};
|
||||||
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
|
namespace ERR_ConvoyRules{bool captured=false;bool Captured(void*,GroupType*){return captured;}}
|
||||||
struct Native{bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
|
struct RecoveryTask{int stops=0;void Stop(){stops++;}};
|
||||||
|
struct Native{RecoveryTask* Recovery=nullptr;bool Released=false;int delegated=0,clears=0;GroupType* Group=nullptr;void* Entity=nullptr;void* Downlink=nullptr;
|
||||||
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
|
void Release(ERR_GameMode*){delegated++;Released=true;}void ClearOrder(){clears++;}
|
||||||
};
|
};
|
||||||
struct Hunter:Native{
|
struct Hunter:Native{
|
||||||
'''+s+r'''
|
'''+s+r'''
|
||||||
};
|
};
|
||||||
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR};}
|
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,PATROL,DEPOT,OUTPOST,MORTAR,HOSPITAL};}
|
||||||
'''+objective+r'''
|
'''+objective+r'''
|
||||||
int main(){
|
int main(){
|
||||||
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;
|
ERR_GameMode mode;GroupType group;Hunter unit;unit.Group=&group;unit.Entity=&group;unit.Downlink=&mode;RecoveryTask recovery;unit.Recovery=&recovery;
|
||||||
unit.Release(&mode);assert(unit.Released&&!unit.Downlink&&unit.clears==1&&unit.delegated==0);
|
unit.Release(&mode);assert(unit.Released&&!unit.Downlink&&unit.clears==1&&unit.delegated==0);
|
||||||
|
assert(recovery.stops==1);
|
||||||
assert(!mode.ledger.record.Pinned&&mode.hq.releases==1&&group.lod==-1&&group.agent.writes==1);
|
assert(!mode.ledger.record.Pinned&&mode.hq.releases==1&&group.lod==-1&&group.agent.writes==1);
|
||||||
assert(unit.Entity==&group&&unit.Group==&group); // living and dead actors/vehicle are preserved
|
assert(unit.Entity==&group&&unit.Group==&group); // living and dead actors/vehicle are preserved
|
||||||
unit.Release(&mode);assert(unit.clears==1&&mode.hq.releases==1&&group.agent.writes==1);
|
unit.Release(&mode);assert(unit.clears==1&&mode.hq.releases==1&&group.agent.writes==1);
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Production opening geometry and first-contact HQ order selection."""
|
||||||
|
import re
|
||||||
|
from policy_harness import ROOT, body, run
|
||||||
|
rules_path=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterOpeningPlan.c'
|
||||||
|
rules='\n'.join(body(rules_path,x) for x in ('static bool Spread(', 'static bool FreshOrder(', 'static bool Select('))
|
||||||
|
rules=re.sub(r'\bvector (?=\w)', 'Vec ', rules).replace('vector.', 'Geometry::').replace('array<vector> candidates','Array<Vec> candidates').replace('array<int> selected','Array<int>& selected')
|
||||||
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
|
||||||
|
queue=body(p,'protected void QueueDestination(').replace('protected ','').replace('foreach (','for (')
|
||||||
|
queue=queue.replace('array<ref ERR_HQReport> reports = {};','Array<ERR_HQReport*> reports;')
|
||||||
|
for typ in ('ERR_HunterVehicle','ERR_HQ','ERR_HQReport','ERR_HQBelief','ERR_SiteRuntime'):
|
||||||
|
queue=re.sub(r'\b'+typ+r' (?=\w)', typ+'* ',queue)
|
||||||
|
for name in ('unit','hq','report','belief','best','m_Mode','site'):
|
||||||
|
queue=queue.replace(name+'.',name+'->')
|
||||||
|
queue=queue.replace('best->Confidence.ToString()', 'std::to_string(best->Confidence)')
|
||||||
|
queue=queue.replace('Memory().Get','Memory()->Get').replace('Sites().','Sites()->').replace('Definition.','Definition->')
|
||||||
|
queue=queue.replace('Waypoint.IsWithinCompletionRadius','Waypoint->IsWithinCompletionRadius').replace('Entity.GetOrigin','Entity->GetOrigin').replace('Group.GetFaction','Group->GetFaction')
|
||||||
|
queue=queue.replace('ERR_HQBelief* best;', 'ERR_HQBelief* best=nullptr;').replace('bool freshContact;', 'bool freshContact=false;').replace('bool found;', 'bool found=false;')
|
||||||
|
queue=queue.replace('vector destination, angles;', 'Vec destination, angles;')
|
||||||
|
for cls in ('ERR_HunterOpeningRules','ERR_SupplyConvoySystem','ERR_ObjectiveDefence','Math','vector'):
|
||||||
|
queue=queue.replace(cls+'.', ('Geometry' if cls=='vector' else cls)+'::')
|
||||||
|
queue=queue.replace('null','nullptr').replace('nullptrptr','nullptr')
|
||||||
|
run(r'''
|
||||||
|
#include <cassert>
|
||||||
|
#include <cmath>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
using string=std::string;
|
||||||
|
struct Vec{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}string ToString(){return "position";}};
|
||||||
|
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
|
||||||
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
|
||||||
|
struct ERR_HunterOpeningRules{
|
||||||
|
'''+rules+r'''
|
||||||
|
};
|
||||||
|
struct Group{int GetFaction(){return 1;}};
|
||||||
|
struct Entity{Vec GetOrigin(){return {};}};
|
||||||
|
struct Waypoint{bool complete=false;bool IsWithinCompletionRadius(Group*){return complete;}};
|
||||||
|
struct Route{};
|
||||||
|
struct ERR_HunterVehicle{float NextOrder=99999,LastContact=-1,NextContactOrder=0;Route* Downlink=nullptr;int SourceMessage=0;Vec Destination;string Source="hunter";struct Group* Group=nullptr;struct Entity* Entity=nullptr;struct Waypoint* Waypoint=nullptr;};
|
||||||
|
struct ERR_HQReport{int Target=1;};
|
||||||
|
struct ERR_HQBelief{bool actionable=true;float ObservedAt=10,Confidence=1;int MessageId=7;Vec PredictedPosition={1000,0,2000};bool Actionable(){return actionable;}};
|
||||||
|
struct Memory{ERR_HQBelief belief;ERR_HQBelief* Get(int){return &belief;}};
|
||||||
|
struct ERR_HQ{bool contact=false,connected=true;int requests=0;struct Memory memory;Route route;void SupportContacts(Array<ERR_HQReport*>& out,float){static ERR_HQReport report;if(contact)out.Insert(&report);}struct Memory* Memory(){return &memory;}Route* Downlink(Group*,string,string,int){requests++;return connected?&route:nullptr;}};
|
||||||
|
struct Definition{int Kind=1;Vec Position;};struct ERR_SiteRuntime{struct Definition* Definition=nullptr;struct Group* Group=nullptr;};
|
||||||
|
struct Sites{int Count(){return 0;}ERR_SiteRuntime* At(int){return nullptr;}};
|
||||||
|
struct Mode{struct Sites sites;struct Sites* Sites(){return &sites;}int EnemyFaction(){return 1;}};
|
||||||
|
namespace Math{int RandomInt(int,int){return 0;}}
|
||||||
|
namespace ERR_ObjectiveDefence{bool Objective(int){return true;}}
|
||||||
|
namespace ERR_SupplyConvoySystem{bool available=true;int calls=0;bool ApproachDirected(Vec p,Vec& dst,Vec& angles,float radius=500){calls++;dst=p;return available;}}
|
||||||
|
struct Service{Mode mode;Mode* m_Mode=&mode;
|
||||||
|
'''+queue+r'''
|
||||||
|
};
|
||||||
|
Vec polar(float degrees,float radius=1000){return {std::sin(degrees*3.14159265f/180)*radius,0,std::cos(degrees*3.14159265f/180)*radius};}
|
||||||
|
int main(){
|
||||||
|
Array<int> chosen;Vec center;
|
||||||
|
assert(ERR_HunterOpeningRules::Select(center,{polar(0),polar(120),polar(240)},chosen)&&chosen.Count()==3);
|
||||||
|
assert(!ERR_HunterOpeningRules::Select(center,{polar(0),polar(5),polar(10)},chosen)&&chosen.empty());
|
||||||
|
assert(!ERR_HunterOpeningRules::Spread(center,polar(359),polar(1)));
|
||||||
|
assert(!ERR_HunterOpeningRules::Spread(center,polar(0,599),polar(180)));
|
||||||
|
Array<Vec> tooMany(65,polar(0));assert(!ERR_HunterOpeningRules::Select(center,tooMany,chosen));
|
||||||
|
assert(!ERR_HunterOpeningRules::FreshOrder(9,10,100,0,false));
|
||||||
|
assert(!ERR_HunterOpeningRules::FreshOrder(10,10,100,0,false));
|
||||||
|
assert(!ERR_HunterOpeningRules::FreshOrder(11,10,100,101,false));
|
||||||
|
assert(!ERR_HunterOpeningRules::FreshOrder(11,10,100,0,true));
|
||||||
|
Service service;ERR_HQ hq;ERR_HunterVehicle unit;Route patrol;unit.Downlink=&patrol;
|
||||||
|
service.QueueDestination(&unit,&hq,100);assert(unit.Downlink==&patrol&&hq.requests==0); // no magical position
|
||||||
|
hq.contact=true;service.QueueDestination(&unit,&hq,100);
|
||||||
|
assert(unit.Downlink==&hq.route&&hq.requests==1&&unit.SourceMessage==7&&unit.LastContact==10);
|
||||||
|
assert(unit.Destination.x==1000&&unit.Destination.z==2000); // delivered belief only, before patrol cooldown
|
||||||
|
hq.memory.belief.ObservedAt=11;service.QueueDestination(&unit,&hq,6000);
|
||||||
|
assert(hq.requests==1); // in-flight interception isn't starved by newer observations
|
||||||
|
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,6000);assert(hq.requests==2&&unit.LastContact==11);
|
||||||
|
unit.Downlink=nullptr;service.QueueDestination(&unit,&hq,7000);assert(hq.requests==2); // no repeated order for same report
|
||||||
|
ERR_HunterVehicle disconnected;hq.connected=false;service.QueueDestination(&disconnected,&hq,10000);
|
||||||
|
assert(!disconnected.Downlink&&disconnected.LastContact==-1);
|
||||||
|
int requests=hq.requests;service.QueueDestination(&disconnected,&hq,11000);assert(hq.requests==requests);
|
||||||
|
hq.connected=true;service.QueueDestination(&disconnected,&hq,15000);assert(disconnected.Downlink&&disconnected.LastContact==11);
|
||||||
|
}
|
||||||
|
''')
|
||||||
|
print('PASS: real opening road geometry enforces wide azimuths; fresh delivered contact preempts patrol cooldown, respects radio failures and does not starve in-flight interception')
|
||||||
@@ -4,7 +4,7 @@ from policy_harness import ROOT, body, run
|
|||||||
MODE = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
MODE = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GameMode.c'
|
||||||
SPAWN = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnLogic.c'
|
SPAWN = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SpawnLogic.c'
|
||||||
def cpp(s):
|
def cpp(s):
|
||||||
s=s.replace('protected ', '').replace('Replication.IsServer()', 'server').replace('Replication.BumpMe()', '(void)0')
|
s=s.replace('ERR_HQ.Event','ERR_HQ::Event').replace('protected ', '').replace('Replication.IsServer()', 'server').replace('Replication.BumpMe()', '(void)0')
|
||||||
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('connected.IsEmpty()', 'connected.empty()')
|
s=s.replace('array<int> connected = {};','std::vector<int> connected;').replace('connected.IsEmpty()', 'connected.empty()')
|
||||||
s=s.replace('foreach (int id : connected)', 'for (int id : connected)')
|
s=s.replace('foreach (int id : connected)', 'for (int id : connected)')
|
||||||
s=s.replace('ERR_PlayerLife life', 'Life* life').replace('SCR_PlayerController controller', 'Controller* controller').replace('PlayerController controller', 'Controller* controller')
|
s=s.replace('ERR_PlayerLife life', 'Life* life').replace('SCR_PlayerController controller', 'Controller* controller').replace('PlayerController controller', 'Controller* controller')
|
||||||
@@ -25,7 +25,8 @@ struct Roster {std::map<int,Life> lives;bool survivor=false;Life* Get(int id){au
|
|||||||
struct Players {std::map<int,Controller*> connected;void GetPlayers(std::vector<int>& v){for(auto p:connected)v.push_back(p.first);}int GetPlayerCount(){return connected.size();}Controller* GetPlayerController(int id){auto i=connected.find(id);return i==connected.end()?nullptr:i->second;}};
|
struct Players {std::map<int,Controller*> connected;void GetPlayers(std::vector<int>& v){for(auto p:connected)v.push_back(p.first);}int GetPlayerCount(){return connected.size();}Controller* GetPlayerController(int id){auto i=connected.find(id);return i==connected.end()?nullptr:i->second;}};
|
||||||
struct Game {Players players;Players* GetPlayerManager(){return &players;}} game;Game* GetGame(){return &game;}
|
struct Game {Players players;Players* GetPlayerManager(){return &players;}} game;Game* GetGame(){return &game;}
|
||||||
struct World {float now=0;float GetWorldTime(){return now;}} world;
|
struct World {float now=0;float GetWorldTime(){return now;}} world;
|
||||||
struct Service {void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
|
struct ERR_HQ {static void Event(std::string,std::string){}};
|
||||||
|
struct Service {bool defeat=false;bool DefeatDue(float){return defeat;}void Tick(float=0){}void Maintain(float){}void Invoke(float){}};
|
||||||
struct Exhausted {std::map<int,Controller*> values;void Remove(int id){values.erase(id);}void Set(int id,Controller* c){values[id]=c;}bool Contains(int id){return values.count(id);}Controller* Get(int id){return values.at(id);}};
|
struct Exhausted {std::map<int,Controller*> values;void Remove(int id){values.erase(id);}void Set(int id,Controller* c){values[id]=c;}bool Contains(int id){return values.count(id);}Controller* Get(int id){return values.at(id);}};
|
||||||
struct Mode {
|
struct Mode {
|
||||||
void TickForces(float){}
|
void TickForces(float){}
|
||||||
@@ -37,6 +38,8 @@ struct Mode {
|
|||||||
};
|
};
|
||||||
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
|
struct Fixture {Mode m;Controller first,second;Fixture(){server=true;world.now=0;game.players.connected={{1,&first}};m.roster.lives[1]=Life();}void tick(float t){world.now=t;m.ServerTick();}void exhaust(){m.InitialAdmissionExhausted(1,&first);}};
|
||||||
int main(){
|
int main(){
|
||||||
|
{Fixture f;f.m.roster.survivor=true;f.m.service.defeat=true;f.tick(31000);
|
||||||
|
assert(!f.m.Active() && f.m.m_EndReason=="All prisoners are unconscious and no treatment or recovery is progressing.");}
|
||||||
// Sole exhausted first admission reaches existing grace, without a life/body.
|
// Sole exhausted first admission reaches existing grace, without a life/body.
|
||||||
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
|
{Fixture f;f.exhaust();f.tick(0);f.tick(29999);assert(f.m.Active());f.tick(30000);
|
||||||
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
|
assert(f.m.m_Phase==5&&!f.m.m_StartedPlayers&&!f.m.roster.lives[1].UsedLife);
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct SCR_Global {static bool edit;static bool IsEditMode(){return edit;}};bool
|
|||||||
struct SCR_ChatComponent {static std::vector<string> messages;static void RadioProtocolMessage(string text){messages.push_back(text);}};std::vector<string> SCR_ChatComponent::messages;
|
struct SCR_ChatComponent {static std::vector<string> messages;static void RadioProtocolMessage(string text){messages.push_back(text);}};std::vector<string> SCR_ChatComponent::messages;
|
||||||
struct Math {static float Max(float a,float b){return a>b?a:b;}};
|
struct Math {static float Max(float a,float b){return a>b?a:b;}};
|
||||||
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,EXTRACTION};}
|
namespace ERR_Phase {enum {PREPARING,PRISON,RADIO_SEARCH,EXTRACTION};}
|
||||||
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR};}
|
namespace ERR_LocationKind {enum {PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
||||||
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}string Id(){return "outpost-id";}};
|
struct ERR_LocationComponent {int kind=ERR_LocationKind::OUTPOST,knowledge=1;string label="Hidden outpost";int Kind(){return kind;}int Knowledge(){return knowledge;}string Label(){return label;}string Id(){return "outpost-id";}};
|
||||||
template<class K,class V> struct Map {std::map<K,V> values;bool Find(K k,V& v){auto i=values.find(k);if(i==values.end())return false;v=i->second;return true;}bool Contains(K k){return values.count(k);}int Count(){return values.size();}bool IsEmpty(){return values.empty();}void Set(K k,V v){values[k]=v;}void Clear(){values.clear();}};
|
template<class K,class V> struct Map {std::map<K,V> values;bool Find(K k,V& v){auto i=values.find(k);if(i==values.end())return false;v=i->second;return true;}bool Contains(K k){return values.count(k);}int Count(){return values.size();}bool IsEmpty(){return values.empty();}void Set(K k,V v){values[k]=v;}void Clear(){values.clear();}};
|
||||||
struct World {float time=0;float GetWorldTime(){return time;}} world;
|
struct World {float time=0;float GetWorldTime(){return time;}} world;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def adapt(s):
|
|||||||
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
s=re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
|
||||||
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState','ERR_LocationKind','ERR_ObjectiveDefence'):
|
for n in ('Replication','ERR_Actors','Math','ERR_Resources','ERR_DiscoveryService','SCR_InventoryStorageManagerComponent','SCR_AIGroupUtilityComponent','ERR_MortarSupplyState','ERR_LocationKind','ERR_ObjectiveDefence'):
|
||||||
s=s.replace(n+'.',n+'::')
|
s=s.replace(n+'.',n+'::')
|
||||||
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars','m_Scouts','m_Recon','m_Airfield'):
|
for n in ('group','member','actor','request','inventory','known','unit','m_Mode','m_Battery','mode','m_Reroll','home','sites','record','m_Mortars','m_Scouts','m_Recon','m_Airfield','m_Hunters','m_Towns'):
|
||||||
s=s.replace(n+'.',n+'->')
|
s=s.replace(n+'.',n+'->')
|
||||||
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
|
s=s.replace('Actor.FindComponent(', 'Actor->FindComponent(').replace('SCR_InventoryStorageManagerComponent inventory', 'SCR_InventoryStorageManagerComponent* inventory').replace('SCR_AIGroupUtilityComponent utility', 'SCR_AIGroupUtilityComponent* utility')
|
||||||
s=s.replace('home->Definition.', 'home->Definition->').replace('Patrol.IsEmpty()', 'Patrol->IsEmpty()').replace('unit->StatusSampler.Bind(', 'unit->StatusSampler.Bind(')
|
s=s.replace('home->Definition.', 'home->Definition->').replace('Patrol.IsEmpty()', 'Patrol->IsEmpty()').replace('unit->StatusSampler.Bind(', 'unit->StatusSampler.Bind(')
|
||||||
@@ -69,7 +69,7 @@ struct Settings{int chance=100;int Runtime(const char*){return chance;}};
|
|||||||
namespace ERR_LocationKind {enum {RADIO=1};}
|
namespace ERR_LocationKind {enum {RADIO=1};}
|
||||||
namespace ERR_ObjectiveDefence {bool Objective(int kind){return kind==1;}}
|
namespace ERR_ObjectiveDefence {bool Objective(int kind){return kind==1;}}
|
||||||
struct Definition{int Kind=3;string Id="site-a";Array<int>* Patrol=nullptr;};
|
struct Definition{int Kind=3;string Id="site-a";Array<int>* Patrol=nullptr;};
|
||||||
struct Site{struct Definition data;struct Definition* Definition=&data;};using ERR_SiteRuntime=Site*;
|
struct Site{SCR_AIGroup Group=nullptr;struct Definition data;struct Definition* Definition=&data;};using ERR_SiteRuntime=Site*;
|
||||||
struct Sites{Site home;Site* FindGroup(SCR_AIGroup){return &home;}};using ERR_SiteService=Sites*;
|
struct Sites{Site home;Site* FindGroup(SCR_AIGroup){return &home;}};using ERR_SiteService=Sites*;
|
||||||
struct ForceRecord{string SourceId="site-a";int InitialStrength=4;bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
|
struct ForceRecord{string SourceId="site-a";int InitialStrength=4;bool Physical=true,Eliminated=false;};using ERR_ForceRecord=ForceRecord*;
|
||||||
struct Forces{ForceRecord record;bool available=false;ForceRecord* FindGroup(SCR_AIGroup){return available?&record:nullptr;}};
|
struct Forces{ForceRecord record;bool available=false;ForceRecord* FindGroup(SCR_AIGroup){return available?&record:nullptr;}};
|
||||||
@@ -91,7 +91,7 @@ struct Recon{SCR_AIGroup owned=nullptr;bool Owns(SCR_AIGroup group){return group
|
|||||||
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
|
struct BodyService:GroupService{void RegisterGroup(SCR_AIGroup g,Sampler& s){assert(s.Bound()&&s.group==g);GroupService::RegisterGroup(g);}};
|
||||||
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
|
struct Reporting{int initializations=0;void Initialize(int id,float now){assert(id>0&&now==10);initializations++;}};
|
||||||
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool ReservedForSupport=false;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
|
struct Unit{Reporting AmmoReporting;string HomeId,ForceSourceId;bool ReservedForSupport=false;bool HomeGuard=false;float NextStatus=0;Sampler StatusSampler;int Id=0;SCR_AIGroup Group=nullptr;struct Utility* Utility=nullptr;float NextReport=0;};using ERR_HQUnit=Unit*;
|
||||||
struct HQ{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Airfield* m_Airfield=nullptr;
|
struct HQ{void BindSpecialSkills(SCR_AIGroup){}void AuditGroup(ERR_HQUnit){}Mode* m_Mode;GroupService m_Equipment;BodyService m_Bodies;int m_NextId=0;Array<ERR_HQUnit> m_Units;Mortars* m_Mortars=nullptr;Scouts* m_Scouts=nullptr;Recon* m_Recon=nullptr;Airfield* m_Airfield=nullptr;Scouts* m_Hunters=nullptr;Scouts* m_Towns=nullptr;
|
||||||
'''+hqbind+hqregister+r'''
|
'''+hqbind+hqregister+r'''
|
||||||
};
|
};
|
||||||
Intel* intel_;HQ* hq_;
|
Intel* intel_;HQ* hq_;
|
||||||
@@ -102,7 +102,7 @@ struct Ready{bool m_Stopped=false,m_Ready=false;Battery* m_Battery;int Finish(Mo
|
|||||||
'''+handoff+r'''
|
'''+handoff+r'''
|
||||||
}};
|
}};
|
||||||
int main(){
|
int main(){
|
||||||
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,{},{},0,{}};intel_=&intel;hq_=&hq;Group group;Mortars mortars;mortars.owned=&group;hq.m_Mortars=&mortars;
|
Mode mode;Intel intel{&mode,{},{} };HQ hq{&mode,{},{},0,{}};intel_=&intel;hq_=&hq;Group group;mode.sites.home.Group=&group;Mortars mortars;mortars.owned=&group;hq.m_Mortars=&mortars;
|
||||||
hq.RegisterGroup(&group);assert(hq.m_Units.Count()==1&&mode.publications==0&&Math::rolls==0);
|
hq.RegisterGroup(&group);assert(hq.m_Units.Count()==1&&mode.publications==0&&Math::rolls==0);
|
||||||
assert(hq.m_Equipment.ids.Count()==1&&hq.m_Equipment.ids[0]==hq.m_Units[0]->Id&&hq.m_Units[0]->Id==1);
|
assert(hq.m_Equipment.ids.Count()==1&&hq.m_Equipment.ids[0]==hq.m_Units[0]->Id&&hq.m_Units[0]->Id==1);
|
||||||
assert(!hq.m_Equipment.fits[0]); // Existing mortar readiness owns this group's radio allocation.
|
assert(!hq.m_Equipment.fits[0]); // Existing mortar readiness owns this group's radio allocation.
|
||||||
@@ -140,9 +140,17 @@ int main(){
|
|||||||
assert(hq.m_Equipment.ids.Count()==3&&hq.m_Equipment.ids[2]==hq.m_Units[2]->Id&&hq.m_Equipment.ids[2]==3&&hq.m_Equipment.fits[2]);
|
assert(hq.m_Equipment.ids.Count()==3&&hq.m_Equipment.ids[2]==hq.m_Units[2]->Id&&hq.m_Equipment.ids[2]==3&&hq.m_Equipment.fits[2]);
|
||||||
// A subsequent missing mortar registry cannot reroll an already bound group's opt-out.
|
// A subsequent missing mortar registry cannot reroll an already bound group's opt-out.
|
||||||
hq.RegisterGroup(&group);assert(hq.m_Equipment.ids.Count()==3&&!hq.m_Equipment.fits[0]&&hq.m_NextId==3);
|
hq.RegisterGroup(&group);assert(hq.m_Equipment.ids.Count()==3&&!hq.m_Equipment.fits[0]&&hq.m_NextId==3);
|
||||||
Group radioGuards;mode.sites.home.data.Kind=ERR_LocationKind::RADIO;hq.RegisterGroup(&radioGuards);
|
Group radioGuards;mode.sites.home.Group=&radioGuards;mode.sites.home.data.Kind=ERR_LocationKind::RADIO;hq.RegisterGroup(&radioGuards);
|
||||||
assert(hq.m_Units[3]->ReservedForSupport && hq.m_Units[3]->HomeGuard);
|
assert(hq.m_Units[3]->ReservedForSupport && hq.m_Units[3]->HomeGuard);
|
||||||
hq.RegisterGroup(&radioGuards);assert(hq.m_Units.Count()==4&&hq.m_Units[3]->ReservedForSupport);
|
hq.RegisterGroup(&radioGuards);assert(hq.m_Units.Count()==4&&hq.m_Units[3]->ReservedForSupport);
|
||||||
|
// Same home, separate mobile formation: only the permanent defenders are reserved.
|
||||||
|
Group mobile;hq.RegisterGroup(&mobile);
|
||||||
|
assert(!hq.m_Units[4]->ReservedForSupport && !hq.m_Units[4]->HomeGuard && hq.m_Units[4]->HomeId==hq.m_Units[3]->HomeId);
|
||||||
|
Group hunter;Scouts hunters;hunters.owned=&hunter;hq.m_Hunters=&hunters;hq.RegisterGroup(&hunter);
|
||||||
|
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()); // every hunter selects physical manpack fitting
|
||||||
|
Group ordinaryAgain;hq.RegisterGroup(&ordinaryAgain);assert(hq.m_Equipment.ids.back()==7); // ordinary fitting remains distributed
|
||||||
|
Group town;Scouts towns;towns.owned=&town;hq.m_Towns=&towns;hq.RegisterGroup(&town);
|
||||||
|
assert(hq.m_Equipment.ids.back()==1&&hq.m_Equipment.fits.back()&&!hq.m_Units.back()->ReservedForSupport&&!hq.m_Units.back()->HomeGuard);
|
||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
print('PASS: actual Intel/HQ methods and one-shot qualified readiness handoff deliver late actors once; duplicate readiness, accepted spawn, pickup/drop, death and original negative rolls do not mint assignments')
|
print('PASS: actual Intel/HQ methods and one-shot qualified readiness handoff deliver late actors once; duplicate readiness, accepted spawn, pickup/drop, death and original negative rolls do not mint assignments')
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user