143 Commits
v1.4.0 ... dev

Author SHA1 Message Date
Taco
9520b76246 Updated soundhandler with mirror performance for Red and Blue Clients… (#575) 2025-11-09 22:21:04 -05:00
Starfire13
67183c31da Improve B-52 mission types and update B-52 and B-1 loadouts (#615) 2025-11-09 21:50:01 -05:00
Druss99
54759f1f94 Fixed bugs wrt planning escort flights (#612) 2025-11-09 21:49:39 -05:00
Starfire13
0fcf308079 Add Mi-17 variant to Mi-8MT configuration (#614) 2025-11-08 18:09:44 -05:00
Drexyl
1ad27e27ea Update Splash Damage Plugin to 3.4.2 (#601)
Co-authored-by: Druss99 <42724070+Druss99@users.noreply.github.com>
2025-11-08 14:35:17 +00:00
Starfire13
eae5e1af30 Starfire campaign updates (#613) 2025-11-08 08:40:40 -05:00
Starfire13
b268d09432 Swap intra-flight and inter-flight radio indices (#608) 2025-11-07 08:33:25 -05:00
Drexyl
220b83a22d Add additional supported Carrier/LHA for Airboss (#603) 2025-11-07 08:32:07 -05:00
Starfire13
83530af35d 10.8 Update for Noisy Cricket, Aegean Aegis, and Vegas Nerve (#607) 2025-11-06 15:18:57 -05:00
Starfire13
00475d767b Update Final Countdown II and Operation Velvet Thunder to 10.8 (#606) 2025-11-06 09:43:14 -05:00
Starfire13
1a8faff717 Update Tornado loadouts (#610) 2025-11-06 09:39:36 -05:00
Druss99
68e0d43a1d fix frontline generation (#609) 2025-11-05 17:52:38 -05:00
Starfire13
0510af8b6b Added missing High Digit SAMs mod version (#605) 2025-11-02 14:59:32 -05:00
Raffson
c54ac4e21a Save compat fix w.r.t. renamed airport in AFG 2025-10-28 21:05:56 +01:00
Raffson
bbc41f4c1a Pydcs update for airfields (Normandy/Lappi/AFG) 2025-10-27 21:10:59 +01:00
Raffson
8bc8eb0e28 Update beacons 2025-10-27 20:50:24 +01:00
Starfire13
86f647cc6d Starfire campaign updates #1 (#604) 2025-10-26 23:16:40 +00:00
Raffson
2f16916ac4 Rename & Extend WW2-Essex layout to support ARA-VDM + CVA-31 2025-10-26 18:21:38 +01:00
Druss99
06044dd4c3 fix frontline debriefing (#602) 2025-10-23 07:15:37 -04:00
Eclipse/Druss99
c46cce01b6 fix ownfor being set to UN Peacekeepers regardless of campaign 2025-10-21 14:20:20 -04:00
Eclipse/Druss99
d78e570d9d fix a bug with untasked aircraft still spawning 2025-10-20 15:14:43 -04:00
Eclipse/Druss99
f4a1908088 fix a bug with pretense generation wrt neutral bases 2025-10-20 15:14:43 -04:00
Druss99
23341e69fa update changelog (#596) 2025-10-19 22:09:48 -04:00
Druss99
11fc28ad3a fix bugs wrt divert airfields for opfor (#595) 2025-10-19 21:40:40 -04:00
Druss99
9b96b7be26 fix runtime error where dictionary size changes (#594) 2025-10-19 20:10:19 -04:00
Raffson
f7ee923cbf Bump campaign version to 10.8 for neutral base support 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
3d788e3ae8 fix numerous UI elements using is_ownfor 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
624f2a00fd show PRESENT status for neutral TGOs 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
6c3cbbf0de avoid spawning neutral tgos with units 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
96d0b2b7d9 fix save compat 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
855d269420 refactor trigger zone generation 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
c0748e2a3e add support for neutral FOBs 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
9f10ecc884 fix save compat 2025-10-19 19:34:38 +02:00
Eclipse/Druss99
31c80dfd02 refactor of previous commits
refactor to enum

typing and many other fixes

fix tests

attempt to fix some typescript

more typescript fixes

more typescript test fixes

revert all API changes

update to pydcs

mypy fixes

Use properties to check if player is blue/red/neutral

update requirements.txt

black -_-

bump pydcs and fix mypy

add opponent property

bump pydcs
2025-10-19 19:34:38 +02:00
Drexyl
362ce66f80 Airboss Moose Module 1.0 (#592)
* Initial Commit

* Rescue Helo Options

* Main modifications to Airboss/Moose Dev Branch

* Restore plugin.json

* Airboss Moose Module 1.0

* Fix plugins.json

---------

Co-authored-by: Druss99 <42724070+Druss99@users.noreply.github.com>
2025-10-18 14:08:48 +00:00
Drexyl
96ee73dd50 Fix for Offensive Jamming when object has been destroyed (#591)
* Fix Offensive Jamming when object has been destroyed
2025-10-18 09:42:02 -04:00
Eclipse/Druss99
f82fc3a930 use aircraft display names instead of variant ids in faction editor 2025-10-12 19:53:01 +02:00
Eclipse/Druss99
cf575fba2a fix a bug with parking allocation 2025-10-12 19:42:52 +02:00
Eclipse/Druss99
96f940bc59 fix bugs with mixed parking 2025-10-12 19:01:07 +02:00
Drexyl
98c71c255b Fix to exclude Moose plugins from Pretense injection (#585) 2025-10-07 18:58:19 -04:00
Drexyl
0e5567ceee Update EW Script with fixes/enhancements (#584) 2025-10-07 08:13:44 -04:00
Druss99
2832ece316 add channels for MiG-29 Fulcrum (#582) 2025-10-05 14:27:06 -04:00
Druss99
5cfd6c86f3 add radios for MiG-29 Fulcrum (#581) 2025-10-04 13:48:11 -04:00
Druss99
27ef269435 add support for more units (#580) 2025-09-22 18:27:34 -04:00
Druss99
8b46ef5e35 add support for MiG-29A Fulcrum (#579) 2025-09-20 18:41:23 -04:00
Eclipse/Druss99
e243d074ff su30 mod update 2025-09-20 19:36:41 +02:00
Eclipse/Druss99
cc39e6579b bump pydcs 2025-09-20 19:36:41 +02:00
Druss99
d7eaf1330f remove antiship from tu-160 (#577) 2025-09-20 13:05:02 -04:00
Druss99
c44ea0025d Fix plugins.json & moose plugins (#574) 2025-09-08 00:35:54 +02:00
Taco
1216c90891 Add Moose's Soundhandler Plugin (#558)
* Soundhandler: Adds sounds for weapons fired at or by blue units as well as destruction sounds for enemy jets and ground units.  By default plays only to the groups involved.

* Added Specific Options and removed 0.1 from mnemonic

* Update was incomplete before, use this one

* Linked in UI choices and consolidated logging functions

* Remove soundhandler duplicate

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-09-07 17:25:14 +00:00
Drexyl
ea80ddc321 Update CJS Super Hornet Mod to 2.4.4 (#572)
* Update CJS Super Hornet Mod to 2.4.4

* Update UI for CJS Super Hornet Mod to 2.4.4

* Update default loadouts for CJS Super Hornet Mod to 2.4.4

* Update changelog.md for CJS Super Hornet Mod to 2.4.4

* Update Growler loadout for missing external fuel tank
2025-09-07 19:24:07 +02:00
Druss99
121ca20edb Fix naval squadron retreat bug (#567) 2025-09-07 17:37:07 +02:00
Starfire13
e4586add72 Add factories to Northern Guardian (#571) 2025-09-07 17:29:45 +02:00
Xisco Lladó
32b2e40b37 Update convoys spawns & CTLD landing zones for Syrian Shield (#570)
* updated convoys spawns and CTLD landing zones

* removed red helipads on palmyra zone

---------

Co-authored-by: Xisco <x.llado@hotelinking.com>
2025-09-07 17:29:00 +02:00
Taco
e8190748eb New Moose-based Autolase alternative with UI options (#565) 2025-08-27 19:24:00 +02:00
Drexyl
d2aa615133 Update EW Script and refine logic (#555) 2025-08-27 19:20:27 +02:00
Starfire13
756602e682 Update Su-24 loadouts (#566) 2025-08-27 19:18:39 +02:00
Taco
204950e487 MarkerOps Plugin (#560)
* MarkerOps

* Change MarkerOps to MooseMarkerOps in all places

* Prettify name in UI

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-08-23 16:37:34 +02:00
Druss99
6290607eb6 Expend all ARMs for SEAD (#562) 2025-08-13 12:32:36 +02:00
Druss99
afad73ac27 Fix bugs while loading air wing configs (#552)
* fix bugs with air wing config loading

* add aircraft type to airwing config for better loading
2025-08-10 21:14:38 +02:00
Eclipse/Druss99
52fbc75e1c update changelog 2025-08-10 19:40:06 +02:00
Xisco Lladó
5c5af40bd5 Update Operation syrian shield (#554)
* New campaign Operation_syrian_shield

* removed author on player factions. Credited to 'Fuzzle'

* changed recommended factions to the easy way

* Update operation_syrian_shield.yaml

removed mission time, and squadrons start full

* Update operation_syrian_shield.yaml

better campaign description

* added better squadron descriptions

* updated campaign description

---------

Co-authored-by: Xisco <x.llado@hotelinking.com>
2025-08-10 16:19:50 +02:00
ingax01
d1f66ed040 Remove unused SU-30 payload files (#551) 2025-08-04 00:08:28 +02:00
Druss99
6467193c88 Save-compat fix for su-30 update (#553) 2025-08-04 00:07:38 +02:00
Drexyl
b2670ba30b Add Military Aircraft Mod 1.7.2 (#547) 2025-08-03 02:02:14 +02:00
Raffson
dcf4da57b3 Update starlette + fastapi 2025-08-02 19:27:30 +02:00
Raffson
6f825db086 Update h11 2025-08-02 19:27:15 +02:00
Raffson
afa9122885 Bump electron to 28.3.2 2025-08-02 19:09:31 +02:00
Raffson
df8e09a1ad Update npm packages 2025-08-02 19:00:36 +02:00
Druss99
5d038e8247 Add support for Control Point Influence Zones (#540) 2025-08-02 18:47:55 +02:00
Xisco Lladó
ce70952ee0 New campaign Operation_syrian_shield (#544)
* New campaign Operation_syrian_shield

* removed author on player factions. Credited to 'Fuzzle'

* changed recommended factions to the easy way

* Update operation_syrian_shield.yaml

removed mission time, and squadrons start full

* Update operation_syrian_shield.yaml

better campaign description

* added better squadron descriptions

---------

Co-authored-by: Xisco <x.llado@hotelinking.com>
2025-08-02 18:30:15 +02:00
Drexyl
1d16aba678 Add Moose as base plugin (#522) 2025-08-02 18:28:36 +02:00
Revinl
f3156ab51a Northern Guardian campaign for the Germany map by Revinl (#545) 2025-08-02 16:27:52 +00:00
ingax01
ed9fa37341 Updated SU-30 weapon definitions to version v2.8.03 Beta + CWS 3.70 (#546) 2025-08-02 18:20:01 +02:00
Starfire13
12aa1ee287 Add sniper pod to Viper loadout (#549) 2025-08-02 18:15:07 +02:00
Druss99
7e755f1eb1 bump pydcs (#548) 2025-07-28 22:18:29 +02:00
Starfire13
3da1689767 Fixed typos in Tetrarch description (#542) 2025-07-21 16:53:02 +02:00
Raffson
0671e45dde Restore squadrons for Essex-transformed carriers 2025-07-20 12:36:48 +02:00
Drexyl
c15b4ac83f Essex as AircraftCarrier hack (#541) 2025-07-19 18:21:33 +00:00
Starfire13
f1eb514295 Crossing the Rubicon improved performance (#539) 2025-07-14 12:38:57 +02:00
Raffson
df8f1c4291 Fix some auto-rotate issues in exercise quasar 2025-07-13 19:19:09 +02:00
Raffson
c32525b542 Formatting -_- 2025-07-13 19:05:10 +02:00
Raffson
db14b691cc Make F4U-1D Carrier/LHA capable 2025-07-13 18:55:09 +02:00
Raffson
052a04ca05 Properly support Essex carrier 2025-07-13 18:41:20 +02:00
Raffson
ad100a1a17 Overrride WFMR-window's reject to properly reset game-state 2025-07-13 13:46:57 +02:00
Drexyl
4412a0fa91 Add Tornado F3 ADV (1.0) mod support 2025-07-13 13:35:53 +02:00
Starfire13
23a7add2f1 Add missing Apache and FC Tiger II to Blufor Late Cold War faction (#538) 2025-07-13 13:34:21 +02:00
Starfire13
040362b333 Add Viet Cong 1965 faction (#537) 2025-07-13 13:32:59 +02:00
Starfire13
4155afc69a Set performance to 3/3 for Crossing The Rubicon (#536)
Setting this to 3/3 for now as folks are having performance issues
2025-07-13 13:32:05 +02:00
Starfire13
cf84c03d8f Implements campaign inversion for 13 of Starfire's campaigns (#535) 2025-07-13 13:31:32 +02:00
Starfire13
336384cd74 Implement campaign inversion for Crossing the Rubicon (#534) 2025-07-13 13:30:27 +02:00
Druss99
da7d3a3f1a Fix S-3B unable to do OCA/Aircraft (#533) 2025-07-08 22:25:40 +00:00
Starfire13
1231cb145d Add Germany Cold War campaign (#530) 2025-07-09 00:16:50 +02:00
Drexyl
47488a0bdd F4U-1D Banner Image (#529) 2025-07-08 07:55:58 +02:00
Starfire13
015859e91e F4U-1D loadouts (#528) 2025-07-08 00:39:34 +02:00
Raffson
9c0d114085 Save compat fix for Sinai's renamed Palmahim to Palmachim 2025-07-06 22:40:23 +02:00
Raffson
f907356f93 Pydcs update for new Sinai airports + save-compat fix 2025-07-06 22:37:07 +02:00
Raffson
fcb610423b Update beacon-data 2025-07-06 22:12:32 +02:00
Raffson
3086f64e98 F4U-1D support except banner & payloads 2025-07-06 22:09:50 +02:00
Raffson
5592a834c9 Downgrade h11 to 0.14 2025-07-06 21:59:09 +02:00
Raffson
1e48affc1c Add Japanese WW2 ground units 2025-07-06 21:59:09 +02:00
Raffson
f27b6e28e0 Add support Essex 2025-07-06 21:58:59 +02:00
Raffson
d0ff271a79 Update pydcs for 2.9.17.12034 2025-07-06 19:31:05 +02:00
Cedric Menard
9a00a60cd4 Initial support for Germany Cold War (#518)
* Initial support of Germany Cold War terrain

* Updated with latest beacon import

* Updated changelog and ran black

* Fixed the GermanyCW landmap and naming scheme

* Temporarily changed the requirements for pydcs to my branch (will switch back when merged)

* Updated landmap for germanycw with even fewer exclusions

* Removed unnecessary changes

* Updated requirements to lastest pydcs

* Fixed naming issues for Germany

* Update pydcs to latest commit for GCW support

* Add landmap to game.theater.__init__

* Update requirements.txt

* Resolve PyCharm type-waring

* Updated pydcs and exclusion of germany

* Updated land and sea shapefiles for Germany Cold War with new map limits

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-07-06 19:30:00 +02:00
MrPozor
5254ef3442 Fix #304: Waypoint numbers should start at 0 on map (#526) 2025-06-11 19:50:30 +02:00
MrPozor
fe8efbe905 Revert "Waypoint numbers should start at 0 on map. (#524)" (#525) 2025-06-10 21:28:14 +02:00
MrPozor
48040d7306 Waypoint numbers should start at 0 on map. (#524) 2025-06-09 22:58:15 +02:00
Raffson
f3ed25b5b2 Make EPLRS configurable
Resolves #517
2025-06-07 22:04:56 +02:00
Raffson
2d15db1932 Update h11 to 0.16.0 2025-06-07 19:40:19 +02:00
Raffson
1fa1f830d4 Update bug-report templates for v1.4.1 2025-06-07 19:33:05 +02:00
Raffson
0c0291fc5e Changelog for v1.4.1 2025-06-07 19:32:29 +02:00
Raffson
24c6b62ad4 Fix missing check in handle_ew_jamming w.r.t. plugin option 2025-06-07 19:23:36 +02:00
Starfire13
8c9343e0db Campaign inversion fix for Operation Vectron's Claw (#520) 2025-06-07 16:04:56 +02:00
Starfire13
a33da01af6 Blufor Late Cold War & Russia 1980 faction update (#521) 2025-06-07 16:03:40 +02:00
Starfire13
ce8ccb0227 Update README.md with correct link to preview builds page on the wiki (#519) 2025-05-30 13:10:07 +02:00
Drexyl
68a9dbd141 Update Grinnelli Designs F-22A Mod to 2.0.0 (May 2025) (#515) 2025-05-28 00:54:14 +02:00
Drexyl
4c3f0a82d9 Add EA-6B Prowler to ALQ-99 Jammer Pod (#516) 2025-05-28 00:48:55 +02:00
Starfire13
dc85287cd8 Add locales to Ukraine factions (#513)
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-24 15:48:38 +00:00
szcz13
65f4a2cf86 Added new and updated weapons CLSIDs and payloads for MiG-31BM mod (#514) 2025-05-24 17:38:16 +02:00
Druss99
02de6d325a Add invisible FOBs (#504)
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-18 17:50:47 +02:00
Druss99
7b16967641 Prevent saving packages with Escorts only (#509)
Fixes a bug that would later pop up when trying to calculate start-times...
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-17 15:58:35 +00:00
Starfire13
0e7ea0050f Add OCA-Aircraft to B-21's tasks (#510)
Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-17 15:57:12 +00:00
Starfire13
ec0e64eaf4 Add Growler + Raptor squadrons to some campaigns (#505) 2025-05-17 17:55:41 +02:00
szcz13
b12169f180 Update for MiG-31BM v2.6 (#512)
* Add files via upload

* Add files via upload

* Update start_generator.py

* Update faction.py

* Update QNewGameWizard.py

* Update QGeneratorSettings.py

* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update MiG-31BM.lua

* Added SEAD Escort to MiG-31BM.yaml

* mig31bm.py fixed weapon injection

* Update mig31bm.py

formatted black

* Update faction.py

reformatted black

* Update start_generator.py

reformatted black

* Update __init__.py

* Update QGeneratorSettings.py

reformatted black

* added support for MiG-31BM mod

* Commit changes which can't seem to be discarded...

* Remove (presumably) accidentally copied file

* Remove unused imports

* Updated MiG-31BM mod version in QGeneratorSettings.py

* Added new Kh-31P missile to MiG-31BM mod

* Added new Kh-31P missile to MiG-31BM mod

* Updated MiG-31BM mod payloads for a new Kh-31P.lua

* Remove double entry

---------

Co-authored-by: Raffson <Raffson@users.noreply.github.com>
2025-05-17 15:55:05 +00:00
Raffson
3a5d0e831c Formatting -_- 2025-05-10 16:40:57 +02:00
Raffson
903a6c4d5f Update changelog 2025-05-07 20:12:03 +02:00
Raffson
e36f07a3ff Preference option to set up preferences on every start (#503) 2025-05-07 20:02:21 +02:00
Raffson
2d65a038ee Remove print statements 2025-05-06 17:24:29 +02:00
Raffson
09617adc56 Passive defense for non-air-to-air flights in package with jammer 2025-05-01 19:57:01 +02:00
Raffson
01832b6594 Add MPS-410 to weapons data 2025-05-01 19:41:02 +02:00
Raffson
30ad71f93b Refactor & improve EW-jamming setup in mission-generator 2025-05-01 17:22:46 +02:00
Raffson
4f8ce77e50 Add "has_built_in_ecm" field to AircraftType
To be used with EW-jamming script...
2025-05-01 17:21:52 +02:00
Raffson
464ece60da Save-compat fix w.r.t. renamed airfield 2025-04-21 21:24:32 +02:00
Raffson
cafaea897d Pydcs update for new Lappi airfields 2025-04-21 21:21:36 +02:00
Raffson
58820dd6c6 Update Lappi beacons 2025-04-21 21:18:29 +02:00
Starfire13
8e845e6a27 Add Tornado IDS to Blufor Late Cold War faction (#502)
Add missing Tornado IDS
2025-04-21 18:04:13 +02:00
Raffson
e1b55816e3 Update actions/checkout to v4 2025-04-20 23:21:00 +02:00
Raffson
e95661c6a2 Update actions/setup-node to v4 2025-04-20 23:17:23 +02:00
Raffson
c96e0d8f28 Update actions/setup-python to v5? 2025-04-20 23:14:52 +02:00
Raffson
dfb65facb5 Another attempt at fixing the pipeline -_- 2025-04-20 23:01:22 +02:00
Raffson
8c6ca4de5f Pin python version for black in lint.yml 2025-04-20 22:58:19 +02:00
Raffson
1c3dee2098 Pydcs data update 2025-04-19 17:49:21 +02:00
Raffson
170e432008 Preparations for v1.5.0 2025-04-14 21:34:17 +02:00
626 changed files with 163561 additions and 32979 deletions

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build"
and provide a link to the applicable build in the field below.
options:
- v1.3.1
- v1.4.1
- Test build
- type: input
attributes:

View File

@@ -39,7 +39,7 @@ body:
If the bug was found in a test/development build, select "Test build" or "Development build"
and provide a link to the applicable build in the field below.
options:
- 1.3.1
- 1.4.1
- Test build
- type: input
attributes:

View File

@@ -4,7 +4,7 @@ runs:
using: composite
steps:
- name: Set up Node
uses: actions/setup-node@v2
uses: actions/setup-node@v4
with:
node-version: "16"
cache: npm

View File

@@ -4,7 +4,7 @@ runs:
using: composite
steps:
- name: Set up Python
uses: actions/setup-python@v2
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

View File

@@ -12,7 +12,7 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: true

View File

@@ -7,8 +7,8 @@ jobs:
name: Black
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: psf/black@stable
with:
src: "."
@@ -18,7 +18,7 @@ jobs:
name: Type checking
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: true

View File

@@ -14,7 +14,7 @@ jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: true

View File

@@ -5,7 +5,7 @@ jobs:
name: Python tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
with:
submodules: true
@@ -24,7 +24,7 @@ jobs:
name: Typescript tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js

View File

@@ -68,7 +68,7 @@ Our plans for future releases can be found on our
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
Each planned release has a Project, and the page for that project has columns for to do,
in progress, and done. Items in the Done column are in the
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
for that release. Items in the To do column are planned to be added to that release.
## Resources

View File

@@ -1,3 +1,47 @@
# Retribution v1.5.0
## Features/Improvements
* **[Campaigns]** Ability to define invisible FOBs
* **[Campaigns]** Ability to define influence zones for Control Points
* **[Plugins]** Improvements to AI support for EW Script 2.0
* **[Plugins]** Add moose as a base plugin
* **[Config]** New preference setting to trigger the first-start window on every start (could help in scenarios multiple Retribution instances need to run concurrently)
* **[Modding]** Update Grinelli Designs F-22A Mod to 2.0.0 (May 2025)
* **[Campaign Design]** Added support for Germany Cold War terrain by Ugra Media
* **[Options]** New option to control EPLRS-task injection in mission generator
* **[Modding]** Add Tornado F3 ADV (v1.0, requires FC3 fix)
* **[Modding]** Add Military Aircraft Mod 1.7.2 (See VSN Discord)
* **[Payload Editor]** Add Sniper POD
* **[Modding]** Update SU-30 mod to v2.8.04 Beta + CWS 3.72
* **[Plugins]** EW Script - DEAD added to applicable flight types.
* **[Plugins]** EW Script - Offensive Jamming restricted to aircraft with ALQ99/249 pods, or "has_built_in_jamming: true" in aircarft yaml (AI and Player)
* **[Modding]** Update to CJS Super Hornet Mod v2.4.4
* **[Engine]** Support for DCS 2.9.20.15010, including MiG-29A Fulcrum support.
* **[Campaigns]** Ability to define neutral FOBs and Airfields
* **[Modding]** Add Airboss Moose Module
* **[AirWing]** Use aircraft display names for easier differentiation between modules
## Fixes
* **[Flight Plans]** Fixed bugs wrt planning escort flights
* **[Flight Plans]** Added AntiShipStrike as a fallback task for OCA/Aircraft to fix a bug where the S-3B could not do OCA/Aircraft
* **[Squadrons]** Fixed a bug where loading an air wing config would not properly load all squadrons
* **[Flight Plans]** Fixed a bug where SEAD flights would fire one ARM and RTB
* **[Plugins]** EW Script - Fix radar detection routine.
* **[Campaign]** Fixed a bug where sinking a destroyer in a carrier group would cause squadrons to be removed from the carrier
* **[Engine]** Fixed a bug with transfers to Helipads
* **[Engine]** Fixed a bug with parking allocation
* **[Flight Plans]** Fixed a bug where divert airfield was broken for opfor
* **[Engine]** Fixed a bug with state.json loading wrt transfers
* **[Engine]** Fixed a bug wrt pretense generation and moose script conflicts
* **[Engine]** Fixed a bug where frontline debriefing was not properly calculated
* **[Mission Generation]** Fixed an issue where blue and red units spawned next to each other on frontlines
# Retribution v1.4.1 (hotfix)
## Fixes
* **[Plugins]** Avoid injecting jamming-tasks for player-controller aircraft when EW-Jamming plugin is disabled
# Retribution v1.4.0
## Features/Improvements

22917
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,7 @@
"@types/leaflet": "^1.9.8",
"@types/redux-logger": "^3.0.13",
"@types/websocket": "^1.0.10",
"electron": "^22.3.27",
"electron": "^28.3.2",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0",

View File

@@ -60,7 +60,7 @@ const WaypointMarker = (props: WaypointMarkerProps) => {
useEffect(() => {
const waypoint = props.waypoint;
marker.current?.setTooltipContent(
`${props.number} ${waypoint.name}<br />` +
`${props.number-1} ${waypoint.name}<br />` +
`${waypoint.altitude_ft.toFixed()} ft ${waypoint.altitude_reference}<br />` +
waypoint.timing
);

View File

@@ -7,9 +7,9 @@
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "DCS Retribution"
copyright = "2024, DCS Retribution Team"
copyright = "2025, DCS Retribution Team"
author = "DCS Retribution Team"
release = "1.4.0"
release = "1.5.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@@ -40,7 +40,7 @@ class ArmedForces:
# all layouts which are usable by the given faction.
for layout in LAYOUTS.layouts:
if layout.generic and layout.usable_by_faction(faction):
# Creates a faction compatible GorceGroup
# Creates a faction compatible ForceGroup
self.add_or_update_force_group(ForceGroup.for_layout(layout, faction))
# Add all preset groups afterwards to prevent them being merged with generics

View File

@@ -258,6 +258,9 @@ class ForceGroup:
if unit_count == 0:
# No units to be created so dont create a theater group for them
return
# If the control point is neutral, don't create any units at all
if ground_object.control_point.captured.is_neutral:
return
# Generate Units
fixed_pos = FIXED_POS_ARG in unit_group.name
fixed_hdg = FIXED_HDG_ARG in unit_group.name

View File

@@ -35,6 +35,7 @@ if TYPE_CHECKING:
from game.sim.gameupdateevents import GameUpdateEvents
from game.sim.simulationresults import SimulationResults
from game.squadrons import Squadron, Pilot
from game.theater.player import Player
from game.transfers import TransferOrder
from game.data.weapons import WeaponType
from .flightmember import FlightMember
@@ -174,7 +175,7 @@ class Flight(
self.roster = FlightMembers.from_roster(self, self.roster)
@property
def blue(self) -> bool:
def blue(self) -> Player:
return self.squadron.player
@property

View File

@@ -23,7 +23,13 @@ class EscortFlightPlan(FormationAttackFlightPlan):
@property
def split_time(self) -> datetime:
if self.package.primary_flight and self.package.primary_flight.flight_plan:
# Avoid infinite recursion when this escort flight is itself the primary flight
# This can happen when only escort flights remain in a package
if (
self.package.primary_flight
and self.package.primary_flight != self.flight
and self.package.primary_flight.flight_plan
):
return self.package.primary_flight.flight_plan.mission_departure_time
else:
return super().split_time

View File

@@ -11,7 +11,7 @@ from ..packagewaypoints import PackageWaypoints
if TYPE_CHECKING:
from game.coalition import Coalition
from game.data.doctrine import Doctrine
from game.theater import ConflictTheater
from game.theater import ConflictTheater, Player
from game.threatzones import ThreatZones
from ..flight import Flight
from ..package import Package
@@ -38,7 +38,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
self._generate_package_waypoints_if_needed(dump_debug_info)
self._flight_plan = self.build(dump_debug_info)
except NavMeshError as ex:
color = "blue" if self.flight.squadron.player else "red"
color = "blue" if self.flight.squadron.player.is_blue else "red"
raise PlanningError(
f"Could not plan {color} {self.flight.flight_type.value} from "
f"{self.flight.departure} to {self.package.target}"
@@ -71,7 +71,7 @@ class IBuilder(ABC, Generic[FlightPlanT, LayoutT]):
return self.flight.coalition
@property
def is_player(self) -> bool:
def is_player(self) -> Player:
return self.coalition.player
@property

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Any, Optional, TYPE_CHECKING, Union
@@ -26,6 +25,7 @@ class SquadronConfig:
name: Optional[str]
nickname: Optional[str]
female_pilot_percentage: Optional[int]
aircraft_type: Optional[str]
@property
def auto_assignable(self) -> set[FlightType]:
@@ -51,6 +51,7 @@ class SquadronConfig:
data.get("name", None),
data.get("nickname", None),
data.get("female_pilot_percentage", None),
data.get("aircraft_type", None),
)
@staticmethod
@@ -83,15 +84,14 @@ class CampaignAirWingConfig:
try:
base = theater.control_point_named(base_id)
except:
if base_id == "Red CV":
base = next((c for c in carriers if not c.captured), None)
elif base_id == "Blue CV":
base = next((c for c in carriers if c.captured), None)
elif base_id == "Red LHA":
base = next((l for l in lhas if not l.captured), None)
elif base_id == "Blue LHA":
base = next((l for l in lhas if l.captured), None)
logging.warning(
f"Control point {base_id} not found, trying to match by full name"
)
if not base:
try:
base = theater.control_point_by_full_name(base_id)
except KeyError:
logging.error(f"Control point {base_id} not found, skipping")
for squadron_data in squadron_configs:
if base is None:
logging.warning(

View File

@@ -60,7 +60,7 @@ class DefaultSquadronAssigner:
) -> Optional[SquadronDef]:
for preferred_aircraft in config.aircraft:
squadron_def = self.find_preferred_squadron(
preferred_aircraft, config.primary, control_point
preferred_aircraft, config.aircraft_type, config.primary, control_point
)
if squadron_def is not None:
return squadron_def
@@ -78,7 +78,11 @@ class DefaultSquadronAssigner:
)
def find_preferred_squadron(
self, preferred_aircraft: str, task: FlightType, control_point: ControlPoint
self,
preferred_aircraft: str,
aircraft_type: Optional[str],
task: FlightType,
control_point: ControlPoint,
) -> Optional[SquadronDef]:
# Attempt to find a squadron with the name in the request.
squadron_def = self.find_squadron_by_name(
@@ -90,7 +94,7 @@ class DefaultSquadronAssigner:
# If the name didn't match a squadron available to this coalition, try to find
# an aircraft with the matching name that meets the requirements.
try:
aircraft = AircraftType.named(preferred_aircraft)
aircraft = AircraftType.named(aircraft_type or preferred_aircraft)
except KeyError:
logging.warning(
"%s is neither a compatible squadron or a known aircraft type, "

View File

@@ -16,6 +16,7 @@ from dcs.terrain import Airport
from dcs.unitgroup import PlaneGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from game.controlpoint_influenceradius import ControlPointInfluenceRadius, point_in_zone
from game.point_with_heading import PointWithHeading
from game.positioned import Positioned
from game.profiling import logged_duration
@@ -24,6 +25,8 @@ from game.theater.controlpoint import (
Airfield,
Carrier,
ControlPoint,
ControlPointType,
Player,
Fob,
Lha,
OffMapSpawn,
@@ -53,6 +56,8 @@ class MizCampaignLoader:
FOB_UNIT_TYPE = Unarmed.SKP_11.id
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD", "FARP"]
INVISIBLE_FOB_UNIT_TYPE = Unarmed.M_818.id
NEUTRAL_FOB_UNIT_TYPE = Unarmed.KrAZ6322.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
@@ -120,9 +125,13 @@ class MizCampaignLoader:
def control_point_from_airport(
self, airport: Airport, ctld_zones: List[Tuple[Point, float]]
) -> ControlPoint:
cp = Airfield(
airport, self.theater, starts_blue=airport.is_blue(), ctld_zones=ctld_zones
)
if airport.dynamic_spawn:
starting_coalition = Player.NEUTRAL
elif airport.is_blue():
starting_coalition = Player.BLUE
else:
starting_coalition = Player.RED
cp = Airfield(airport, self.theater, starting_coalition, ctld_zones=ctld_zones)
# Use the unlimited aircraft option to determine if an airfield should
# be owned by the player when the campaign is "inverted".
@@ -130,42 +139,54 @@ class MizCampaignLoader:
return cp
def country(self, blue: bool) -> Country:
country = self.mission.country(
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
)
def country(self, blue: Player) -> Country:
if blue.is_blue:
country = self.mission.country(self.BLUE_COUNTRY.name)
else:
country = self.mission.country(self.RED_COUNTRY.name)
# Should be guaranteed because we initialized them.
assert country
return country
@property
def blue(self) -> Country:
return self.country(blue=True)
return self.country(blue=Player.BLUE)
@property
def red(self) -> Country:
return self.country(blue=False)
return self.country(blue=Player.RED)
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
def off_map_spawns(self, blue: Player) -> Iterator[PlaneGroup]:
for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
def carriers(self, blue: Player) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.CV_UNIT_TYPE:
yield group
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
def lhas(self, blue: Player) -> Iterator[ShipGroup]:
for group in self.country(blue).ship_group:
if group.units[0].type == self.LHA_UNIT_TYPE:
yield group
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
def fobs(self, blue: Player) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.FOB_UNIT_TYPE:
yield group
def invisible_fobs(self, blue: Player) -> Iterator[VehicleGroup]:
for group in self.country(blue).vehicle_group:
if group.units[0].type == self.INVISIBLE_FOB_UNIT_TYPE:
yield group
@property
def neutral_fobs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type == self.NEUTRAL_FOB_UNIT_TYPE:
yield group
@property
def ships(self) -> Iterator[ShipGroup]:
for group in self.red.ship_group:
@@ -272,16 +293,22 @@ class MizCampaignLoader:
def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@cached_property
def cp_influence_zones(self) -> List[ControlPointInfluenceRadius]:
return ControlPointInfluenceRadius.from_trigger_zones(
self.mission.triggers._zones
)
@cached_property
def control_points(self) -> dict[UUID, ControlPoint]:
control_points = {}
for airport in self.mission.terrain.airport_list():
if airport.is_blue() or airport.is_red():
if airport.is_blue() or airport.is_red() or airport.is_neutral():
ctld_zones = self.get_ctld_zones(airport.name)
control_point = self.control_point_from_airport(airport, ctld_zones)
control_points[control_point.id] = control_point
for blue in (False, True):
for blue in (Player.RED, Player.BLUE):
for group in self.off_map_spawns(blue):
control_point = OffMapSpawn(
str(group.name), group.position, self.theater, starts_blue=blue
@@ -312,17 +339,47 @@ class MizCampaignLoader:
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
for fob in self.invisible_fobs(blue):
ctld_zones = self.get_ctld_zones(fob.name)
control_point = Fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=blue,
ctld_zones=ctld_zones,
is_invisible=True,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
for fob in self.neutral_fobs:
ctld_zones = self.get_ctld_zones(fob.name)
control_point = Fob(
str(fob.name),
fob.position,
self.theater,
starts_blue=Player.NEUTRAL,
ctld_zones=ctld_zones,
)
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
if self.cp_influence_zones:
for cp in control_points.values():
for influence_radius in self.cp_influence_zones:
if cp.full_name == influence_radius.cp_name:
cp.influence_radius = influence_radius
return control_points
@property
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group:
for group in self.country(blue=Player.BLUE).vehicle_group:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group
@property
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
for group in self.country(blue=True).ship_group:
for group in self.country(blue=Player.BLUE).ship_group:
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
yield group
@@ -346,7 +403,7 @@ class MizCampaignLoader:
@property
def cp_convoy_spawns(self) -> Iterator[VehicleGroup]:
for group in self.country(blue=True).vehicle_group:
for group in self.country(blue=Player.BLUE).vehicle_group:
if group.units[0].type == self.CP_CONVOY_SPAWN_TYPE:
yield group
@@ -471,7 +528,59 @@ class MizCampaignLoader:
def objective_info(
self, near: Positioned, allow_naval: bool = False
) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(near.position, allow_naval)
zones_containing_point = [
z
for z in self.cp_influence_zones
if point_in_zone(z.zone_def, near.position)
]
# Ensure we only consider naval control points if allow_naval is True
candidates = [
self.theater.control_point_named(z.cp_name) for z in zones_containing_point
]
if not allow_naval:
candidates = [
cp
for cp in candidates
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
if candidates:
closest = min(
candidates, key=lambda cp: cp.position.distance_to_point(near.position)
)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance
# If no zones contain the point, find the closest control point without an influence radius
if not allow_naval:
fallback_candidates = [
cp
for cp in self.theater.controlpoints
if cp.cptype
not in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
]
]
else:
fallback_candidates = self.theater.controlpoints
fallback_candidates = [
cp for cp in fallback_candidates if not cp.influence_radius
]
if not fallback_candidates:
raise RuntimeError(
f"All control points have an influence zone but no zones contain {near} at {near.position}"
)
closest = min(
fallback_candidates,
key=lambda cp: cp.position.distance_to_point(near.position),
)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance

View File

@@ -17,6 +17,7 @@ from game.procurement import AircraftProcurementRequest, ProcurementAi
from game.profiling import MultiEventTracer, logged_duration
from game.squadrons import AirWing
from game.theater.bullseye import Bullseye
from game.theater.player import Player
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from game.threatzones import ThreatZones
from game.transfers import PendingTransfers
@@ -32,7 +33,7 @@ if TYPE_CHECKING:
class Coalition:
def __init__(
self, game: Game, faction: Faction, budget: float, player: bool
self, game: Game, faction: Faction, budget: float, player: Player
) -> None:
self.game = game
self.player = player
@@ -68,9 +69,11 @@ class Coalition:
@property
def coalition_id(self) -> int:
if self.player:
if self.player.is_blue:
return 2
return 1
elif self.player.is_red:
return 1
return 0
@property
def opponent(self) -> Coalition:
@@ -95,12 +98,23 @@ class Coalition:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility.
del state["_threat_zone"]
del state["_navmesh"]
del state["faker"]
# TODO: Figure out why this is needed after adding neutral point support
if state["player"] != Player.NEUTRAL:
del state["_threat_zone"]
del state["_navmesh"]
return state
def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
@@ -203,7 +217,7 @@ class Coalition:
squadron.refund_orders()
def plan_missions(self, now: datetime) -> None:
color = "Blue" if self.player else "Red"
color = "Blue" if self.player.is_blue else "Red"
with MultiEventTracer() as tracer:
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
@@ -220,7 +234,7 @@ class Coalition:
# to ground forces and 1400 to aircraft. After that the budget will be spent
# proportionally based on how much is already invested.
if self.player:
if self.player.is_blue:
manage_runways = self.game.settings.automate_runway_repair
manage_front_line = self.game.settings.automate_front_line_reinforcements
manage_aircraft = self.game.settings.automate_aircraft_reinforcements

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import itertools
import math
import operator
from collections.abc import Iterable, Iterator
@@ -16,6 +17,7 @@ from game.theater import (
OffMapSpawn,
ParkingType,
NavalControlPoint,
Player,
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
@@ -35,7 +37,7 @@ MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
class ObjectiveFinder:
"""Identifies potential objectives for the mission planner."""
def __init__(self, game: Game, is_player: bool) -> None:
def __init__(self, game: Game, is_player: Player) -> None:
self.game = game
self.is_player = is_player
@@ -154,7 +156,7 @@ class ObjectiveFinder:
airfields_in_proximity = self.closest_airfields_to(cp)
airbase_threat_range = self.game.settings.airbase_threat_range
if (
not self.is_player
self.is_player.is_red
and randint(1, 100)
> self.game.settings.opfor_autoplanner_aggressiveness
):
@@ -216,7 +218,7 @@ class ObjectiveFinder:
def farthest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is farthest from any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
farthest = None
max_distance = meters(0)
@@ -234,7 +236,7 @@ class ObjectiveFinder:
def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
threat_zones = self.game.threat_zone_for(self.is_player.opponent)
closest = None
min_distance = meters(math.inf)
@@ -258,14 +260,37 @@ class ObjectiveFinder:
return (
c
for c in self.game.theater.controlpoints
if not c.is_friendly(self.is_player)
if not c.is_friendly(self.is_player) and c.captured != Player.NEUTRAL
)
def prioritized_points(self) -> list[ControlPoint]:
prioritized = []
capturable_later = []
isolated = []
for cp in self.game.theater.control_points_for(not self.is_player):
for cp in self.game.theater.control_points_for(self.is_player.opponent):
if cp.is_isolated:
isolated.append(cp)
continue
if cp.has_active_frontline:
prioritized.append(cp)
else:
capturable_later.append(cp)
prioritized.extend(self._targets_by_range(capturable_later))
prioritized.extend(self._targets_by_range(isolated))
return prioritized
def air_assault_targets(self) -> list[ControlPoint]:
"""Returns control points suitable for air assault missions, including neutral bases."""
prioritized = []
capturable_later = []
isolated = []
combined_control_points = itertools.chain(
self.game.theater.control_points_for(self.is_player.opponent),
self.game.theater.control_points_for(Player.NEUTRAL),
)
for cp in combined_control_points:
if cp.is_isolated:
isolated.append(cp)
continue

View File

@@ -43,7 +43,9 @@ class PackageFulfiller:
@property
def is_player(self) -> bool:
return self.coalition.player
if self.coalition.player.is_blue:
return True
return False
@property
def ato(self) -> AirTaskingOrder:

View File

@@ -10,7 +10,7 @@ from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
from game.theater import ControlPoint, FrontLine
from game.theater import ControlPoint, FrontLine, Player
@dataclass(frozen=True)
@@ -26,12 +26,12 @@ class CaptureBase(CompoundTask[TheaterState]):
def enemy_cp(self, state: TheaterState) -> ControlPoint:
return self.front_line.control_point_hostile_to(state.context.coalition.player)
def units_deployable(self, state: TheaterState, player: bool) -> int:
def units_deployable(self, state: TheaterState, player: Player) -> int:
cp = self.front_line.control_point_friendly_to(player)
ammo_depots = list(state.ammo_dumps_at(cp))
return cp.deployable_front_line_units_with(len(ammo_depots))
def unit_cap(self, state: TheaterState, player: bool) -> int:
def unit_cap(self, state: TheaterState, player: Player) -> int:
cp = self.front_line.control_point_friendly_to(player)
ammo_depots = list(state.ammo_dumps_at(cp))
return cp.front_line_capacity_with(len(ammo_depots))

View File

@@ -11,10 +11,11 @@ from game.theater import FrontLine
if TYPE_CHECKING:
from game.coalition import Coalition
from game.theater.player import Player
class FrontLineStanceTask(TheaterCommanderTask, ABC):
def __init__(self, front_line: FrontLine, player: bool) -> None:
def __init__(self, front_line: FrontLine, player: Player) -> None:
self.front_line = front_line
self.friendly_cp = self.front_line.control_point_friendly_to(player)
self.enemy_cp = self.front_line.control_point_hostile_to(player)
@@ -26,7 +27,7 @@ class FrontLineStanceTask(TheaterCommanderTask, ABC):
@staticmethod
def management_allowed(state: TheaterState) -> bool:
return (
not state.context.coalition.player
not state.context.coalition.player.is_blue
or state.context.settings.automate_front_line_stance
)

View File

@@ -45,7 +45,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
state.context.coalition.player.is_blue
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
):
return False
@@ -102,7 +102,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
return 1
def fulfill_mission(self, state: TheaterState) -> bool:
color = "blue" if state.context.coalition.player else "red"
color = "blue" if state.context.coalition.player.is_blue else "red"
self.propose_flights()
fulfiller = PackageFulfiller(
state.context.coalition,
@@ -214,7 +214,7 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
settings = state.context.coalition.game.settings
margin = 100 - (
settings.ownfor_autoplanner_aggressiveness
if state.context.coalition.player
if state.context.coalition.player.is_blue
else settings.opfor_autoplanner_aggressiveness
)
threat_range = iads_threat.max_threat_range() * (margin / 100)

View File

@@ -12,7 +12,7 @@ from game.theater import MissionTarget
class PlanAewc(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
state.context.coalition.player.is_blue
and not state.context.settings.auto_ato_behavior_awacs
):
return False

View File

@@ -6,7 +6,7 @@ from game.ato.flighttype import FlightType
from game.commander.missionproposals import EscortType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import FrontLine
from game.theater import FrontLine, Player
@dataclass
@@ -19,9 +19,8 @@ class PlanCas(PackagePlanningTask[FrontLine]):
# An exception is made for turn zero since that's not being truly planned, but
# just to determine what missions should be planned on turn 1 (when there *will*
# be ground units) and what aircraft should be ordered.
enemy_cp = self.target.control_point_friendly_to(
player=not state.context.coalition.player
)
player = state.context.coalition.player.opponent
enemy_cp = self.target.control_point_friendly_to(player)
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
return False
return super().preconditions_met(state)

View File

@@ -14,7 +14,7 @@ MARGIN = 4 # assume 4 aircraft can land without refueling
class PlanRecovery(PackagePlanningTask[ControlPoint]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
state.context.coalition.player.is_blue
and not state.context.settings.auto_ato_behavior_tankers
):
return False

View File

@@ -12,7 +12,7 @@ from game.theater import MissionTarget
class PlanRefueling(PackagePlanningTask[MissionTarget]):
def preconditions_met(self, state: TheaterState) -> bool:
if (
state.context.coalition.player
state.context.coalition.player.is_blue
and not state.context.settings.auto_ato_behavior_tankers
):
return False

View File

@@ -56,7 +56,7 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from game.ato.starttype import StartType
from game.commander.tasks.compound.nextaction import PlanNextAction
@@ -67,10 +67,11 @@ from game.profiling import MultiEventTracer
if TYPE_CHECKING:
from game import Game
from game.theater.player import Player
class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
def __init__(self, game: Game, player: bool) -> None:
def __init__(self, game: Game, player: Player) -> None:
super().__init__(
PlanNextAction(
aircraft_cold_start=game.settings.default_start_type is StartType.COLD
@@ -79,6 +80,18 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
self.game = game
self.player = player
def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state)
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, now, tracer)
while True:

View File

@@ -20,6 +20,7 @@ from game.theater import (
ControlPoint,
FrontLine,
MissionTarget,
Player,
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
@@ -152,11 +153,12 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod
def from_game(
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
cls, game: Game, player: Player, now: datetime, tracer: MultiEventTracer
) -> TheaterState:
coalition = game.coalition_for(player)
finder = ObjectiveFinder(game, player)
ordered_capturable_points = finder.prioritized_points()
air_assault_capturable_points = finder.air_assault_targets()
context = PersistentContext(
game.db,
@@ -176,7 +178,7 @@ class TheaterState(WorldState["TheaterState"]):
battle_postitions: Dict[ControlPoint, BattlePositions] = {
cp: BattlePositions.for_control_point(cp)
for cp in ordered_capturable_points
for cp in air_assault_capturable_points
}
vulnerable_control_points = [
@@ -213,8 +215,8 @@ class TheaterState(WorldState["TheaterState"]):
)
),
strike_targets=list(finder.strike_targets()),
enemy_barcaps=list(game.theater.control_points_for(not player)),
threat_zones=game.threat_zone_for(not player),
enemy_barcaps=list(game.theater.control_points_for(player.opponent)),
threat_zones=game.threat_zone_for(player.opponent),
vulnerable_control_points=vulnerable_control_points,
control_point_priority_queue=ordered_capturable_points,
priority_cp=(

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
from typing import Iterable, List, TYPE_CHECKING
from functools import cached_property
from dcs.mapping import Polygon
from dcs.triggers import TriggerZone, TriggerZoneCircular, TriggerZoneQuadPoint
from game.theater.theatergroundobject import NAME_BY_CATEGORY
if TYPE_CHECKING:
from dcs.mapping import Point
class ControlPointInfluenceRadiusError(RuntimeError):
"""Error for when there are insufficient conditions to create a ControlPointInfluenceRadius."""
pass
class ControlPointInfluenceRadius:
"""Store information about a scenery objective."""
def __init__(self, zone_def: TriggerZone, cp_name: str) -> None:
self.zone_def = zone_def
self.position = zone_def.position
self.cp_name = cp_name
@staticmethod
def from_trigger_zones(
trigger_zones: Iterable[TriggerZone],
) -> List[ControlPointInfluenceRadius]:
"""Define scenery objectives based on their encompassing blue/red circle."""
zone_definitions = []
cp_influence_zones = []
for zone in trigger_zones:
if ControlPointInfluenceRadius.is_red(zone):
zone_definitions.append(zone)
# For each objective definition.
for zone_def in zone_definitions:
if len(zone_def.properties) == 0:
raise ControlPointInfluenceRadiusError(
"Undefined ControlPointInfluenceRadius category in TriggerZone: "
+ zone_def.name
)
zone_def_cp_name = zone_def.properties[1].get("value")
cp_influence_zones.append(
ControlPointInfluenceRadius(zone_def, zone_def_cp_name)
)
return cp_influence_zones
@staticmethod
def is_red(zone: TriggerZone) -> bool:
# Red in RGB is [1 Red], [0 Green], [0 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 1 and zone.color[2] == 0 and zone.color[3] == 0
def point_in_zone(zone: TriggerZone, pos: Point) -> bool:
if isinstance(zone, TriggerZoneCircular):
return zone.position.distance_to_point(pos) < zone.radius
elif isinstance(zone, TriggerZoneQuadPoint):
return Polygon(pos._terrain, zone.verticies).point_in_poly(pos)
raise RuntimeError(f"Invalid trigger-zone: {zone.name}")

View File

@@ -6,6 +6,7 @@ REQUIRED_BUILDINGS = [
"ammo",
"factory",
"fob",
"invisiblefob",
"oil",
]

View File

@@ -60,6 +60,7 @@ class GroupTask(Enum):
FARP = ("Farp", GroupRole.BUILDING)
FOB = ("FOB", GroupRole.BUILDING)
FUEL = ("Fuel", GroupRole.BUILDING)
INVISIBLE_FOB = ("InvisibleFOB", GroupRole.BUILDING)
OFFSHORE_STRIKE_TARGET = ("OffShoreStrikeTarget", GroupRole.BUILDING)
OIL = ("Oil", GroupRole.BUILDING)

View File

@@ -123,6 +123,7 @@ class WeaponType(Enum):
TGP = "TGP"
DECOY = "DECOY"
JAMMER = "JAMMER"
OFFENSIVE_JAMMER = "OFFENSIVE_JAMMER"
UNKNOWN = "unknown"

View File

@@ -41,6 +41,9 @@ from game.radio.channels import (
WarthogChannelNamer,
PhantomChannelNamer,
KiowaChannelNamer,
ARC5RadioChannelAllocator,
ARC5ChannelNamer,
FulcrumChannelNamer,
)
from game.utils import (
Distance,
@@ -94,6 +97,7 @@ class RadioConfig:
return None
allocator_type: Type[RadioChannelAllocator] = {
"SCR-522": SCR522RadioChannelAllocator,
"ARC-5": ARC5RadioChannelAllocator,
"common": CommonRadioChannelAllocator,
"farmer": FarmerRadioChannelAllocator,
"noop": NoOpChannelAllocator,
@@ -105,6 +109,7 @@ class RadioConfig:
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
return {
"SCR-522": SCR522ChannelNamer,
"ARC-5": ARC5ChannelNamer,
"default": ChannelNamer,
"huey": HueyChannelNamer,
"mirage": MirageChannelNamer,
@@ -118,6 +123,7 @@ class RadioConfig:
"a10c-ii": WarthogChannelNamer,
"phantom": PhantomChannelNamer,
"kiowa": KiowaChannelNamer,
"fulcrum": FulcrumChannelNamer,
}[config.get("namer", "default")]
@@ -229,6 +235,14 @@ class AircraftType(UnitType[Type[FlyingType]]):
# when no TGP is mounted on any station.
has_built_in_target_pod: bool
# indicates if the aircraft has a built-in jammer allowing Defensive EWJamming to be used
# without the need for a jamming pod
has_built_in_ecm: bool
# Indicates if the aircraft has a built in jammer allowing Offensive EWJamming to be used
# without the need for a jamming pod
has_built_in_jamming: bool
task_priorities: dict[FlightType, int]
laser_code_configs: list[LaserCodeConfig]
@@ -591,6 +605,8 @@ class AircraftType(UnitType[Type[FlyingType]]):
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
task_priorities=task_priorities,
has_built_in_target_pod=data.get("has_built_in_target_pod", False),
has_built_in_ecm=data.get("has_built_in_ecm", False),
has_built_in_jamming=data.get("has_built_in_jamming", False),
laser_code_configs=[
LaserCodeConfig.from_yaml(d) for d in data.get("laser_codes", [])
],

View File

@@ -16,7 +16,7 @@ from uuid import UUID
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint
from game.theater import Airfield, ControlPoint, Player
if TYPE_CHECKING:
from game import Game
@@ -45,9 +45,9 @@ class AirLosses:
def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[AircraftType, int]:
def by_type(self, player: Player) -> Dict[AircraftType, int]:
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
losses = self.player if player else self.enemy
losses = self.player if player.is_blue else self.enemy
for loss in losses:
losses_by_type[loss.flight.unit_type] += 1
return losses_by_type
@@ -87,7 +87,7 @@ class GroundLosses:
@dataclass(frozen=True)
class BaseCaptureEvent:
control_point: ControlPoint
captured_by_player: bool
captured_by_player: Player
@dataclass(frozen=True)
@@ -166,7 +166,7 @@ class Debriefing:
def merge_simulation_results(self, results: SimulationResults) -> None:
for air_loss in results.air_losses:
if air_loss.flight.squadron.player:
if air_loss.flight.squadron.player.is_blue:
self.air_losses.player.append(air_loss)
else:
self.air_losses.enemy.append(air_loss)
@@ -209,9 +209,9 @@ class Debriefing:
def casualty_count(self, control_point: ControlPoint) -> int:
return len([x for x in self.front_line_losses if x.origin == control_point])
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
def front_line_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
if player.is_blue:
losses = self.ground_losses.player_front_line
else:
losses = self.ground_losses.enemy_front_line
@@ -219,9 +219,9 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
def convoy_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
if player.is_blue:
losses = self.ground_losses.player_convoy
else:
losses = self.ground_losses.enemy_convoy
@@ -229,9 +229,9 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
def cargo_ship_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
if player.is_blue:
ships = self.ground_losses.player_cargo_ships
else:
ships = self.ground_losses.enemy_cargo_ships
@@ -240,9 +240,9 @@ class Debriefing:
losses_by_type[unit_type] += count
return losses_by_type
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
def airlift_losses_by_type(self, player: Player) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
if player.is_blue:
losses = self.ground_losses.player_airlifts
else:
losses = self.ground_losses.enemy_airlifts
@@ -251,9 +251,9 @@ class Debriefing:
losses_by_type[unit_type] += 1
return losses_by_type
def ground_object_losses_by_type(self, player: bool) -> Dict[str, int]:
def ground_object_losses_by_type(self, player: Player) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
if player.is_blue:
losses = self.ground_losses.player_ground_objects
else:
losses = self.ground_losses.enemy_ground_objects
@@ -261,9 +261,9 @@ class Debriefing:
losses_by_type[loss.theater_unit.type.id] += 1
return losses_by_type
def scenery_losses_by_type(self, player: bool) -> Dict[str, int]:
def scenery_losses_by_type(self, player: Player) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
if player.is_blue:
losses = self.ground_losses.player_scenery
else:
losses = self.ground_losses.enemy_scenery
@@ -279,7 +279,7 @@ class Debriefing:
if aircraft is None:
logging.error(f"Could not find Flight matching {unit_name}")
continue
if aircraft.flight.departure.captured:
if aircraft.flight.departure.captured.is_blue:
player_losses.append(aircraft)
else:
enemy_losses.append(aircraft)
@@ -290,7 +290,7 @@ class Debriefing:
for unit_name in self.state_data.killed_ground_units:
front_line_unit = self.unit_map.front_line_unit(unit_name)
if front_line_unit is not None:
if front_line_unit.origin.captured:
if front_line_unit.origin.captured.is_blue:
losses.player_front_line.append(front_line_unit)
else:
losses.enemy_front_line.append(front_line_unit)
@@ -298,7 +298,7 @@ class Debriefing:
convoy_unit = self.unit_map.convoy_unit(unit_name)
if convoy_unit is not None:
if convoy_unit.convoy.player_owned:
if convoy_unit.convoy.player_owned.is_blue:
losses.player_convoy.append(convoy_unit)
else:
losses.enemy_convoy.append(convoy_unit)
@@ -306,7 +306,7 @@ class Debriefing:
cargo_ship = self.unit_map.cargo_ship(unit_name)
if cargo_ship is not None:
if cargo_ship.player_owned:
if cargo_ship.player_owned.is_blue:
losses.player_cargo_ships.append(cargo_ship)
else:
losses.enemy_cargo_ships.append(cargo_ship)
@@ -314,7 +314,9 @@ class Debriefing:
ground_object = self.unit_map.theater_units(unit_name)
if ground_object is not None:
if ground_object.theater_unit.ground_object.is_friendly(to_player=True):
if ground_object.theater_unit.ground_object.is_friendly(
to_player=Player.BLUE
):
losses.player_ground_objects.append(ground_object)
else:
losses.enemy_ground_objects.append(ground_object)
@@ -323,7 +325,9 @@ class Debriefing:
scenery_object = self.unit_map.scenery_object(unit_name)
# Try appending object to the name, because we do this for building statics.
if scenery_object is not None:
if scenery_object.ground_unit.ground_object.is_friendly(to_player=True):
if scenery_object.ground_unit.ground_object.is_friendly(
to_player=Player.BLUE
):
losses.player_scenery.append(scenery_object)
else:
losses.enemy_scenery.append(scenery_object)
@@ -331,9 +335,9 @@ class Debriefing:
airfield = self.unit_map.airfield(unit_name)
if airfield is not None:
if airfield.captured:
if airfield.captured.is_blue:
losses.player_airfields.append(airfield)
else:
elif airfield.captured.is_red:
losses.enemy_airfields.append(airfield)
continue
@@ -349,7 +353,7 @@ class Debriefing:
for unit_name in self.state_data.killed_aircraft:
airlift_unit = self.unit_map.airlift_unit(unit_name)
if airlift_unit is not None:
if airlift_unit.transfer.player:
if airlift_unit.transfer.player.is_blue:
losses.player_airlifts.append(airlift_unit)
else:
losses.enemy_airlifts.append(airlift_unit)
@@ -377,8 +381,10 @@ class Debriefing:
# Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do.
continue
captured_by_player = int(new_owner_id_str) == blue_coalition_id
if int(new_owner_id_str) == blue_coalition_id:
captured_by_player = Player.BLUE
else:
captured_by_player = Player.RED
if control_point.is_friendly(to_player=captured_by_player):
# Base is currently friendly to the new owner. Was captured and
# recaptured in the same mission. Nothing to do.

View File

@@ -865,6 +865,19 @@ class Faction:
# MiG-31BM
if not mod_settings.mig31bm_foxhound:
self.remove_aircraft("MiG-31BM")
# Mlitary Aircraft Mod 1.7.1
if not mod_settings.mam:
self.remove_aircraft("A400M_Atlas")
self.remove_aircraft("B2_Spirit")
self.remove_aircraft("C2A_Greyhound")
self.remove_aircraft("C5_Galaxy")
self.remove_aircraft("KC_10_Extender")
self.remove_aircraft("KC_10_Extender_D")
self.remove_aircraft("P3C_Orion")
self.remove_aircraft("V22_Osprey")
# Tornado f.3 ADV (1.0)
if not mod_settings.tornado_adv:
self.remove_aircraft("Tornado_ADV")
def remove_aircraft(self, name: str) -> None:
for aircraft_set in [self.aircraft, self.awacs, self.tankers]:

View File

@@ -4,9 +4,10 @@ import itertools
import logging
import math
from collections.abc import Iterator
from copy import deepcopy
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from typing import Any, List, TYPE_CHECKING, Union, cast
from uuid import UUID
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
@@ -32,7 +33,7 @@ from .infos.information import Information
from .lasercodes.lasercoderegistry import LaserCodeRegistry
from .profiling import logged_duration
from .settings import Settings
from .theater import ConflictTheater
from .theater import ConflictTheater, Player
from .theater.bullseye import Bullseye
from .theater.theatergroundobject import (
EwrGroundObject,
@@ -138,8 +139,11 @@ class Game:
self.conditions = self.generate_conditions(forced_time=start_time)
self.sanitize_sides(player_faction, enemy_faction)
self.blue = Coalition(self, player_faction, player_budget, player=True)
self.red = Coalition(self, enemy_faction, enemy_budget, player=False)
self.blue = Coalition(self, player_faction, player_budget, player=Player.BLUE)
self.red = Coalition(self, enemy_faction, enemy_budget, player=Player.RED)
neutral_faction = deepcopy(player_faction)
neutral_faction.country = self.neutral_country
self.neutral = Coalition(self, neutral_faction, 0, player=Player.NEUTRAL)
self.blue.set_opponent(self.red)
self.red.set_opponent(self.blue)
@@ -178,10 +182,10 @@ class Game:
def point_in_world(self, x: float, y: float) -> Point:
return Point(x, y, self.theater.terrain)
def ato_for(self, player: bool) -> AirTaskingOrder:
def ato_for(self, player: Player) -> AirTaskingOrder:
return self.coalition_for(player).ato
def transit_network_for(self, player: bool) -> TransitNetwork:
def transit_network_for(self, player: Player) -> TransitNetwork:
return self.coalition_for(player).transit_network
def generate_conditions(self, forced_time: time | None = None) -> Conditions:
@@ -209,32 +213,35 @@ class Game:
else:
enemy_faction.country = country_with_name("Russia")
def faction_for(self, player: bool) -> Faction:
def faction_for(self, player: Player) -> Faction:
return self.coalition_for(player).faction
def faker_for(self, player: bool) -> Faker:
def faker_for(self, player: Player) -> Faker:
return self.coalition_for(player).faker
def air_wing_for(self, player: bool) -> AirWing:
def air_wing_for(self, player: Player) -> AirWing:
return self.coalition_for(player).air_wing
@property
def neutral_country(self) -> Type[Country]:
def neutral_country(self) -> Country:
"""Return the best fitting country that can be used as neutral faction in the generated mission"""
countries_in_use = {self.red.faction.country, self.blue.faction.country}
if UnitedNationsPeacekeepers not in countries_in_use:
return UnitedNationsPeacekeepers
elif Switzerland.name not in countries_in_use:
return Switzerland
if UnitedNationsPeacekeepers() not in countries_in_use:
return UnitedNationsPeacekeepers()
elif Switzerland() not in countries_in_use:
return Switzerland()
else:
return USAFAggressors
return USAFAggressors()
def coalition_for(self, player: bool) -> Coalition:
if player:
def coalition_for(self, player: Player) -> Coalition:
if player.is_neutral:
return self.neutral
elif player.is_blue:
return self.blue
return self.red
else:
return self.red
def adjust_budget(self, amount: float, player: bool) -> None:
def adjust_budget(self, amount: float, player: Player) -> None:
self.coalition_for(player).adjust_budget(amount)
def on_load(self, game_still_initializing: bool = False) -> None:
@@ -489,7 +496,7 @@ class Game:
self.current_group_id += 1
return self.current_group_id
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
def compute_transit_network_for(self, player: Player) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self, events: GameUpdateEvents) -> None:
@@ -498,10 +505,10 @@ class Game:
self.blue.compute_nav_meshes(events)
self.red.compute_nav_meshes(events)
def threat_zone_for(self, player: bool) -> ThreatZones:
def threat_zone_for(self, player: Player) -> ThreatZones:
return self.coalition_for(player).threat_zone
def navmesh_for(self, player: bool) -> NavMesh:
def navmesh_for(self, player: Player) -> NavMesh:
return self.coalition_for(player).nav_mesh
def compute_unculled_zones(self, events: GameUpdateEvents) -> None:

View File

@@ -66,7 +66,7 @@ class GroundUnitOrders:
bought_units: dict[GroundUnitType, int] = {}
units_needing_transfer: dict[GroundUnitType, int] = {}
for unit_type, count in self.units.items():
allegiance = "Ally" if self.destination.captured else "Enemy"
allegiance = "Ally" if self.destination.captured.is_blue else "Enemy"
d: dict[GroundUnitType, int]
if self.destination != ground_unit_source:
source = ground_unit_source

View File

@@ -4,6 +4,7 @@ from dataclasses import dataclass
from typing import TYPE_CHECKING
from game.config import REWARDS
from game.theater.player import Player
if TYPE_CHECKING:
from game import Game
@@ -22,8 +23,8 @@ class BuildingIncome:
class Income:
def __init__(self, game: Game, player: bool) -> None:
if player:
def __init__(self, game: Game, player: Player) -> None:
if player.is_blue:
self.multiplier = game.settings.player_income_multiplier
else:
self.multiplier = game.settings.enemy_income_multiplier

View File

@@ -288,6 +288,8 @@ class BuildingLayout(TgoLayout):
@property
def category(self) -> str:
for task in self.tasks:
if task is GroupTask.INVISIBLE_FOB:
return "fob"
if task not in [GroupTask.STRIKE_TARGET, GroupTask.OFFSHORE_STRIKE_TARGET]:
return task.description.lower()
raise RuntimeError(f"Building Template {self.name} has no building category")

View File

@@ -192,7 +192,8 @@ class AircraftBehavior:
@staticmethod
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
if flight.unit_type.eplrs_capable:
eplrs_enabled = flight.coalition.game.settings.eplrs_enabled
if eplrs_enabled and flight.unit_type.eplrs_capable:
group.points[0].tasks.append(EPLRS(group.id))
def configure_cap(self, group: FlyingGroup[Any], flight: Flight) -> None:
@@ -306,7 +307,7 @@ class AircraftBehavior:
)
def configure_oca_strike(self, group: FlyingGroup[Any], flight: Flight) -> None:
self.configure_task(flight, group, CAS, [AFAC, SEAD])
self.configure_task(flight, group, CAS, [AFAC, SEAD, AntishipStrike])
self.configure_behavior(
flight,
group,

View File

@@ -13,6 +13,7 @@ from dcs.datalinks.datalink import DataLinkType
from dcs.datalinks.datalinkbase import DataLinkSettingsWithFlightLead
from dcs.datalinks.link16 import Link16Network, ViperLink16NetworkMemberLink
from dcs.mission import Mission
from dcs.task import RunScript
from dcs.terrain.terrain import NoParkingSlotError
from dcs.triggers import TriggerOnce, Event
from dcs.unit import Skill
@@ -103,6 +104,23 @@ class AircraftGenerator:
for parking_slot in cp.parking_slots:
parking_slot.unit_id = None
def _prioritized_packages(self, ato: AirTaskingOrder) -> List[Package]:
"""Returns the packages in the order they should be generated."""
return sorted(
ato.packages,
key=lambda p: (
(
1
if any(
f.flight_type in [FlightType.AEWC, FlightType.REFUELING]
for f in p.flights
)
else 0
),
p.time_over_target,
),
)
def generate_flights(
self,
country: Country,
@@ -123,7 +141,7 @@ class AircraftGenerator:
self._reserve_frequencies_and_tacan(ato)
self.mission_data.packages.clear()
for package in reversed(sorted(ato.packages, key=lambda x: x.time_over_target)):
for package in reversed(self._prioritized_packages(ato)):
logging.info(f"Generating package for target: {package.target.name}")
if not package.flights:
continue
@@ -202,7 +220,7 @@ class AircraftGenerator:
):
continue
if control_point.captured:
if control_point.captured.is_blue:
country = player_country
else:
country = enemy_country
@@ -219,12 +237,12 @@ class AircraftGenerator:
squadron.location, Fob
)
if (
squadron.coalition.player
squadron.coalition.player.is_blue
and self.game.settings.perf_disable_untasked_blufor_aircraft
):
return
elif (
not squadron.coalition.player
squadron.coalition.player.is_red
and self.game.settings.perf_disable_untasked_opfor_aircraft
):
return
@@ -256,7 +274,7 @@ class AircraftGenerator:
).create_idle_aircraft()
if group:
if (
not squadron.coalition.player
squadron.coalition.player.is_red
and squadron.aircraft.flyable
and (
self.game.settings.enable_squadron_pilot_limits
@@ -341,16 +359,51 @@ class AircraftGenerator:
def _track_ewrj_flight(self, flight: Flight, group: FlyingGroup[Any]) -> None:
if not self.ewrj_package_dict.get(id(flight.package)):
self.ewrj_package_dict[id(flight.package)] = []
added = False
if (
flight.package.primary_flight
and flight is flight.package.primary_flight
or flight.client_count
not flight.flight_type.is_air_to_air
and any(
[
wpt
for wpt in group.points
if wpt.name in ["JOIN", "SPLIT", "RACETRACK START", "RACETRACK END"]
and any(
[
t
for t in wpt.tasks
if isinstance(t, RunScript)
and (
"Djamming" in t.params["action"]["params"]["command"]
or "EWjamm" in t.params["action"]["params"]["command"]
)
]
)
]
)
and (
not self.need_ecm
or flight.any_member_has_weapon_of_type(WeaponType.JAMMER)
or flight.any_member_has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
or flight.squadron.aircraft.has_built_in_ecm
or flight.squadron.aircraft.has_built_in_jamming
)
):
self.ewrj_package_dict[id(flight.package)].append(group)
added = True
if (
added
or not flight.flight_type.is_air_to_air
and self.ewrj_package_dict[id(flight.package)]
):
for f in flight.package.flights:
if f is flight or f.group_id == 0 or f.flight_type.is_air_to_air:
continue
g = self.mission.find_group_by_id(f.group_id)
if (
isinstance(g, FlyingGroup)
and g not in self.ewrj_package_dict[id(flight.package)]
):
self.ewrj_package_dict[id(flight.package)].append(g)
def _reserve_frequencies_and_tacan(self, ato: AirTaskingOrder) -> None:
for package in ato.packages:

View File

@@ -14,6 +14,7 @@ if TYPE_CHECKING:
from game.dcs.aircrafttype import AircraftType
from game.radio.radios import RadioFrequency
from game.runways import RunwayData
from game.theater.player import Player
@dataclass(frozen=True)
@@ -42,7 +43,7 @@ class FlightData:
size: int
#: True if this flight belongs to the player's coalition.
friendly: bool
friendly: Player
#: Number of seconds after mission start the flight is set to depart.
departure_delay: timedelta

View File

@@ -185,10 +185,21 @@ class FlightGroupConfigurator:
if not member.is_player:
return
settings = self.flight.coalition.game.settings
if not settings.plugin_option("ewrj"):
return
# Check if ecm_required option is enabled
jammer_required = settings.plugin_option("ewrj.ecm_required")
has_jammer = member.loadout.has_weapon_of_type(WeaponType.JAMMER)
if jammer_required and not has_jammer:
offensive_jammer = member.loadout.has_weapon_of_type(
WeaponType.OFFENSIVE_JAMMER
)
offensive_inbuilt = self.flight.squadron.aircraft.has_built_in_jamming
has_jammer = (
member.loadout.has_weapon_of_type(WeaponType.JAMMER) or offensive_jammer
)
built_in_jammer = (
self.flight.squadron.aircraft.has_built_in_ecm or offensive_inbuilt
)
if jammer_required and not (has_jammer or built_in_jammer):
return
# Create the original ewrj_menu_trigger for player flight members
ewrj_menu_trigger = TriggerStart(comment=f"EWRJ-{unit.name}")
@@ -197,6 +208,20 @@ class FlightGroupConfigurator:
self.group.points[0].tasks[0] = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense
)
# Create LUA Flags for Offensive Jamming in EW Script for Player Flights
if not (offensive_jammer or offensive_inbuilt):
return
ewrj_offensive_trigger = TriggerStart(
comment=f"Offensive Jammer Flag {unit.name}"
)
ewrj_offensive_trigger.add_action(
DoScript(
String(
f'trigger.action.setUserFlag("offensive_jamming_{unit.name}", 1)'
)
)
)
self.mission.triggerrules.triggers.append(ewrj_offensive_trigger)
def setup_radios(self) -> RadioFrequency:
freq = self.flight.frequency
@@ -274,7 +299,7 @@ class FlightGroupConfigurator:
unit.set_player()
def skill_level_for(self, unit: FlyingUnit, pilot: Optional[Pilot]) -> Skill:
if self.flight.squadron.player:
if self.flight.squadron.player.is_blue:
base_skill = Skill(self.game.settings.player_skill)
else:
base_skill = Skill(self.game.settings.enemy_skill)

View File

@@ -9,13 +9,10 @@ from dcs.task import (
OptFormation,
Targets,
SetUnlimitedFuelCommand,
RunScript,
OptReactOnThreat,
)
from game.ato import FlightType
from game.data.doctrine import Doctrine
from game.data.weapons import WeaponType
from game.theater import NavalControlPoint
from game.utils import nautical_miles, feet
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -63,8 +60,14 @@ class JoinPointBuilder(PydcsWaypointBuilder):
FlightType.SEAD_SWEEP,
FlightType.SEAD,
FlightType.SEAD_ESCORT,
FlightType.DEAD,
]:
self.start_defensive_jamming(waypoint)
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
self.offensive_jamming(waypoint, "start")
self.defensive_jamming(waypoint, "start")
if self.flight.flight_type == FlightType.SEAD_ESCORT:
self.handle_sead_escort(doctrine, waypoint)
# Let the AI use ECM to preemptively defend themselves.
@@ -104,28 +107,6 @@ class JoinPointBuilder(PydcsWaypointBuilder):
vertical_spacing=doctrine.sead_escort_spacing.feet,
)
def start_defensive_jamming(self, waypoint: MovingPoint) -> None:
# Start Defensive Jamming
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
ecm_required = settings.plugin_option("ewrj.ecm_required")
has_jammer = False
for unit, member in zip(self.group.units, self.flight.iter_members()):
has_jammer = member.loadout.has_weapon_of_type(WeaponType.JAMMER)
if ecm_required and not has_jammer:
continue
if not member.is_player:
script_content = f'startDjamming("{unit.name}")'
start_jamming_script = RunScript(script_content)
waypoint.tasks.append(start_jamming_script)
has_jammer = True
if has_jammer:
passive_defense = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense
)
waypoint.tasks.append(passive_defense)
def configure_escort_tasks(
self,
waypoint: MovingPoint,

View File

@@ -13,6 +13,7 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.ato.traveltime import GroundSpeed
from game.data.weapons import WeaponType
from game.missiongenerator.missiondata import MissionData
from game.theater import MissionTarget, TheaterUnit, OffMapSpawn
@@ -157,3 +158,36 @@ class PydcsWaypointBuilder:
self.group.units[0].unit_type in (F_14A_135_GR, F_14B)
):
self.group.add_nav_target_point(self.waypoint.position, "IP")
def defensive_jamming(self, waypoint: MovingPoint, action: str) -> None:
# Explodes incoming missiles within the jamming bubble through the EW-Jamming script
settings = self.flight.coalition.game.settings
ecm_required = settings.plugin_option("ewrj.ecm_required")
for unit, member in zip(self.group.units, self.flight.iter_members()):
has_jammer = member.loadout.has_weapon_of_type(
WeaponType.JAMMER
) or member.loadout.has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
built_in_jammer = (
self.flight.squadron.aircraft.has_built_in_ecm
or self.flight.squadron.aircraft.has_built_in_jamming
)
if ecm_required and not (has_jammer or built_in_jammer):
continue
if not member.is_player:
script_content = f'{action}IAdefjamming("{unit.name}")'
jamming_script = RunScript(script_content)
waypoint.tasks.append(jamming_script)
def offensive_jamming(self, waypoint: MovingPoint, action: str) -> None:
# Silences enemy radars through the EW-Jamming script
settings = self.flight.coalition.game.settings
ecm_required = settings.plugin_option("ewrj.ecm_required")
for unit, member in zip(self.group.units, self.flight.iter_members()):
has_jammer = member.loadout.has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
built_in_jammer = self.flight.squadron.aircraft.has_built_in_jamming
if ecm_required and not (has_jammer or built_in_jammer):
continue
if not member.is_player:
script_content = f'{action}EWjamm("{unit.name}")'
stop_jamming_script = RunScript(script_content)
waypoint.tasks.append(stop_jamming_script)

View File

@@ -9,8 +9,6 @@ from dcs.task import (
Tanker,
Targets,
SetUnlimitedFuelCommand,
RunScript,
OptReactOnThreat,
)
from game.ato import FlightType
@@ -40,21 +38,12 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
)
return
if self.flight.flight_type == FlightType.AEWC:
# Start Offensive Jamming for all AWACS flights
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
# all units in group are AWACS, no specific checks needed
for unit, member in zip(self.group.units, self.flight.iter_members()):
script_content = f'startEWjamm("{unit.name}")'
start_jamming_script = RunScript(script_content)
waypoint.tasks.append(start_jamming_script)
passive_defense = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense
)
waypoint.tasks.append(passive_defense)
# Start Defensive Jamming for all flights
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
self.defensive_jamming(waypoint, "start")
self.offensive_jamming(waypoint, "start")
# NB: It's important that the engage task comes before the orbit task.
# Though they're on the same waypoint, if the orbit task comes first it

View File

@@ -3,8 +3,6 @@ import logging
from dcs.point import MovingPoint
from dcs.task import (
SetUnlimitedFuelCommand,
RunScript,
OptReactOnThreat,
)
from game.ato import FlightType
@@ -20,18 +18,12 @@ class RaceTrackEndBuilder(PydcsWaypointBuilder):
# Disable Offensive Jamming at Racetrack End
if self.flight.flight_type == FlightType.AEWC:
# Stop Offensive Jamming for all AWACS flights
# Stop Defensive Jamming for all AWACS flights
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
# all units in group are AWACS, no specific checks needed
for unit, member in zip(self.group.units, self.flight.iter_members()):
script_content = f'stopEWjamming("{unit.name}")'
stop_jamming_script = RunScript(script_content)
waypoint.tasks.append(stop_jamming_script)
evade_fire = OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
waypoint.tasks.append(evade_fire)
self.defensive_jamming(waypoint, "stop")
self.offensive_jamming(waypoint, "stop")
def build(self) -> MovingPoint:
waypoint = super().build()

View File

@@ -62,6 +62,7 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
# into the SAM instead of waiting for it to come alive
engage_task = EngageGroup(miz_group.id)
engage_task.params["weaponType"] = DcsWeaponType.ARM.value
engage_task.params["expend"] = Expend.All.value
waypoint.tasks.append(engage_task)
# Use other Air-to-Surface Missiles at last

View File

@@ -5,11 +5,9 @@ from dcs.task import (
SetUnlimitedFuelCommand,
SwitchWaypoint,
RunScript,
OptReactOnThreat,
)
from game.ato import FlightType
from game.data.weapons import WeaponType
from game.utils import knots
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -49,28 +47,15 @@ class SplitPointBuilder(PydcsWaypointBuilder):
FlightType.SEAD_SWEEP,
FlightType.SEAD,
FlightType.SEAD_ESCORT,
FlightType.DEAD,
]:
if self.flight.flight_type == FlightType.SEAD_ESCORT:
# Moved previous escort split tasks
if self.flight.flight_type.is_escort_type:
index = len(self.group.points)
self.group.add_trigger_action(SwitchWaypoint(None, index))
self.stop_defensive_jamming(waypoint)
def stop_defensive_jamming(self, waypoint: MovingPoint) -> None:
# Stop Defensive Jamming
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
for unit, member in zip(self.group.units, self.flight.iter_members()):
if settings.plugin_option("ewrj.ecm_required"):
ecm = WeaponType.JAMMER
if not member.loadout.has_weapon_of_type(ecm):
continue
if not member.is_player:
script_content = f'stopDjamming("{unit.name}")'
stop_jamming_script = RunScript(script_content)
waypoint.tasks.append(stop_jamming_script)
evaide_fire = OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
waypoint.tasks.append(evaide_fire)
settings = self.flight.coalition.game.settings
ai_jammer = settings.plugin_option("ewrj.ai_jammer_enabled")
if settings.plugins.get("ewrj") and ai_jammer:
self.offensive_jamming(waypoint, "stop")
self.defensive_jamming(waypoint, "stop")

View File

@@ -17,8 +17,8 @@ from game.radio.radios import RadioFrequency
from game.runways import RunwayData
from game.theater import ControlPoint, FrontLine
from .aircraft.flightdata import FlightData
from .missiondata import AwacsInfo, TankerInfo
from .flotgenerator import JtacInfo
from .missiondata import AwacsInfo, TankerInfo
if TYPE_CHECKING:
from game import Game
@@ -182,7 +182,7 @@ class BriefingGenerator(MissionInfoGenerator):
def generate_allied_flights_by_departure(self) -> None:
"""Create iterable to display allied flights grouped by departure airfield."""
for flight in self.flights:
if not flight.client_units and flight.friendly:
if not flight.client_units and flight.friendly.is_blue:
name = flight.departure.airfield_name
if (
name in self.allied_flights_by_departure

View File

@@ -18,6 +18,7 @@ from game.utils import kph
if TYPE_CHECKING:
from game import Game
from game.theater.player import Player
class ConvoyGenerator:
@@ -94,7 +95,7 @@ class ConvoyGenerator:
name: str,
position: Point,
units: dict[GroundUnitType, int],
for_player: bool,
for_player: Player,
) -> VehicleGroup:
unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0]

View File

@@ -15,6 +15,7 @@ FRONTLINE_COLORS = Rgba(255, 0, 0, 255)
WHITE = Rgba(255, 255, 255, 255)
CP_RED = Rgba(255, 0, 0, 80)
CP_BLUE = Rgba(0, 0, 255, 80)
CP_NEUTRAL = Rgba(128, 128, 128, 80)
BLUE_PATH_COLOR = Rgba(0, 0, 255, 100)
RED_PATH_COLOR = Rgba(255, 0, 0, 100)
ACTIVE_PATH_COLOR = Rgba(255, 80, 80, 100)
@@ -35,10 +36,12 @@ class DrawingsGenerator:
Generate cps as circles
"""
for cp in self.game.theater.controlpoints:
if cp.captured:
if cp.captured.is_blue:
color = CP_BLUE
else:
elif cp.captured.is_red:
color = CP_RED
else:
color = CP_NEUTRAL
shape = self.player_layer.add_circle(
cp.position,
TRIGGER_RADIUS_CAPTURE,
@@ -61,9 +64,9 @@ class DrawingsGenerator:
continue
else:
# Determine path color
if cp.captured and destination.captured:
if cp.captured.is_blue and destination.captured.is_blue:
color = BLUE_PATH_COLOR
elif not cp.captured and not destination.captured:
elif cp.captured.is_red and destination.captured.is_red:
color = RED_PATH_COLOR
else:
color = ACTIVE_PATH_COLOR

View File

@@ -40,7 +40,7 @@ from game.ground_forces.ai_ground_planner import (
from game.ground_forces.combat_stance import CombatStance
from game.naming import namegen
from game.radio.radios import RadioRegistry
from game.theater.controlpoint import ControlPoint
from game.theater.controlpoint import ControlPoint, Player
from game.unitmap import UnitMap
from game.utils import Heading
from .frontlineconflictdescription import FrontLineConflictDescription
@@ -101,12 +101,12 @@ class FlotGenerator:
# Create player groups at random position
player_groups = self._generate_groups(
self.player_planned_combat_groups, is_player=True
self.player_planned_combat_groups, is_player=Player.BLUE
)
# Create enemy groups at random position
enemy_groups = self._generate_groups(
self.enemy_planned_combat_groups, is_player=False
self.enemy_planned_combat_groups, is_player=Player.RED
)
# TODO: Differentiate AirConflict and GroundConflict classes.
@@ -193,7 +193,7 @@ class FlotGenerator:
callsign=callsign,
region=frontline,
code=str(code),
blue=True,
blue=Player.BLUE,
freq=freq,
)
)
@@ -215,7 +215,7 @@ class FlotGenerator:
def gen_infantry_group_for_group(
self,
group: VehicleGroup,
is_player: bool,
is_player: Player,
side: Country,
forward_heading: Heading,
) -> None:
@@ -294,7 +294,7 @@ class FlotGenerator:
GroundForcePainter(faction, vehicle).apply_livery()
vg.hidden_on_mfd = True
def _earliest_tot_on_flot(self, player: bool) -> timedelta:
def _earliest_tot_on_flot(self, player: Player) -> timedelta:
tots = [
x.time_over_target
for x in self.game.ato_for(player).packages
@@ -413,7 +413,7 @@ class FlotGenerator:
"""
duration = timedelta()
if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]:
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent)
self._set_reform_waypoint(dcs_group, forward_heading, duration)
if stance == CombatStance.AGGRESSIVE:
# Attack nearest enemy if any
@@ -503,7 +503,7 @@ class FlotGenerator:
"""
duration = timedelta()
if stance in [CombatStance.DEFENSIVE, CombatStance.AGGRESSIVE]:
duration = self._earliest_tot_on_flot(not to_cp.coalition.player)
duration = self._earliest_tot_on_flot(to_cp.coalition.player.opponent)
self._set_reform_waypoint(dcs_group, forward_heading, duration)
if stance in [
CombatStance.AGGRESSIVE,
@@ -762,13 +762,15 @@ class FlotGenerator:
)
def _generate_groups(
self, groups: list[CombatGroup], is_player: bool
self, groups: list[CombatGroup], is_player: Player
) -> List[Tuple[VehicleGroup, CombatGroup]]:
"""Finds valid positions for planned groups and generates a pydcs group for them"""
positioned_groups = []
assert self.conflict.heading is not None
spawn_heading = (
self.conflict.heading.left if is_player else self.conflict.heading.right
self.conflict.heading.left
if is_player.is_blue
else self.conflict.heading.right
)
country = self.game.coalition_for(is_player).faction.country
country = self.mission.country(country.name)
@@ -795,7 +797,7 @@ class FlotGenerator:
final_position,
heading=spawn_heading.opposite,
)
if is_player:
if is_player == Player.BLUE:
g.set_skill(Skill(self.game.settings.player_skill))
else:
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
@@ -813,7 +815,7 @@ class FlotGenerator:
def _generate_group(
self,
player: bool,
player: Player,
side: Country,
unit_type: GroundUnitType,
count: int,

View File

@@ -10,15 +10,14 @@ from dcs import Mission
from dcs.action import DoScript, DoScriptFile
from dcs.translation import String
from dcs.triggers import TriggerStart
from dcs.unit import Skill
from game.ato import FlightType
from game.data.units import UnitClass
from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua
from game.data.units import UnitClass
from .missiondata import MissionData
if TYPE_CHECKING:
@@ -102,13 +101,15 @@ class LuaGenerator:
for logistic_info in self.mission_data.logistics:
if logistic_info.transport not in transports:
transports.append(logistic_info.transport)
coalition_color = "blue" if logistic_info.blue else "red"
coalition_color = "blue" if logistic_info.blue.is_blue else "red"
logistics_item = logistics_flights.add_item()
logistics_item.add_data_array("pilot_names", logistic_info.pilot_names)
logistics_item.add_key_value("pickup_zone", logistic_info.pickup_zone)
logistics_item.add_key_value("drop_off_zone", logistic_info.drop_off_zone)
logistics_item.add_key_value("target_zone", logistic_info.target_zone)
logistics_item.add_key_value("side", str(2 if logistic_info.blue else 1))
logistics_item.add_key_value(
"side", str(2 if logistic_info.blue.is_blue else 1)
)
logistics_item.add_key_value("logistic_unit", logistic_info.logistic_unit)
logistics_item.add_key_value(
"aircraft_type", logistic_info.transport.dcs_id
@@ -144,7 +145,7 @@ class LuaGenerator:
target_points = lua_data.add_item("TargetPoints")
for flight in self.mission_data.flights:
if flight.friendly and flight.flight_type in [
if flight.friendly.is_blue and flight.flight_type in [
FlightType.ANTISHIP,
FlightType.DEAD,
FlightType.SEAD,
@@ -178,7 +179,7 @@ class LuaGenerator:
for cp in self.game.theater.controlpoints:
coalition_object = (
lua_data.get_or_create_item("BlueAA")
if cp.captured
if cp.captured.is_blue
else lua_data.get_or_create_item("RedAA")
)
for ground_object in cp.ground_objects:
@@ -202,7 +203,9 @@ class LuaGenerator:
# Should probably do the same with all the roles... but the script is already
# tolerant of those being empty.
for node in self.game.theater.iads_network.skynet_nodes(self.game):
coalition = iads_object.get_or_create_item("BLUE" if node.player else "RED")
coalition = iads_object.get_or_create_item(
"BLUE" if node.player.is_blue else "RED"
)
iads_type = coalition.get_or_create_item(node.iads_role.value)
iads_element = iads_type.add_item()
iads_element.add_key_value("dcsGroupName", node.dcs_name)

View File

@@ -15,6 +15,7 @@ from game.runways import RunwayData
if TYPE_CHECKING:
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
from game.theater.player import Player
from game.utils import Distance
from uuid import UUID
@@ -24,7 +25,7 @@ class GroupInfo:
group_name: str
callsign: str
freq: RadioFrequency
blue: bool
blue: Player
@dataclass
@@ -85,7 +86,7 @@ class LogisticsInfo:
pilot_names: list[str]
transport: AircraftType
blue: bool
blue: Player
logistic_unit: str = field(default_factory=str)
pickup_zone: str = field(default_factory=str)

View File

@@ -9,6 +9,7 @@ import dcs.lua
from dcs import Mission, Point
from dcs.coalition import Coalition
from dcs.countries import country_dict
from dcs.point import MovingPoint
from dcs.task import OptReactOnThreat
from dcs.terrain import Airport
from dcs.unit import Static
@@ -131,28 +132,42 @@ class MissionGenerator:
return self.unit_map
@staticmethod
def _configure_ewrj(gen: AircraftGenerator) -> None:
def _configure_react_to_threat_for_ew_jamming_packages(
gen: AircraftGenerator,
) -> None:
for groups in gen.ewrj_package_dict.values():
optrot = [
task
for task in groups[0].points[0].tasks
if isinstance(task, OptReactOnThreat)
][0]
assert isinstance(optrot, OptReactOnThreat)
if (
len(groups) == 1
and optrot.value != OptReactOnThreat.Values.PassiveDefense
):
# primary flight with no EWR-Jamming capability
continue
for group in groups:
tasks = group.points[0].tasks
for i in range(len(tasks)):
if isinstance(tasks[i], OptReactOnThreat):
tasks[i] = OptReactOnThreat(
OptReactOnThreat.Values.PassiveDefense
)
break
start_point = [
p for p in group.points if p.name in ["JOIN", "RACETRACK START"]
]
if not start_point:
sp = group.points[0]
else:
sp = start_point[0]
MissionGenerator.set_react_to_threat(
OptReactOnThreat.Values.PassiveDefense, sp
)
stop_point = [
p for p in group.points if p.name in ["SPLIT", "RACETRACK END"]
]
if not stop_point:
sp = group.points[0]
else:
sp = stop_point[0]
MissionGenerator.set_react_to_threat(
OptReactOnThreat.Values.EvadeFire, sp
)
@staticmethod
def set_react_to_threat(
optrot: OptReactOnThreat.Values, start_point: MovingPoint
) -> None:
tasks = start_point.tasks
for i in range(len(tasks)):
if isinstance(tasks[i], OptReactOnThreat):
tasks[i] = OptReactOnThreat(optrot)
return
tasks.append(OptReactOnThreat(optrot))
def setup_mission_coalitions(self) -> None:
self.mission.coalition["blue"] = Coalition(
@@ -291,7 +306,7 @@ class MissionGenerator:
flight.aircraft_type.assign_channels_for_flight(flight, self.mission_data)
if self.game.settings.plugins.get("ewrj"):
self._configure_ewrj(aircraft_generator)
self._configure_react_to_threat_for_ew_jamming_packages(aircraft_generator)
def generate_destroyed_units(self) -> None:
"""Add destroyed units to the Mission"""
@@ -376,9 +391,12 @@ class MissionGenerator:
tmu.theater_unit.position,
self.mission.terrain,
).dict()
warehouse["coalition"] = (
"blue" if tmu.theater_unit.ground_object.coalition.player else "red"
)
if tmu.theater_unit.ground_object.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif tmu.theater_unit.ground_object.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
warehouse["dynamicCargo"] = settings.dynamic_cargo
if tmu.theater_unit.is_ship or tmu.dcs_unit.category == "Heliports": # type: ignore
warehouse["dynamicSpawn"] = settings.dynamic_slots

View File

@@ -13,6 +13,7 @@ from dcs.vehicles import vehicle_map
from game.dcs.groundunittype import GroundUnitType
from game.naming import namegen
from game.theater import Player
if TYPE_CHECKING:
from game import Game
@@ -25,12 +26,12 @@ class RebellionGenerator:
def generate(self) -> None:
ownfor_country = self.mission.country(
self.game.coalition_for(player=True).faction.country.name
self.game.coalition_for(player=Player.BLUE).faction.country.name
)
for rz in self.game.theater.ownfor_rebel_zones:
self._generate_rebel_zone(ownfor_country, rz)
opfor_country = self.mission.country(
self.game.coalition_for(player=False).faction.country.name
self.game.coalition_for(player=Player.RED).faction.country.name
)
for rz in self.game.theater.opfor_rebel_zones:
self._generate_rebel_zone(opfor_country, rz)

View File

@@ -67,6 +67,7 @@ from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
from game.runways import RunwayData
from game.theater import (
ControlPoint,
Player,
TheaterGroundObject,
TheaterUnit,
NavalControlPoint,
@@ -386,9 +387,9 @@ class GroundObjectGenerator:
)
self._register_theater_unit(unit, static_group.units[0])
@staticmethod
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
if unit_type.eplrs:
def enable_eplrs(self, group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
eplrs_enabled = self.game.settings.eplrs_enabled
if eplrs_enabled and unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: MovingGroup[Any]) -> None:
@@ -408,7 +409,7 @@ class GroundObjectGenerator:
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
color = (
{1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
if scenery.ground_object.is_friendly(to_player=True)
if scenery.ground_object.is_friendly(to_player=Player.BLUE)
else {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
)
@@ -809,9 +810,12 @@ class HelipadGenerator:
# capture triggers
pad: BaseFARP
neutral_country = self.m.country(self.game.neutral_country.name)
country = self.m.country(
self.game.coalition_for(self.cp.captured).faction.country.name
)
if self.cp.captured is Player.NEUTRAL:
country = neutral_country
else:
country = self.m.country(
self.game.coalition_for(self.cp.captured).faction.country.name
)
name = f"{self.cp.name} {helipad_type} {i}"
logging.info("Generating helipad static : " + name)
@@ -862,7 +866,7 @@ class HelipadGenerator:
if self.game.position_culled(helipad):
cull_farp_statics = True
if self.cp.coalition.player:
if self.cp.coalition.player.is_blue:
for package in self.cp.coalition.ato.packages:
for flight in package.flights:
if flight.squadron.location == self.cp:
@@ -878,7 +882,12 @@ class HelipadGenerator:
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
@@ -989,7 +998,7 @@ class GroundSpawnRoadbaseGenerator:
cull_farp_statics = True
elif self.game.position_culled(ground_spawn[0]):
cull_farp_statics = True
if self.cp.coalition.player:
if self.cp.coalition.player.is_blue:
for package in self.cp.coalition.ato.packages:
for flight in package.flights:
if flight.squadron.location == self.cp:
@@ -1005,7 +1014,12 @@ class GroundSpawnRoadbaseGenerator:
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
@@ -1131,7 +1145,12 @@ class GroundSpawnLargeGenerator:
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
@@ -1258,7 +1277,7 @@ class GroundSpawnGenerator:
cull_farp_statics = True
elif self.game.position_culled(vtol_pad[0]):
cull_farp_statics = True
if self.cp.coalition.player:
if self.cp.coalition.player.is_blue:
for package in self.cp.coalition.ato.packages:
for flight in package.flights:
if flight.squadron.location == self.cp:
@@ -1275,7 +1294,12 @@ class GroundSpawnGenerator:
pad.position,
self.m.terrain,
).dict()
warehouse["coalition"] = "blue" if self.cp.coalition.player else "red"
if self.cp.coalition.player.is_neutral:
warehouse["coalition"] = "neutral"
elif self.cp.coalition.player.is_blue:
warehouse["coalition"] = "blue"
else:
warehouse["coalition"] = "red"
# configure dynamic spawn + hot start of DS, plus dynamic cargo?
self.m.warehouses.warehouses[pad.id] = warehouse
@@ -1382,7 +1406,11 @@ class TgoGenerator:
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
country = self.m.country(cp.coalition.faction.country.name)
# Use neutral country for neutral control points
if cp.captured is Player.NEUTRAL:
country = self.m.country(self.game.neutral_country.name)
else:
country = self.m.country(cp.coalition.faction.country.name)
# Generate helipads
helipad_gen = HelipadGenerator(

View File

@@ -12,7 +12,6 @@ from dcs.action import (
RemoveSceneObjects,
RemoveSceneObjectsMask,
SceneryDestructionZone,
Smoke,
)
from dcs.condition import (
AllOfCoalitionOutsideZone,
@@ -33,6 +32,7 @@ from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE
if TYPE_CHECKING:
from game.game import Game
from game.theater.controlpoint import ControlPoint
PUSH_TRIGGER_SIZE = 3000
PUSH_TRIGGER_ACTIVATION_AGL = 25
@@ -99,9 +99,12 @@ class TriggerGenerator:
raise RuntimeError(
f"Could not find {airfield.airport.name} in the mission"
)
cp_airport.set_coalition(
airfield.captured and player_coalition or enemy_coalition
)
if airfield.captured.is_neutral:
cp_airport.set_coalition("neutral")
elif airfield.captured.is_blue:
cp_airport.set_coalition(player_coalition)
elif airfield.captured.is_red:
cp_airport.set_coalition(enemy_coalition)
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
"""
@@ -138,7 +141,7 @@ class TriggerGenerator:
zone = self.mission.triggers.add_triggerzone(
location, radius=10, hidden=True, name="MARK"
)
if cp.captured:
if cp.captured.is_blue:
name = ground_object.obj_name + " [ALLY]"
else:
name = ground_object.obj_name + " [ENEMY]"
@@ -177,26 +180,57 @@ class TriggerGenerator:
# clear_trigger.add_action(MessageToAll(text=String("Enable clear trigger"),))
self.mission.triggerrules.triggers.append(enable_clear_trigger)
def _generate_capture_triggers(
self, player_coalition: str, enemy_coalition: str
) -> None:
def _create_capture_trigger(
self,
trigger_zone_id: int,
flag: int,
capturing_coalition: str,
losing_coalition: str,
capturing_coalition_int: int,
cp: ControlPoint,
flag_condition_true: bool = False,
) -> TriggerCondition:
"""Helper method to create a single capture trigger with the given parameters."""
trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
trigger.add_condition(
AllOfCoalitionOutsideZone(
losing_coalition, trigger_zone_id, unit_type="GROUND"
)
)
trigger.add_condition(
PartOfCoalitionInZone(
capturing_coalition, trigger_zone_id, unit_type="GROUND"
)
)
if flag_condition_true:
trigger.add_condition(FlagIsTrue(flag=flag))
trigger.add_action(ClearFlag(flag=flag))
else:
trigger.add_condition(FlagIsFalse(flag=flag))
trigger.add_action(SetFlag(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{capturing_coalition_int}||{cp.full_name}"'
)
trigger.add_action(DoScript(script_string))
return trigger
def _generate_capture_triggers(self) -> None:
"""Creates a pair of triggers for each control point of `cls.capture_zone_types`.
One for the initial capture of a control point, and one if it is recaptured.
Directly appends to the global `base_capture_events` var declared by `dcs_libaration.lua`
"""
for cp in self.game.theater.controlpoints:
attacking_coalition = str.lower(cp.captured.opponent.name)
defending_coalition = str.lower(cp.captured.opponent.opponent.name)
if attacking_coalition == "red":
attack_coalition_int = 1 # 1 is the Event int for Red
defend_coalition_int = 2 # 2 is the Event int for Blue
else:
attack_coalition_int = 2
defend_coalition_int = 1
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
if cp.captured:
attacking_coalition = enemy_coalition
attack_coalition_int = 1 # 1 is the Event int for Red
defending_coalition = player_coalition
defend_coalition_int = 2 # 2 is the Event int for Blue
else:
attacking_coalition = player_coalition
attack_coalition_int = 2
defending_coalition = enemy_coalition
defend_coalition_int = 1
trigger_zone = self.mission.triggers.add_triggerzone(
cp.position,
radius=TRIGGER_RADIUS_CAPTURE,
@@ -204,43 +238,48 @@ class TriggerGenerator:
name="CAPTURE",
)
flag = self.get_capture_zone_flag()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
capture_trigger.add_condition(
AllOfCoalitionOutsideZone(
defending_coalition, trigger_zone.id, unit_type="GROUND"
)
capture = self._create_capture_trigger(
trigger_zone.id,
flag,
attacking_coalition,
defending_coalition,
attack_coalition_int,
cp,
)
capture_trigger.add_condition(
PartOfCoalitionInZone(
attacking_coalition, trigger_zone.id, unit_type="GROUND"
)
recapture = self._create_capture_trigger(
trigger_zone.id,
flag,
defending_coalition,
attacking_coalition,
defend_coalition_int,
cp,
flag_condition_true=True,
)
capture_trigger.add_condition(FlagIsFalse(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{attack_coalition_int}||{cp.full_name}"'
)
capture_trigger.add_action(DoScript(script_string))
capture_trigger.add_action(SetFlag(flag=flag))
self.mission.triggerrules.triggers.append(capture_trigger)
self.mission.triggerrules.triggers.extend([capture, recapture])
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
recapture_trigger.add_condition(
AllOfCoalitionOutsideZone(
attacking_coalition, trigger_zone.id, unit_type="GROUND"
if cp.captured.is_neutral:
# In this case attacking_coalition is blue and defending_coalition is red because
# neutral control points always have blue set as the opponent
red_capture = self._create_capture_trigger(
trigger_zone.id,
flag,
defending_coalition,
attacking_coalition,
defend_coalition_int,
cp,
)
)
recapture_trigger.add_condition(
PartOfCoalitionInZone(
defending_coalition, trigger_zone.id, unit_type="GROUND"
red_recapture = self._create_capture_trigger(
trigger_zone.id,
flag,
defending_coalition,
attacking_coalition,
defend_coalition_int,
cp,
flag_condition_true=True,
)
self.mission.triggerrules.triggers.extend(
[red_capture, red_recapture]
)
)
recapture_trigger.add_condition(FlagIsTrue(flag=flag))
script_string = String(
f'base_capture_events[#base_capture_events + 1] = "{cp.id}||{defend_coalition_int}||{cp.full_name}"'
)
recapture_trigger.add_action(DoScript(script_string))
recapture_trigger.add_action(ClearFlag(flag=flag))
self.mission.triggerrules.triggers.append(recapture_trigger)
def generate(self) -> None:
player_coalition = "blue"
@@ -249,7 +288,7 @@ class TriggerGenerator:
self._set_skill(player_coalition, enemy_coalition)
self._set_allegiances(player_coalition, enemy_coalition)
self._gen_markers()
self._generate_capture_triggers(player_coalition, enemy_coalition)
self._generate_capture_triggers()
if self.game.settings.ground_start_scenery_remove_triggers:
try:
self._generate_clear_statics_trigger(self.game.scenery_clear_zones)

View File

@@ -55,7 +55,7 @@ class GameStats:
turn_data = GameTurnMetadata()
for cp in game.theater.controlpoints:
if cp.captured:
if cp.captured.is_blue:
for squadron in cp.squadrons:
turn_data.allied_units.aircraft_count += squadron.owned_aircraft
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())

View File

@@ -36,6 +36,18 @@ class DummyObject:
class MigrationUnpickler(pickle.Unpickler):
"""Custom unpickler to migrate campaign save-files for when components have been moved"""
def find_class(self, module: Any, name: str) -> Any:
if name == "Su_30MKA_AG":
from pydcs_extensions.su30 import Su_30MKA
return Su_30MKA
if name == "Su_30MKI_AG":
from pydcs_extensions.su30 import Su_30MKI
return Su_30MKI
if name == "Su_30SM_AG":
from pydcs_extensions.su30 import Su_30SM
return Su_30SM
if name == "Su_30MKM_AG":
from pydcs_extensions.su30 import Su_30MKM
return Su_30MKM
if name == "NightMissions":
from game.settings import NightMissions
return NightMissions
@@ -108,6 +120,16 @@ class MigrationUnpickler(pickle.Unpickler):
elif name == "Bas_100":
from dcs.terrain.kola.airports import Vuojarvi
return Vuojarvi
elif name == "Alakourtti":
from dcs.terrain.kola.airports import Alakurtti
return Alakurtti
if module == "dcs.terrain.sinai.airports":
if name == "Borj_El_Arab_International_Airport":
from dcs.terrain.sinai.airports import Borg_El_Arab_International_Airport
return Borg_El_Arab_International_Airport
elif name == "Palmahim":
from dcs.terrain.sinai.airports import Palmachim
return Palmachim
if module == "dcs.terrain.syria.airports":
if name == "Amman":
from dcs.terrain.syria.airports import Marka
@@ -119,6 +141,10 @@ class MigrationUnpickler(pickle.Unpickler):
"Helipad_218"
]:
return dcs.terrain.Airport # use base-class if airport was removed
if module == "dcs.terrain.afghanistan.airports":
if name == "Khost_Heliport":
from dcs.terrain.afghanistan.airports import FOB_Salerno
return FOB_Salerno
if module == "dcs.terrain.falklands.airports":
if name == "Aerodromo_De_Tolhuin":
from dcs.terrain.falklands.airports import Tolhuin

View File

@@ -35,6 +35,7 @@ from game.runways import RunwayData
from game.settings import Settings
from game.squadrons import AirWing
from game.squadrons import Squadron
from game.theater.player import Player
from game.theater.controlpoint import (
ControlPoint,
OffMapSpawn,
@@ -833,7 +834,7 @@ class PretenseAircraftGenerator:
"""
self.initialize_pretense_data_structures(cp)
is_player = True
is_player = Player.BLUE
if country == cp.coalition.faction.country:
offmap_transport_cp = self.find_pretense_cargo_plane_cp(cp)
@@ -868,7 +869,7 @@ class PretenseAircraftGenerator:
coalition = (
self.game.coalition_for(is_player)
if country == self.game.coalition_for(is_player).faction.country
else self.game.coalition_for(False)
else self.game.coalition_for(Player.RED)
)
self.generate_pretense_aircraft_for_other_side(cp, coalition, ato)
@@ -1057,6 +1058,7 @@ class PretenseAircraftGenerator:
and (
not self.need_ecm
or flight.any_member_has_weapon_of_type(WeaponType.JAMMER)
or flight.any_member_has_weapon_of_type(WeaponType.OFFENSIVE_JAMMER)
)
):
self.ewrj_package_dict[id(flight.package)].append(group)

View File

@@ -25,7 +25,7 @@ from game.missiongenerator.aircraft.flightgroupspawner import (
)
from game.missiongenerator.missiondata import MissionData
from game.naming import NameGenerator
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, Player
class PretenseNameGenerator(NameGenerator):
@@ -87,7 +87,7 @@ class PretenseFlightGroupSpawner(FlightGroupSpawner):
def insert_into_pretense(self, name: str) -> None:
cp = self.flight.departure
is_player = True
is_player = Player.BLUE
cp_side = (
2
if self.flight.coalition

View File

@@ -1495,13 +1495,13 @@ class PretenseLuaGenerator(LuaGenerator):
cp_name.replace("ä", "a")
cp_name.replace("ö", "o")
cp_name.replace("ø", "o")
cp_side = 2 if cp.captured else 1
cp_side = 2 if cp.captured.is_blue else 1
if isinstance(cp, OffMapSpawn):
continue
elif (
cp.is_fleet
and cp.captured
and cp.captured.is_blue
and self.game.settings.pretense_controllable_carrier
):
# Friendly carrier, generate carrier parameters
@@ -1591,7 +1591,7 @@ class PretenseLuaGenerator(LuaGenerator):
# Also connect carrier and LHA control points to adjacent friendly points
if cp.is_fleet and (
not self.game.settings.pretense_controllable_carrier
or not cp.captured
or cp.captured.is_red
):
num_of_carrier_connections = 0
for (
@@ -1616,13 +1616,13 @@ class PretenseLuaGenerator(LuaGenerator):
try:
if (
cp.is_fleet
and cp.captured
and cp.captured.is_blue
and self.game.settings.pretense_controllable_carrier
):
break
elif (
closest_cps[extra_connection].is_fleet
and closest_cps[extra_connection].captured
and closest_cps[extra_connection].captured.is_blue
and self.game.settings.pretense_controllable_carrier
):
break
@@ -1647,7 +1647,7 @@ class PretenseLuaGenerator(LuaGenerator):
if isinstance(cp, OffMapSpawn):
continue
cp_side_captured = cp_side == 2
if cp_side_captured != cp.captured:
if cp_side_captured != cp.captured.is_blue:
continue
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
cp.name
@@ -1723,6 +1723,13 @@ class PretenseLuaGenerator(LuaGenerator):
def inject_plugin_script(
self, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
# Hard block MOOSE injection for Pretense missions
if script_mnemonic.lower() == "moose" or "moose" in script.lower():
logging.info(
"PretenseLuaGenerator: Skipping hard-blocked Moose.lua injection"
)
return
if script_mnemonic in self.plugin_scripts:
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
return
@@ -1730,7 +1737,6 @@ class PretenseLuaGenerator(LuaGenerator):
self.plugin_scripts.append(script_mnemonic)
plugin_path = Path("./resources/plugins", plugin_mnemonic)
script_path = Path(plugin_path, script)
if not script_path.exists():
logging.error(f"Cannot find {script_path} for plugin {plugin_mnemonic}")

View File

@@ -28,6 +28,7 @@ from game.missiongenerator.visualsgenerator import VisualsGenerator
from game.naming import namegen
from game.persistency import pre_pretense_backups_dir
from game.pretense.pretenseaircraftgenerator import PretenseAircraftGenerator
from game.theater import Player
from game.theater.bullseye import Bullseye
from game.unitmap import UnitMap
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@@ -233,7 +234,7 @@ class PretenseMissionGenerator(MissionGenerator):
callsign=callsign,
region=frontline,
code=str(code),
blue=True,
blue=Player.BLUE,
freq=freq,
)
)

View File

@@ -49,6 +49,7 @@ from game.theater import (
TheaterUnit,
NavalControlPoint,
PresetLocation,
Player,
)
from game.theater.theatergroundobject import (
CarrierGroundObject,
@@ -248,7 +249,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
"""
unit_type = None
faction = self.coalition.faction
is_player = True
is_player = Player.BLUE
side = (
2
if self.country == self.game.coalition_for(is_player).faction.country
@@ -458,7 +459,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
control_point.name
)
is_player = True
is_player = Player.BLUE
side = (
2
if self.country
@@ -567,7 +568,7 @@ class PretenseGroundObjectGenerator(GroundObjectGenerator):
cp_name_trimmed = PretenseNameGenerator.pretense_trimmed_cp_name(
control_point.name
)
is_player = True
is_player = Player.BLUE
side = (
2
if self.country == self.game.coalition_for(is_player).faction.country

View File

@@ -1,9 +1,7 @@
from __future__ import annotations
import logging
import math
import random
from typing import TYPE_CHECKING, List
from typing import TYPE_CHECKING
from dcs import Point
from dcs.action import (
@@ -11,10 +9,6 @@ from dcs.action import (
DoScript,
MarkToAll,
SetFlag,
RemoveSceneObjects,
RemoveSceneObjectsMask,
SceneryDestructionZone,
Smoke,
)
from dcs.condition import (
AllOfCoalitionOutsideZone,
@@ -22,7 +16,6 @@ from dcs.condition import (
FlagIsTrue,
PartOfCoalitionInZone,
TimeAfter,
TimeSinceFlag,
)
from dcs.mission import Mission
from dcs.task import Option
@@ -31,12 +24,11 @@ from dcs.terrain.syria.airports import Damascus, Khalkhalah
from dcs.translation import String
from dcs.triggers import Event, TriggerCondition, TriggerOnce
from dcs.unit import Skill
from numpy import cross, einsum, arctan2
from shapely import MultiPolygon, Point as ShapelyPoint
from game.naming import ALPHA_MILITARY
from game.pretense.pretenseflightgroupspawner import PretenseNameGenerator
from game.theater import Airfield
from game.theater import Airfield, Player
from game.theater.controlpoint import Fob, TRIGGER_RADIUS_CAPTURE, OffMapSpawn
if TYPE_CHECKING:
@@ -119,7 +111,7 @@ class PretenseTriggerGenerator:
f"Could not find {airfield.airport.name} in the mission"
)
cp_airport.set_coalition(
airfield.captured and player_coalition or enemy_coalition
airfield.captured.is_blue and player_coalition or enemy_coalition
)
def _set_skill(self, player_coalition: str, enemy_coalition: str) -> None:
@@ -157,7 +149,7 @@ class PretenseTriggerGenerator:
zone = self.mission.triggers.add_triggerzone(
location, radius=10, hidden=True, name="MARK"
)
if cp.captured:
if cp.captured.is_blue:
name = ground_object.obj_name + " [ALLY]"
else:
name = ground_object.obj_name + " [ENEMY]"
@@ -174,7 +166,7 @@ class PretenseTriggerGenerator:
"""
for cp in self.game.theater.controlpoints:
if isinstance(cp, self.capture_zone_types) and not cp.is_carrier:
if cp.captured:
if cp.captured.is_blue:
attacking_coalition = enemy_coalition
attack_coalition_int = 1 # 1 is the Event int for Red
defending_coalition = player_coalition
@@ -243,7 +235,7 @@ class PretenseTriggerGenerator:
self.game.settings.pretense_carrier_zones_navmesh == "Blue navmesh"
)
sea_zones_landmap = self.game.coalition_for(
player=False
player=Player.RED
).nav_mesh.theater.landmap
if (
self.game.settings.pretense_controllable_carrier
@@ -251,7 +243,7 @@ class PretenseTriggerGenerator:
):
navmesh_number = 0
for navmesh_poly in self.game.coalition_for(
player=use_blue_navmesh
player=Player.BLUE if use_blue_navmesh else Player.RED
).nav_mesh.polys:
navmesh_number += 1
if sea_zones_landmap.sea_zones.intersects(navmesh_poly.poly):
@@ -325,7 +317,7 @@ class PretenseTriggerGenerator:
if (
cp.is_fleet
and self.game.settings.pretense_controllable_carrier
and cp.captured
and cp.captured.is_blue
):
# Friendly carrier zones are generated above
continue

View File

@@ -8,7 +8,7 @@ from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from game.config import RUNWAY_REPAIR_COST
from game.data.units import UnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint, MissionTarget, ParkingType
from game.theater import ControlPoint, MissionTarget, ParkingType, Player
if TYPE_CHECKING:
from game import Game
@@ -34,20 +34,20 @@ class ProcurementAi:
def __init__(
self,
game: Game,
for_player: bool,
owner: Player,
faction: Faction,
manage_runways: bool,
manage_front_line: bool,
manage_aircraft: bool,
) -> None:
self.game = game
self.is_player = for_player
self.air_wing = game.air_wing_for(for_player)
self.is_player = owner
self.air_wing = game.air_wing_for(owner)
self.faction = faction
self.manage_runways = manage_runways
self.manage_front_line = manage_front_line
self.manage_aircraft = manage_aircraft
self.threat_zones = self.game.threat_zone_for(not self.is_player)
self.threat_zones = self.game.threat_zone_for(self.is_player.opponent)
def calculate_ground_unit_budget_share(self) -> float:
armor_investment = 0
@@ -78,7 +78,7 @@ class ProcurementAi:
balance = (
self.game.settings.auto_procurement_balance
if self.is_player
if self.is_player.is_blue
else self.game.settings.auto_procurement_balance_red
)
air = balance / 100.0
@@ -114,7 +114,7 @@ class ProcurementAi:
if control_point.runway_can_be_repaired:
control_point.begin_runway_repair()
budget -= RUNWAY_REPAIR_COST
if self.is_player:
if self.is_player.is_blue:
self.game.message(
"We have begun repairing the runway at " f"{control_point}"
)
@@ -223,7 +223,7 @@ class ProcurementAi:
@property
def owned_points(self) -> List[ControlPoint]:
if self.is_player:
if self.is_player.is_blue:
return self.game.theater.player_points()
else:
return self.game.theater.enemy_points()
@@ -269,7 +269,7 @@ class ProcurementAi:
reserves_factor = (
self.game.settings.frontline_reserves_factor
if self.is_player
if self.is_player.is_blue
else self.game.settings.frontline_reserves_factor_red
)
fr_factor = reserves_factor / 100.0
@@ -308,7 +308,7 @@ class ProcurementAi:
)
target = (
self.game.settings.reserves_procurement_target
if self.is_player
if self.is_player.is_blue
else self.game.settings.reserves_procurement_target_red
)
if allocated.total >= target:

View File

@@ -219,6 +219,27 @@ class SCR522RadioChannelAllocator(RadioChannelAllocator):
return "SCR-522"
@dataclass(frozen=True)
class ARC5RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the ARC-5 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, mission_data: MissionData
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@classmethod
def name(cls) -> str:
return "ARC-5"
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
@@ -376,6 +397,23 @@ class SCR522ChannelNamer(ChannelNamer):
return "SCR-522"
class ARC5ChannelNamer(ChannelNamer):
"""
Channel namer for F4U-1D Corsair
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Channel " + "1234"[channel_id - 1]
@classmethod
def name(cls) -> str:
return "ARC-5"
class LegacyWarthogChannelNamer(ChannelNamer):
"""Channel namer for the legacy A-10C."""
@@ -434,3 +472,15 @@ class KiowaChannelNamer(ChannelNamer):
@classmethod
def name(cls) -> str:
return "kiowa"
class FulcrumChannelNamer(ChannelNamer):
"""Channel namer for the MiG-29 Fulcrum A"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id -1}"
@classmethod
def name(cls) -> str:
return "fulcrum"

View File

@@ -84,6 +84,10 @@ def kHz(
return RadioFrequency(num * 1000 + hz, modulation)
def Hz(num: int, hz: int = 0, modulation: Modulation = Modulation.AM) -> RadioFrequency:
return RadioFrequency(num + hz, modulation)
@dataclass(frozen=True)
class RadioRange:
"""Defines the minimum (inclusive) and maximum (exclusive) range of the radio."""
@@ -231,6 +235,15 @@ RADIOS: List[Radio] = [
# P-51 / P-47 Radio
# 4 preset channels (A/B/C/D)
Radio("SCR522", (RadioRange(MHz(100), MHz(156), kHz(25), Modulation.AM),)),
# F4U-1D Radio
# 4 preset channels (1/2/3/4)
Radio(
"ARC-5",
(
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
RadioRange(MHz(220), MHz(390), kHz(25), Modulation.AM),
),
),
# JF-17 Radios should use AM
Radio("R&S M3AR VHF", (RadioRange(MHz(120), MHz(174), kHz(25), Modulation.AM),)),
Radio("R&S M3AR UHF", (RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),)),
@@ -343,6 +356,20 @@ RADIOS: List[Radio] = [
"AN/ARC-27",
(RadioRange(MHz(225), MHz(400), kHz(100), Modulation.AM),),
),
# MiG-29 Fulcrum A
Radio(
"R-862",
(
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.AM),
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.AM),
RadioRange(MHz(220), MHz(400), kHz(25), Modulation.FM),
RadioRange(MHz(100), MHz(150), kHz(25), Modulation.FM),
),
),
Radio(
"ARK-19",
(RadioRange(kHz(150), kHz(1300), Hz(500), Modulation.AM),),
),
]

View File

@@ -29,12 +29,16 @@ class ControlPointJs(BaseModel):
destination = None
if control_point.target_position is not None:
destination = control_point.target_position.latlng()
if control_point.captured.is_blue:
blue = True
else:
blue = False
return ControlPointJs(
id=control_point.id,
name=control_point.name,
blue=control_point.captured,
blue=blue,
position=control_point.position.latlng(),
mobile=control_point.moveable and control_point.captured,
mobile=control_point.moveable and control_point.captured.is_blue,
destination=destination,
sidc=str(control_point.sidc()),
)

View File

@@ -6,6 +6,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, status
from starlette.responses import Response
from game import Game
from game.theater.player import Player
from .models import ControlPointJs
from ..dependencies import GameContext
from ..leaflet import LeafletPoint
@@ -75,7 +76,7 @@ def set_destination(
)
if not cp.moveable:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile")
if not cp.captured:
if not cp.captured.is_blue:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player"
)
@@ -120,7 +121,7 @@ def cancel_travel(cp_id: UUID, game: Game = Depends(GameContext.require)) -> Non
)
if not cp.moveable:
raise HTTPException(status.HTTP_403_FORBIDDEN, detail=f"{cp} is not mobile")
if not cp.captured:
if not cp.captured.is_blue:
raise HTTPException(
status.HTTP_403_FORBIDDEN, detail=f"{cp} is not owned by the player"
)

View File

@@ -15,6 +15,7 @@ from game.server.mapzones.models import ThreatZonesJs, UnculledZoneJs
from game.server.navmesh.models import NavMeshJs
from game.server.supplyroutes.models import SupplyRouteJs
from game.server.tgos.models import TgoJs
from game.theater import Player
if TYPE_CHECKING:
from game import Game
@@ -26,9 +27,9 @@ class GameUpdateEventsJs(BaseModel):
new_combats: list[FrozenCombatJs]
updated_combats: list[FrozenCombatJs]
ended_combats: list[UUID]
navmesh_updates: dict[bool, NavMeshJs]
navmesh_updates: dict[Player, NavMeshJs]
updated_unculled_zones: list[UnculledZoneJs]
threat_zones_updated: dict[bool, ThreatZonesJs]
threat_zones_updated: dict[Player, ThreatZonesJs]
new_flights: list[FlightJs]
updated_flights: list[FlightJs]
deleted_flights: set[UUID]

View File

@@ -41,9 +41,13 @@ class FlightJs(BaseModel):
waypoints = None
if with_waypoints:
waypoints = waypoints_for_flight(flight)
if flight.blue.is_blue:
blue = True
else:
blue = False
return FlightJs(
id=flight.id,
blue=flight.blue,
blue=blue,
position=position,
sidc=str(flight.sidc()),
waypoints=waypoints,

View File

@@ -49,7 +49,9 @@ class GameJs(BaseModel):
iads_network=IadsNetworkJs.from_network(game.theater.iads_network),
threat_zones=ThreatZoneContainerJs.for_game(game),
navmeshes=NavMeshesJs.from_game(game),
map_center=game.theater.terrain.map_view_default.position.latlng(),
map_center=LeafletPoint.from_latlng(
game.theater.terrain.map_view_default.position.latlng()
),
unculled_zones=UnculledZoneJs.from_game(game),
map_zones=MapZonesJs.from_game(game),
)

View File

@@ -5,6 +5,7 @@ from uuid import UUID
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint
from game.theater.player import Player
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode, IadsNetwork
@@ -34,8 +35,16 @@ class IadsConnectionJs(BaseModel):
iads_connections = []
tgo = network_node.group.ground_object
for id, connection in network_node.connections.items():
if connection.ground_object.is_friendly(True) != tgo.is_friendly(True):
if connection.ground_object.is_friendly(Player.BLUE) != tgo.is_friendly(
Player.BLUE
):
continue # Skip connections which are not from same coalition
if tgo.is_friendly(Player.BLUE):
blue = True
elif tgo.is_friendly(Player.RED):
blue = False
else:
continue # Skip neutral
iads_connections.append(
IadsConnectionJs(
id=id,
@@ -49,7 +58,7 @@ class IadsConnectionJs(BaseModel):
network_node.group.alive_units > 0
and connection.alive_units > 0
),
blue=tgo.is_friendly(True),
blue=blue,
is_power="power"
in [tgo.category, connection.ground_object.category],
)

View File

@@ -19,6 +19,10 @@ class LeafletPoint(BaseModel):
title = "LatLng"
@staticmethod
def from_latlng(latlng: LatLng) -> LeafletPoint:
return LeafletPoint(lat=latlng.lat, lng=latlng.lng)
LeafletLine = list[LeafletPoint]

View File

@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
from pydantic import BaseModel
from game.server.leaflet import LeafletPoint, LeafletPoly, ShapelyUtil
from game.theater import ConflictTheater
from game.theater import ConflictTheater, Player
from game.threatzones import ThreatZones
if TYPE_CHECKING:
@@ -85,9 +85,9 @@ class ThreatZoneContainerJs(BaseModel):
def for_game(game: Game) -> ThreatZoneContainerJs:
return ThreatZoneContainerJs(
blue=ThreatZonesJs.from_zones(
game.threat_zone_for(player=True), game.theater
game.threat_zone_for(player=Player.BLUE), game.theater
),
red=ThreatZonesJs.from_zones(
game.threat_zone_for(player=False), game.theater
game.threat_zone_for(player=Player.RED), game.theater
),
)

View File

@@ -2,12 +2,13 @@ from fastapi import APIRouter, Depends
from game import Game
from game.server import GameContext
from game.theater.player import Player
from .models import NavMeshJs
router: APIRouter = APIRouter(prefix="/navmesh")
@router.get("/", operation_id="get_navmesh", response_model=NavMeshJs)
def get(for_player: bool, game: Game = Depends(GameContext.require)) -> NavMeshJs:
def get(for_player: Player, game: Game = Depends(GameContext.require)) -> NavMeshJs:
mesh = game.coalition_for(for_player).nav_mesh
return NavMeshJs.from_navmesh(mesh, game)

View File

@@ -76,6 +76,10 @@ class SupplyRouteJs(BaseModel):
def for_link(
game: Game, a: ControlPoint, b: ControlPoint, points: list[Point], sea: bool
) -> SupplyRouteJs:
if a.captured.is_blue:
blue = True
else:
blue = False
return SupplyRouteJs(
# Although these are not persistent objects in the backend, the frontend
# needs unique IDs for anything that it will use in a list. That means that
@@ -93,7 +97,7 @@ class SupplyRouteJs(BaseModel):
points=[p.latlng() for p in points],
front_active=not sea and a.front_is_active(b),
is_sea=sea,
blue=a.captured,
blue=blue,
active_transports=TransportFinder(game, a, b).describe_active_transports(
sea
),

View File

@@ -34,12 +34,16 @@ class TgoJs(BaseModel):
def for_tgo(tgo: TheaterGroundObject) -> TgoJs:
threat_ranges = [group.max_threat_range().meters for group in tgo.groups]
detection_ranges = [group.max_detection_range().meters for group in tgo.groups]
if tgo.control_point.captured.is_blue:
blue = True
else:
blue = False
return TgoJs(
id=tgo.id,
name=tgo.name,
control_point_name=tgo.control_point.name,
category=tgo.category,
blue=tgo.control_point.captured,
blue=blue,
position=tgo.position.latlng(),
units=[unit.display_name for unit in tgo.units],
threat_ranges=threat_ranges,

View File

@@ -816,6 +816,12 @@ class Settings:
GAMEPLAY_SECTION,
default=True,
)
eplrs_enabled: bool = boolean_option(
"Enable EPLRS",
MISSION_GENERATOR_PAGE,
GAMEPLAY_SECTION,
default=True,
)
generate_dark_kneeboard: bool = boolean_option(
"Generate dark kneeboard",
MISSION_GENERATOR_PAGE,

View File

@@ -46,7 +46,7 @@ class AirCombat(JoinableCombat):
blue_flights = []
red_flights = []
for flight in self.flights:
if flight.squadron.player:
if flight.squadron.player.is_blue:
blue_flights.append(str(flight))
else:
red_flights.append(str(flight))
@@ -71,7 +71,7 @@ class AirCombat(JoinableCombat):
blue = []
red = []
for flight in self.flights:
if flight.squadron.player:
if flight.squadron.player.is_blue:
blue.append(flight)
else:
red.append(flight)

View File

@@ -6,6 +6,7 @@ from collections.abc import Iterator
from datetime import timedelta
from typing import Optional, TYPE_CHECKING
from game.theater.player import Player
from .aircombat import AirCombat
from .aircraftengagementzones import AircraftEngagementZones
from .atip import AtIp
@@ -31,8 +32,10 @@ class CombatInitiator:
def update_active_combats(self) -> None:
blue_a2a = AircraftEngagementZones.from_ato(self.game.blue.ato)
red_a2a = AircraftEngagementZones.from_ato(self.game.red.ato)
blue_sam = SamEngagementZones.from_theater(self.game.theater, player=True)
red_sam = SamEngagementZones.from_theater(self.game.theater, player=False)
blue_sam = SamEngagementZones.from_theater(
self.game.theater, player=Player.BLUE
)
red_sam = SamEngagementZones.from_theater(self.game.theater, player=Player.RED)
# Check each vulnerable flight to see if it has initiated combat. If any flight
# initiates combat, a single FrozenCombat will be created for all involved
@@ -46,7 +49,7 @@ class CombatInitiator:
if flight.state.in_combat:
return
if flight.squadron.player:
if flight.squadron.player.is_blue:
a2a = red_a2a
own_a2a = blue_a2a
sam = red_sam

View File

@@ -9,7 +9,7 @@ from shapely.ops import unary_union
from game.utils import dcs_to_shapely_point
if TYPE_CHECKING:
from game.theater import ConflictTheater, TheaterGroundObject
from game.theater import ConflictTheater, TheaterGroundObject, Player
from game.threatzones import ThreatPoly
@@ -31,7 +31,9 @@ class SamEngagementZones:
yield tgo
@classmethod
def from_theater(cls, theater: ConflictTheater, player: bool) -> SamEngagementZones:
def from_theater(
cls, theater: ConflictTheater, player: Player
) -> SamEngagementZones:
commit_regions = []
individual_zones = []
for cp in theater.control_points_for(player):

View File

@@ -12,7 +12,7 @@ if TYPE_CHECKING:
from game.ato import Flight, Package
from game.navmesh import NavMesh
from game.sim.combat import FrozenCombat
from game.theater import ControlPoint, FrontLine, TheaterGroundObject
from game.theater import ControlPoint, FrontLine, TheaterGroundObject, Player
from game.threatzones import ThreatZones
from game.theater.iadsnetwork.iadsnetwork import IadsNetworkNode
@@ -24,9 +24,9 @@ class GameUpdateEvents:
updated_combats: list[FrozenCombat] = field(default_factory=list)
ended_combats: list[FrozenCombat] = field(default_factory=list)
updated_flight_positions: list[tuple[Flight, Point]] = field(default_factory=list)
navmesh_updates: dict[bool, NavMesh] = field(default_factory=dict)
navmesh_updates: dict[Player, NavMesh] = field(default_factory=dict)
unculled_zones_updated: list[Point] = field(default_factory=list)
threat_zones_updated: dict[bool, ThreatZones] = field(default_factory=dict)
threat_zones_updated: dict[Player, ThreatZones] = field(default_factory=dict)
new_flights: set[Flight] = field(default_factory=set)
updated_flights: set[Flight] = field(default_factory=set)
deleted_flights: set[UUID] = field(default_factory=set)
@@ -70,7 +70,7 @@ class GameUpdateEvents:
self.updated_flight_positions.append((flight, new_position))
return self
def update_navmesh(self, player: bool, navmesh: NavMesh) -> GameUpdateEvents:
def update_navmesh(self, player: Player, navmesh: NavMesh) -> GameUpdateEvents:
self.navmesh_updates[player] = navmesh
return self
@@ -78,7 +78,9 @@ class GameUpdateEvents:
self.unculled_zones_updated = zones
return self
def update_threat_zones(self, player: bool, zones: ThreatZones) -> GameUpdateEvents:
def update_threat_zones(
self, player: Player, zones: ThreatZones
) -> GameUpdateEvents:
self.threat_zones_updated[player] = zones
return self

View File

@@ -145,7 +145,7 @@ class MissionResultsProcessor:
def commit_captures(self, debriefing: Debriefing, events: GameUpdateEvents) -> None:
for captured in debriefing.base_captures:
try:
if captured.captured_by_player:
if captured.captured_by_player.is_blue:
self.game.message(
f"{captured.control_point} captured!",
f"We took control of {captured.control_point}.",
@@ -174,7 +174,7 @@ class MissionResultsProcessor:
self, debriefing: Debriefing, events: GameUpdateEvents
) -> None:
for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured]
enemy_cps = [e for e in cp.connected_points if e.captured.is_red]
for enemy_cp in enemy_cps:
front_line = cp.front_line_with(enemy_cp)
front_line.update_position()
@@ -321,7 +321,7 @@ class MissionResultsProcessor:
settings = cp.coalition.game.settings
factor = (
settings.frontline_reserves_factor
if cp.captured
if cp.captured.is_blue
else settings.frontline_reserves_factor_red
)
@@ -345,7 +345,7 @@ class MissionResultsProcessor:
1,
(
settings.reserves_procurement_target
if source.captured
if source.captured.is_blue
else settings.reserves_procurement_target_red
),
)
@@ -367,7 +367,7 @@ class MissionResultsProcessor:
source.base.commit_losses(moved_units)
# Also transfer pending deliveries.
for unit_type, count in source.ground_unit_orders.units.items():
for unit_type, count in list(source.ground_unit_orders.units.items()):
move_count = int(count * move_factor)
source.ground_unit_orders.sell({unit_type: move_count})
destination.ground_unit_orders.order({unit_type: move_count})

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import itertools
from collections import defaultdict
from typing import Iterator, Optional, Sequence, TYPE_CHECKING
from typing import Iterator, Optional, Sequence, TYPE_CHECKING, Any
from game.ato.closestairfields import ObjectiveDistanceCache
from game.dcs.aircrafttype import AircraftType
@@ -14,18 +14,31 @@ from ..utils import Distance
if TYPE_CHECKING:
from game.game import Game
from game.theater.player import Player
from ..ato.flighttype import FlightType
from .squadron import Squadron
class AirWing:
def __init__(self, player: bool, game: Game, faction: Faction) -> None:
def __init__(self, player: Player, game: Game, faction: Faction) -> None:
self.player = player
self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
self.squadron_defs = SquadronDefLoader(game, faction).load()
self.squadron_def_generator = SquadronDefGenerator(faction)
self.settings = game.settings
def __setstate__(self, state: dict[str, Any]) -> None:
# Migration: Convert old boolean player values to Player enum
if "player" in state and isinstance(state["player"], bool):
from game.theater.player import Player
if state["player"]:
state["player"] = Player.BLUE
else:
state["player"] = Player.RED
self.__dict__.update(state)
def unclaim_squadron_def(self, squadron: Squadron) -> None:
if squadron.aircraft in self.squadron_defs:
for squadron_def in self.squadron_defs[squadron.aircraft]:

View File

@@ -15,6 +15,7 @@ from faker import Faker
from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings
from game.theater import ParkingType
from game.theater.player import Player
from .pilot import Pilot, PilotStatus
from ..db.database import Database
from ..radio.radios import RadioFrequency
@@ -96,7 +97,7 @@ class Squadron:
self._livery_pool: list[str] = []
@property
def player(self) -> bool:
def player(self) -> Player:
return self.coalition.player
def assign_to_base(self, base: ControlPoint) -> None:
@@ -134,7 +135,7 @@ class Squadron:
return self.claim_new_pilot_if_allowed()
# For opfor, so player/AI option is irrelevant.
if not self.player:
if self.player != Player.BLUE:
return self.available_pilots.pop()
preference = self.settings.auto_ato_behavior

View File

@@ -2,5 +2,6 @@ from .base import *
from .conflicttheater import *
from .controlpoint import *
from .frontline import FrontLine
from .landmap import *
from .missiontarget import MissionTarget
from .theatergroundobject import SamGroundObject

View File

@@ -15,6 +15,7 @@ from .daytimemap import DaytimeMap
from .frontline import FrontLine
from .iadsnetwork.iadsnetwork import IadsNetwork
from .landmap import poly_contains, load_landmap
from .player import Player
from .seasonalconditions import SeasonalConditions
from ..utils import Heading
@@ -170,10 +171,10 @@ class ConflictTheater:
return new_point
def control_points_for(
self, player: bool, state_check: bool = False
self, player: Player, state_check: bool = False
) -> Iterator[ControlPoint]:
for point in self.controlpoints:
if point.captured == player:
if point.captured is player:
if not state_check:
yield point
elif point.is_carrier and point.runway_is_operational():
@@ -182,14 +183,21 @@ class ConflictTheater:
yield point
def player_points(self, state_check: bool = False) -> List[ControlPoint]:
return list(self.control_points_for(player=True, state_check=state_check))
return list(
self.control_points_for(player=Player.BLUE, state_check=state_check)
)
def conflicts(self) -> Iterator[FrontLine]:
for cp in self.player_points():
yield from cp.front_lines.values()
def enemy_points(self, state_check: bool = False) -> List[ControlPoint]:
return list(self.control_points_for(player=False, state_check=state_check))
return list(self.control_points_for(player=Player.RED, state_check=state_check))
def neutral_points(self, state_check: bool = False) -> List[ControlPoint]:
return list(
self.control_points_for(player=Player.NEUTRAL, state_check=state_check)
)
def closest_control_point(
self, point: Point, allow_naval: bool = False
@@ -259,10 +267,12 @@ class ConflictTheater:
"""
closest_cps = list()
distances_to_cp = dict()
if cp.captured:
if cp.captured.is_blue:
control_points = self.player_points()
else:
elif cp.captured.is_red:
control_points = self.enemy_points()
elif cp.captured.is_neutral:
control_points = self.neutral_points()
for other_cp in control_points:
if cp == other_cp:
continue
@@ -292,6 +302,12 @@ class ConflictTheater:
return cp
raise KeyError(f"Cannot find ControlPoint named {name}")
def control_point_by_full_name(self, full_name: str) -> ControlPoint:
for cp in self.controlpoints:
if cp.full_name == full_name:
return cp
raise KeyError(f"Cannot find ControlPoint with full name {full_name}")
def find_carriers(self) -> List[ControlPoint]:
try:
carriers = [cp for cp in self.controlpoints if cp.is_carrier]

View File

@@ -40,12 +40,14 @@ from dcs.ships import (
Stennis,
Type_071,
hms_invincible,
Essex,
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import ShipType
from game.ato.closestairfields import ObjectiveDistanceCache
from game.controlpoint_influenceradius import ControlPointInfluenceRadius
from game.ground_forces.combat_stance import CombatStance
from game.point_with_heading import PointWithHeading
from game.runways import RunwayAssigner, RunwayData
@@ -66,6 +68,7 @@ from .base import Base
from .frontline import FrontLine
from .interfaces.CTLD import CTLD
from .missiontarget import MissionTarget
from .player import Player
from .theatergroundobject import (
GenericCarrierGroundObject,
TheaterGroundObject,
@@ -375,15 +378,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
position: Point,
at: StartingPosition,
theater: ConflictTheater,
starts_blue: bool,
starting_coalition: Player,
cptype: ControlPointType = ControlPointType.AIRBASE,
is_invisible: bool = False,
) -> None:
super().__init__(name, position)
self.id = uuid.uuid4()
self.full_name = name
self.at = at
self.theater = theater
self.starts_blue = starts_blue
self.is_invisible = is_invisible
self.starting_coalition = starting_coalition
self.connected_objectives: List[TheaterGroundObject] = []
self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = []
@@ -414,6 +419,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# Initialized late because ControlPoints are constructed before the game is.
self._front_line_db: Database[FrontLine] | None = None
self.influence_radius: ControlPointInfluenceRadius | None = None
def __repr__(self) -> str:
return f"<{self.__class__}: {self.name}>"
@@ -429,7 +436,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def finish_init(self, game: Game) -> None:
assert self._coalition is None
self._coalition = game.coalition_for(self.starts_blue)
self._coalition = game.coalition_for(self.starting_coalition)
assert self._front_line_db is None
self._front_line_db = game.db.front_lines
@@ -438,7 +445,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
# the entire game state when it comes up.
from game.sim import GameUpdateEvents
self._create_missing_front_lines(laser_code_registry, GameUpdateEvents())
if self.captured != Player.NEUTRAL:
self._create_missing_front_lines(laser_code_registry, GameUpdateEvents())
@property
def front_line_db(self) -> Database[FrontLine]:
@@ -449,9 +457,11 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
self, laser_code_registry: LaserCodeRegistry, events: GameUpdateEvents
) -> None:
for connection in self.convoy_routes.keys():
if not connection.front_line_active_with(
self
) and not connection.is_friendly_to(self):
if (
not connection.front_line_active_with(self)
and not connection.is_friendly_to(self)
and connection.captured != Player.NEUTRAL
):
self._create_front_line_with(laser_code_registry, connection, events)
def _create_front_line_with(
@@ -485,6 +495,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
@property
def has_frontline(self) -> bool:
if self.captured.is_neutral:
return False
return bool(self.front_lines)
def front_line_active_with(self, other: ControlPoint) -> bool:
@@ -494,14 +506,17 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
return self.front_lines[other]
@property
def captured(self) -> bool:
def captured(self) -> Player:
return self.coalition.player
@property
def standard_identity(self) -> StandardIdentity:
return (
StandardIdentity.FRIEND if self.captured else StandardIdentity.HOSTILE_FAKER
)
if self.captured.is_neutral:
return StandardIdentity.UNKNOWN
elif self.captured.is_blue:
return StandardIdentity.FRIEND
else:
return StandardIdentity.HOSTILE_FAKER
@property
def sidc_status(self) -> Status:
@@ -813,7 +828,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
found.append(g)
return found
def is_friendly(self, to_player: bool) -> bool:
def is_friendly(self, to_player: Player) -> bool:
return self.captured == to_player
def is_friendly_to(self, control_point: ControlPoint) -> bool:
@@ -822,7 +837,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def capture_equipment(self, game: Game) -> None:
total = self.base.total_armor_value
self.base.armor.clear()
game.adjust_budget(total, player=not self.captured)
game.adjust_budget(
total, player=Player.BLUE if self.captured.is_red else Player.RED
)
game.message(
f"{self.name} is not connected to any friendly points. Ground "
f"vehicles have been captured and sold for ${total}M."
@@ -851,7 +868,9 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
def capture_aircraft(self, game: Game, airframe: AircraftType, count: int) -> None:
value = airframe.price * count
game.adjust_budget(value, player=not self.captured)
game.adjust_budget(
value, player=Player.BLUE if self.captured.is_red else Player.RED
)
game.message(
f"No valid retreat destination in range of {self.name} for {airframe} "
f"{count} aircraft have been captured and sold for ${value}M."
@@ -954,7 +973,7 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
pass
# TODO: Should be Airbase specific.
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
new_coalition = game.coalition_for(for_player)
self.ground_unit_orders.refund_all(self.coalition)
self.retreat_ground_units(game)
@@ -1119,6 +1138,8 @@ class ControlPoint(MissionTarget, SidcDescribable, ABC):
@property
def has_active_frontline(self) -> bool:
if self.captured.is_neutral:
return False
return any(not c.is_friendly(self.captured) for c in self.connected_points)
def front_is_active(self, other: ControlPoint) -> bool:
@@ -1205,20 +1226,22 @@ class Airfield(ControlPoint, CTLD):
self,
airport: Airport,
theater: ConflictTheater,
starts_blue: bool,
starting_coalition: Player,
ctld_zones: Optional[List[Tuple[Point, float]]] = None,
influence_zone: Optional[List[Tuple[Point, float]]] = None,
) -> None:
super().__init__(
airport.name,
airport.position,
airport,
theater,
starts_blue,
starting_coalition,
cptype=ControlPointType.AIRBASE,
)
self.airport = airport
self._runway_status = RunwayStatus()
self.ctld_zones = ctld_zones
self.influence_zone = influence_zone
@property
def dcs_airport(self) -> Airport:
@@ -1244,7 +1267,7 @@ class Airfield(ControlPoint, CTLD):
return True
return self.runway_is_operational()
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType
if not self.is_friendly(for_player):
@@ -1365,7 +1388,7 @@ class NavalControlPoint(
def is_fleet(self) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType
if self.is_friendly(for_player):
@@ -1426,6 +1449,7 @@ class NavalControlPoint(
CVN_72,
CVN_73,
CVN_75,
Essex,
]:
return True
return False
@@ -1473,7 +1497,7 @@ class NavalControlPoint(
class Carrier(NavalControlPoint):
def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player
):
super().__init__(
name,
@@ -1488,7 +1512,7 @@ class Carrier(NavalControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.CARRIER
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
raise RuntimeError("Carriers cannot be captured")
@property
@@ -1506,9 +1530,14 @@ class Carrier(NavalControlPoint):
return "cv"
class EssexCarrier(Carrier):
def can_operate(self, aircraft: AircraftType) -> bool:
return aircraft.lha_capable
class Lha(NavalControlPoint):
def __init__(
self, name: str, at: Point, theater: ConflictTheater, starts_blue: bool
self, name: str, at: Point, theater: ConflictTheater, starts_blue: Player
):
super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.LHA_GROUP
@@ -1518,7 +1547,7 @@ class Lha(NavalControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.SEA_SURFACE, SeaSurfaceEntity.AMPHIBIOUS_ASSAULT_SHIP_GENERAL
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
raise RuntimeError("LHAs cannot be captured")
@property
@@ -1541,7 +1570,7 @@ class OffMapSpawn(ControlPoint):
return True
def __init__(
self, name: str, position: Point, theater: ConflictTheater, starts_blue: bool
self, name: str, position: Point, theater: ConflictTheater, starts_blue: Player
):
super().__init__(
name,
@@ -1556,10 +1585,10 @@ class OffMapSpawn(ControlPoint):
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
return SymbolSet.LAND_INSTALLATIONS, LandInstallationEntity.AIPORT_AIR_BASE
def capture(self, game: Game, events: GameUpdateEvents, for_player: bool) -> None:
def capture(self, game: Game, events: GameUpdateEvents, for_player: Player) -> None:
raise RuntimeError("Off map control points cannot be captured")
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
yield from []
def total_aircraft_parking(self, parking_type: ParkingType) -> int:
@@ -1616,14 +1645,18 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
name: str,
at: Point,
theater: ConflictTheater,
starts_blue: bool,
starts_blue: Player,
ctld_zones: Optional[List[Tuple[Point, float]]] = None,
is_invisible: bool = False,
influence_zone: Optional[List[Tuple[Point, float]]] = None,
) -> None:
super().__init__(
name, at, at, theater, starts_blue, cptype=ControlPointType.FOB
)
self.name = name
self.ctld_zones = ctld_zones
self.is_invisible = is_invisible
self.influence_zone = influence_zone
@property
def symbol_set_and_entity(self) -> tuple[SymbolSet, Entity]:
@@ -1649,7 +1682,7 @@ class Fob(ControlPoint, RadioFrequencyContainer, CTLD):
def runway_status(self) -> RunwayStatus:
return RunwayStatus()
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType
if not self.is_friendly(for_player):

View File

@@ -8,6 +8,7 @@ from typing import Any, Iterator, List, TYPE_CHECKING, Tuple
from dcs.mapping import Point
from .missiontarget import MissionTarget
from .player import Player
from ..lasercodes.lasercode import LaserCode
from ..utils import Heading, pairwise
@@ -89,19 +90,22 @@ class FrontLine(MissionTarget):
def update_position(self) -> None:
self.position = self._compute_position()
def control_point_friendly_to(self, player: bool) -> ControlPoint:
if player:
def control_point_friendly_to(self, player: Player) -> ControlPoint:
if player.is_blue:
return self.blue_cp
return self.red_cp
def control_point_hostile_to(self, player: bool) -> ControlPoint:
return self.control_point_friendly_to(not player)
def control_point_hostile_to(self, player: Player) -> ControlPoint:
if player.is_blue:
return self.red_cp
else:
return self.blue_cp
def is_friendly(self, to_player: bool) -> bool:
def is_friendly(self, to_player: Player) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
def mission_types(self, for_player: Player) -> Iterator[FlightType]:
from game.ato import FlightType
yield from [
@@ -210,6 +214,6 @@ class FrontLine(MissionTarget):
raise ValueError(
"Cannot sort control points that are friendly to each other"
)
if a.captured:
if a.captured.is_blue:
return a, b
return b, a

View File

@@ -16,6 +16,7 @@ from game.theater.theatergroundobject import (
TheaterGroundObject,
)
from game.theater.theatergroup import IadsGroundGroup
from game.theater.player import Player
if TYPE_CHECKING:
from game.game import Game
@@ -31,7 +32,7 @@ class SkynetNode:
"""Dataclass for a SkynetNode used in the LUA Data table by the luagenerator"""
dcs_name: str
player: bool
player: Player
iads_role: IadsRole
properties: dict[str, str] = field(default_factory=dict)
connections: dict[str, list[str]] = field(default_factory=lambda: defaultdict(list))
@@ -62,7 +63,7 @@ class SkynetNode:
def from_group(cls, group: IadsGroundGroup) -> SkynetNode:
node = cls(
cls.dcs_name_for_group(group),
group.ground_object.is_friendly(True),
group.ground_object.coalition.player,
group.iads_role,
)
unit_type = group.units[0].unit_type
@@ -314,8 +315,8 @@ class IadsNetwork:
self._make_advanced_connections_by_range(node)
def _is_friendly(self, node: IadsNetworkNode, tgo: TheaterGroundObject) -> bool:
node_friendly = node.group.ground_object.is_friendly(True)
tgo_friendly = tgo.is_friendly(True)
node_friendly = node.group.ground_object.is_friendly(Player.BLUE)
tgo_friendly = tgo.is_friendly(Player.BLUE)
return node_friendly == tgo_friendly
def _update_network(

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