Compare commits

...

205 Commits
5.2.0 ... 4.1.0

Author SHA1 Message Date
bgreman
4ad8c01d53 Indiciated gripen support version 2021-08-08 12:52:04 -07:00
RndName
2410771eac Fix AAA Flak generator using wrong index
- Fixes #1519 as the Opel Blitz unit generator was using the index without incrementing it
2021-08-08 12:52:04 -07:00
Dan Albert
58a3c45052 Remove abandoned campaigns.
(cherry picked from commit 07f8a203ea)
2021-08-08 12:52:04 -07:00
RndName
e92a1eed75 tweak the airlift procurement
- only buy airlift capable aircraft if there is one friendly cp without a factory which can only be reached via airlift
- prevent that an airlift procurement gets fulfilled at a different cp than the requesting one. this ensures that the cp also has a factory to produce ground units which can then be transported
- fixes an infinite buy loop if the fulfilling cp has no factory and the requesting cp has no space for airlift
- have always 2 reserve transport planes at the biggest CP
2021-08-08 12:52:04 -07:00
Magnus Wolffelt
02f0738666 Fix patrol_speed save compat for 4.1 (#1507)
* Fix patrol_speed save compat for 4.1

* Fix and simplify 4.1 compat fix
2021-08-08 12:52:04 -07:00
Khopa
ed46b8a467 Campaign Update 8.0 : operation_dynamo.miz, updated mission targets ids
(cherry picked from commit f2608cecd5)
2021-08-08 12:52:04 -07:00
Khopa
50f41c573b Campaign Update 8.0 : golan_heights_lite.miz, updated mission targets ids
(cherry picked from commit c95d5464d8)
2021-08-08 12:52:04 -07:00
Khopa
3e6685811e Campaign Update 8.0 : caen_to_evreux.miz, updated mission targets ids
(cherry picked from commit 0ac7466a81)
2021-08-08 12:52:04 -07:00
Dan Albert
add840c23f Update Syria Full and Humble Helper campaigns.
https://github.com/dcs-liberation/dcs_liberation/issues/1494
https://github.com/dcs-liberation/dcs_liberation/issues/1497
(cherry picked from commit ff571db494)
2021-08-08 12:52:04 -07:00
Kangwook Lee
d08407a8f2 Wrap lines for NotesPage
(cherry picked from commit 8608b73009)
2021-08-08 12:52:04 -07:00
Dan Albert
a41dc15f4e Stop cluttering the kneeboard with empty notes.
(cherry picked from commit d11174da21)
2021-08-08 12:52:04 -07:00
Kangwook Lee
222467d429 Fix text foreground color for dark kneeboard
(cherry picked from commit 77e62d5a54)
2021-08-08 12:52:04 -07:00
Magnus Wolffelt
be5b87be2a Update changelog for 4.x
BAI missions are actually planned at low altitude. The problem remaining is that they have join/hold/split waypoints, which makes the flight times _incredibly_ long for these slow movers.
2021-08-08 12:52:04 -07:00
Magnus Wolffelt
8cb372674a Changelog updates for 4.x.
Regarding patrol speeds and helo fix.
2021-08-08 12:52:04 -07:00
Magnus Wolffelt
57f49a52e2 Use more sensible patrol speeds for CAP, and fix is_helo (#1492)
* Use more sensible patrol speeds for CAP, and fix is_helo
2021-08-08 12:52:04 -07:00
Dan Albert
1760ee39bb Changelog updates for 4.x.
(cherry picked from commit bef015eb57)
2021-08-08 12:52:04 -07:00
Magnus Wolffelt
0b38c3bbda Tweak max-speed-based patrol altitudes
(cherry picked from commit 6621421a6f)
2021-08-08 12:52:04 -07:00
Magnus Wolffelt
b6895e302e Estimate preferred patrol altitude based on max speed
(cherry picked from commit a3e3e9046f)
2021-08-08 12:52:04 -07:00
RndName
0aef7a1c80 fix for wrong patrol speed
(cherry picked from commit 04cdb6fbfc)
2021-08-08 12:52:04 -07:00
bgreman
86f9ab26e0 Update skynet plugin (#1478)
(cherry picked from commit 8c7e56a2bd)
2021-08-08 12:52:04 -07:00
RndName
ad7b9dac38 improved the validation for planned transfers
- instead of only checking if the transfer destination was captured it now checks if there is a valid route between origin and destination. This also ensures that there will be a check if the current position or next_stop was captured and therefore the transfer should be disbanded.
- disband uncompletable transfer before planning or performing (also when user cheated a base capture)

(cherry picked from commit ac088ea692)
2021-08-08 12:52:04 -07:00
Mustang-25
b6a4489bb1 Rebalanced Aircraft Planning Hierarchies
CAP List:
[+] Mig-21 #1372
[+] Su-34
[moved up] F-15C above the F-14 (probably contentious to some but IMO the AI never capitalizes on the AIM-54 range and the Eagle AI seems to do better in general)
[moved up] JF-17
[moved up] Gripen
[moved down] Su-33
[moved down] Su-27
[moved down] MiG-31
[moved down] MiG-25
[moved down] MiG-29G
[moved down] MiG-29A

* Downgraded MiGs and Sukhois that do not have Fox-3s due to this disadvantage. From personal experience, the 31s and 25s also won't use the longer range of their Fox-1s to warrant for a higher spot on the list.

CAS/BAI List:
[+] Su-33 #1367
[-] Su-24MR (dedicated recce platform, no AG munitions)
[moved up] Su-34
[moved down] Mig-19P
[moved down] UH-1

Strike List:
[-] Su-24MR (dedicated recce platform, no AG munitions)
[moved up] JF-17
[moved up] Harrier

Runway Attack List:
[+] Mirage 2000C

(cherry picked from commit 0d6f420f97)
2021-08-08 12:52:04 -07:00
Dan Albert
6ee0feebb4 Update USN 2005 faction.
https://github.com/dcs-liberation/dcs_liberation/issues/1427
(cherry picked from commit bef85963a6)
2021-08-08 12:52:04 -07:00
Mustang-25
c0e674f0e0 Replace TGP with SPJ for JF-17 CAP/SEAD.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1422.

(cherry picked from commit ee77516716)
2021-08-08 12:52:04 -07:00
Dan Albert
4406d8c1af Campaign updates from Starfire.
(cherry picked from commit 3be57efa97)
2021-08-08 12:52:04 -07:00
Dan Albert
0e6b3b6e0f Ack new campaign version for unaffected maps.
(cherry picked from commit 981d8510c2)
2021-08-08 12:52:04 -07:00
Khopa
f9187d6b59 Added Tin Shield EWR support
(cherry picked from commit 32f05dccd9)
2021-08-08 12:52:04 -07:00
Khopa
da143e403c Added NASAMS support
(cherry picked from commit 4aac2d2b7b)
2021-08-08 12:52:04 -07:00
Dan Albert
e6d2c76641 Add ALIC codes for the tin shield and NASAMS.
https://github.com/dcs-liberation/dcs_liberation/issues/1448
(cherry picked from commit 971d7e730a)
2021-08-08 12:52:04 -07:00
Dan Albert
56307df630 Update the F-16 DEAD loadout to use JSOWs.
https://github.com/dcs-liberation/dcs_liberation/issues/1448
(cherry picked from commit 06f8b9b817)
2021-08-08 12:52:04 -07:00
Dan Albert
46240ca712 Work around pydcs bug.
https://github.com/pydcs/dcs/issues/175 causes setting the AI comm
frequency to raise an exception for aircraft without preset channel
support.

(cherry picked from commit 5d8f655243)
2021-08-08 12:52:04 -07:00
Dan Albert
4c93bb143d Update pydcs to latest master.
https://github.com/dcs-liberation/dcs_liberation/issues/1448
(cherry picked from commit 0cb41469ab)
2021-08-08 12:52:04 -07:00
Dan Albert
77fc358d55 Update pydcs to latest master.
(cherry picked from commit 9f23cb35a9)
2021-08-08 12:52:04 -07:00
bgreman
eedaecab2c Vendor ruler (#1476)
* Fixes ruler module integrity issues by bringing module into source

* Changing ruler stylesheet to vaguely match DCS theme in Liberation

* Changelog

(cherry picked from commit 119d4b9514)
2021-08-08 12:52:04 -07:00
bgreman
09b29b50c9 Updates gripen support fixes legacy DEAD loadouts 2021-08-08 12:52:04 -07:00
bgreman
136c0d49b5 Adds more details to frontline movement logging (#1465)
* adds more detailed logging for frontline movement

* Fixing attribute name

* Fixing if, adding else

(cherry picked from commit 58c96e1329)
2021-08-08 12:52:04 -07:00
RndName
b1b8ad3f1a fix generation of empty transfer during cp capture
when a cp capture happens and the next cp has pending unit deliveries then they will be redeployed to the newly captured cp. The redeploy was drecreasing the num of pending unit deliveries for the old cp but was not removing them completly from the dict when all were removed

(cherry picked from commit 67fa4a8910)
2021-08-08 12:52:04 -07:00
Mustang-25
49b1764d89 Increment to Campaign v8.0
(cherry picked from commit 0117ab8aa4)
2021-08-08 12:52:04 -07:00
Mustang-25
675bfdf628 Increment to Campaign v8.0
(cherry picked from commit a5ade0c41a)
2021-08-08 12:52:04 -07:00
Mustang-25
575da95581 Increment to Campaign v8.0
(cherry picked from commit 4df12ae675)
2021-08-08 12:52:04 -07:00
Mustang-25
41a9d1194d Increment to Campaign v8.0
(cherry picked from commit 274a41f052)
2021-08-08 12:52:04 -07:00
Mustang-25
4f7f93ebd8 Increment to Campaign v8.0
(cherry picked from commit 3670c8f879)
2021-08-08 12:52:04 -07:00
Mustang-25
0f36273de1 Increment to Campaign v8.0
(cherry picked from commit e88bb442f3)
2021-08-08 12:52:04 -07:00
Dan Albert
c6a5161a2a Remove the SA-10 from Syria 2011.
They didn't get this until a few years later. This was a stand-in for
the SA-5 that DCS doesn't have, but the SA-10 is so much more capable
that it's not a good replacement.

(cherry picked from commit 80bf3c97b2)
2021-08-08 12:52:04 -07:00
Dan Albert
2295f4edfe Bump campaign version to 8.0 for latest DCS.
Building IDs changed again. Ack the change in my two campaigns which
don't use these target types.

(cherry picked from commit edbd3de4a4)
2021-08-08 12:52:04 -07:00
Dan Albert
1991c4d48f Add decorator for tracking save compat.
Used to decorate functions or methods that have save compat code for a
given major version.

```
@has_save_compat_for(5)
def foo() -> None:
    ...
```

This function will raise an error at startup if it is decorated as
having save compat for a version other than the current major version of
the game. A new major version is the definition of a save compat break,
so keeping around the old compat code serves no purpose other than
hiding initialization bugs. The compat code and the decorator should be
removed in the branch raising the error.

(cherry picked from commit cd558daf5a)
2021-08-08 12:52:04 -07:00
Dan Albert
81c2bd6c76 Correct int/float confusion in Point APIs.
The heading and distance calculations always return floats.

(cherry picked from commit 6ce02282e7)
2021-08-08 12:52:04 -07:00
Dan Albert
fb964648a8 Use Pillow types from typeshed.
(cherry picked from commit a19a0b6789)
2021-08-08 12:52:04 -07:00
Dan Albert
93c6cb34fb Update to latest pydcs.
This includes the basics that we need to get type checking for pydcs
calls.

Type checking has been disabled in a few monkey-patching cases. Patches
ought to be sent upstream (or in the case of dead unit tracking,
replaced with a better model).

(cherry picked from commit 9de08dc83f)
2021-08-08 12:52:04 -07:00
Dan Albert
be13d8c0a6 More adaptation for pydcs updates.
This is as much as we can do until pydcs actually adds the py.typed
file. Once that's added there are a few ugly monkey patching corners
that will just need `# type: ignore` for now, but we can't pre-add those
since we have mypy warning us about superfluous ignore comments.

(cherry picked from commit 96c7b87ac7)
2021-08-08 12:52:04 -07:00
Brock Greman
6a8a9ef7e9 Fixing broken group generation.
(cherry picked from commit 469dd49def)
2021-08-08 12:52:04 -07:00
Dan Albert
c0f6974d07 Fix some typing in preparation for pydcs types.
Not complete, but progress.

(cherry picked from commit 53f6a0b32b)
2021-08-08 12:52:04 -07:00
Dan Albert
4e9d661c0c Flesh out typing information, enforce.
(cherry picked from commit fb9a0fe833)
2021-08-08 12:52:04 -07:00
Dan Albert
7cfd6b7151 Disallow partially specified generics.
(cherry picked from commit 69c3d41a8a)
2021-08-08 12:52:04 -07:00
Dan Albert
65dfa8e209 Type check the contents of untyped functions.
By default mypy doesn't type check the code within an untyped function.
This enables that and fixes typing errors to accomodate it.

This did uncover a very old bug:
https://github.com/dcs-liberation/dcs_liberation/issues/1417

(cherry picked from commit fc32b98341)
2021-08-08 12:52:04 -07:00
Dan Albert
7015b3a40d Fix unreachable code issues, enable checking.
The loadout case actually could (and previously did) hide bugs from the
type checker, since mypy was smart enough to see that we were removing
None from the input it assumed that the member was non-optional, but
later modifications could cause null values, and since those came from
the UI mypy couldn't reason about this. This meant that mypy assumed the
type could not be optional and wouldn't check that case.

(cherry picked from commit 299ed88f09)
2021-08-08 12:52:04 -07:00
Dan Albert
f9c20d729b Add (mostly disabled) mypy configs.
We're missing a lot of checking right now. Most of it requires
additional cleanup. For now I've enabled what I could and will follow up
to clean up and enable more checking.

(cherry picked from commit 29753a6aa9)
2021-08-08 12:52:04 -07:00
Dan Albert
fa5cbac2fc Add documentation for turn processing.
(cherry picked from commit 7983cd8d62)
2021-08-08 12:52:04 -07:00
RndName
638903ddf7 correct display of turn statistics 2021-08-08 12:52:04 -07:00
RndName
40092bf87f replan opfor mission on sell or buy of tgos
(cherry picked from commit 7229b886e0)
2021-08-08 12:52:04 -07:00
Dan Albert
ec15c37332 Note fix for empty convoy groups.
(cherry picked from commit 8b70d2674f)
2021-08-08 12:52:04 -07:00
RndName
4b1d629e9b remove completely destroyed units from the convoy
(cherry picked from commit 8ba27cdaea)
2021-08-08 12:52:04 -07:00
bgreman
4b526e2b90 Adds Marianas Islands support (#1406)
* Implements #1399

* Reverting accidental change in generate_landmap.py

* Changelog update

* Import beacon data for Marianas.

Co-authored-by: Dan Albert <dan@gingerhq.net>
(cherry picked from commit aa328d3ef7)
2021-08-08 12:52:04 -07:00
Dan Albert
8efe66bb45 Fixup None loadouts for aircraft with no loadouts.
Aircraft that have no loadouts at all (such as the IL-78M) will have no
loadouts and thus no values in the dropdown menu. If the player toggles
the custom layout box we reset the flight's loadout to the selected
loadout, and with no loadouts in the combo box that is None, and
`Flight.loadout` isn't supposed to be optional.

Check for that case in the loadout selector and replace with an empty
loadout if that happens.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1402

(cherry picked from commit 727facfb90)
2021-08-08 12:52:04 -07:00
Dan Albert
d9dadfb43c Fix the legacy tanker.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1379

(cherry picked from commit 4add853473)
2021-08-08 12:52:04 -07:00
RndName
f250be97c7 remove prices from sam generators
The prices are only estimations due to randomization. the real price will be only known when the generator was used and the final units are known

(cherry picked from commit b2db27f9aa)
2021-08-08 12:52:04 -07:00
RndName
c8d22925ee correct prices for ewr and sams
prices will now be calculated for the whole group by the generator by
looking up the price using the  GroundUnitType wrapper

fixes #1163

(cherry picked from commit 96be6c0efe)
2021-08-08 12:52:04 -07:00
Dan Albert
7c706be82b Note the silkworm fix in the changelog.
(cherry picked from commit 3f42f1281d)
2021-08-08 12:52:04 -07:00
Mustang-25
55e5aee75a Corrected Silkworm launcher name
(cherry picked from commit bab8384803)
2021-08-08 12:52:04 -07:00
Florian
e550ca29e8 Remove the randomness from SAM group size.
(cherry picked from commit 3f65928e9d)
2021-08-08 12:52:04 -07:00
Dan Albert
229290dc3d Bump version to 4.1.0. 2021-08-08 12:52:04 -07:00
Dan Albert
670bbc0a28 4.0.1 -> 4.1.0
This includes new features now.

(cherry picked from commit 4e6659e7e8)
2021-08-08 12:52:04 -07:00
Chris Seagraves
3550166a42 Note TGO tooltip improvement in the changelog.
(cherry picked from commit 9e22d4b5df)
2021-08-08 12:52:04 -07:00
RndName
72fe24c656 fixed lua data generation
(cherry picked from commit 357361de3d)
2021-08-08 12:52:04 -07:00
RndName
201fe121ff reworked the skynet group name generation
- added information about the role of the aa site
- moved handling of ground name from tgo to the sam generator to make the tgo cleaner
- adjusted the skynet-config lua to the changes

(cherry picked from commit de443fa3f0)
2021-08-08 12:52:04 -07:00
Dan Albert
a8bd3df46f Minor formatting fix for the changelog.
(cherry picked from commit 20839853b7)
2021-08-08 12:52:04 -07:00
Chris Seagraves
1bc16fc82c Fix for crash when clear weather.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1394

(cherry picked from commit bc2539b566)
2021-08-08 12:52:04 -07:00
Dan Albert
c3f99161ca Remove debug cruft.
We don't need to print the description of every unit on startup.

(cherry picked from commit 2ef2eafdd3)
2021-08-08 12:52:04 -07:00
bgreman
fb85fca565 Addresses #478 to clean up the angle summing functionality. (#1386)
(cherry picked from commit 9bd6f9ef47)
2021-08-08 12:52:04 -07:00
bgreman
9a2041b9d2 Increasing time JTAC radio messages stay on the UI. (#1369)
- Target lost or killed: 10s -> 20s
- New target : 10s -> 30s
- Request JTAC Status: 25s -> 60s

(cherry picked from commit c8e5cefd36)
2021-08-08 12:52:04 -07:00
bgreman
104a46de85 Fixes #240 by making statistics windows axis labels integers (#1370)
(cherry picked from commit 7ba4077f9f)
2021-08-08 12:52:04 -07:00
Mustang-25
3171fd0ef7 Update TGP Restriction Dates
TGP dates to more accurately reflect IRL IOC dates.

(cherry picked from commit 151f8bf329)
2021-08-08 12:52:04 -07:00
Chris Seagraves
a58d3febcb Notes to kneeboard (#1375)
Adds global-level kneeboard notes.  Explicit save compatability with 4.0.0

(cherry picked from commit e94d48c265)
2021-08-08 12:52:04 -07:00
Fryderyk Wysocki
51577c2eb4 Update poland_2010.json (#1380)
* Update poland_2010.json

* Adding MiG-29G to PL faction

Poland has bought some MiG-29Gs from unified Germany in the early '90s

(cherry picked from commit 2a5c523afd)
2021-08-08 12:52:04 -07:00
Chris Seagraves
54ac4a387a Add Cloud Base Altitude to Weather Display (#1371)
Adds tooltip with cloud base altitude to weather panel

(cherry picked from commit f80696b724)
2021-08-08 12:52:04 -07:00
Chris Seagraves
32dc3c3170 asset reference links 😎 (#1363)
Adds urls to unit info pages that don't have data.

(cherry picked from commit 5f5b5f69e3)
2021-08-08 12:52:04 -07:00
Chris Seagraves
dc8f17774e Update main.py (#1382)
(cherry picked from commit d99f8fef09)
2021-08-08 12:52:04 -07:00
Dan Albert
320a5a8abf Add changelog section for 4.0.1.
(cherry picked from commit 0b90b53e09)
2021-08-08 12:52:04 -07:00
Simon Clark
3badda600d Fix begin campaign button on reload. 2021-08-08 12:52:04 -07:00
Dan Albert
8e6e1469d7 Merge branch 'develop-4.x' into master. 2021-06-26 12:59:07 -07:00
Dan Albert
6cc967742a Add the most important feature to the changelog.
(cherry picked from commit aa86a6e53b)
2021-06-26 12:34:51 -07:00
Brock Greman
17f2bcc9c9 Clarify the impact of non-cold flight starts.
(cherry picked from commit 34470336e4)
2021-06-26 12:29:23 -07:00
Mustang-25
9d499a1430 Update Op Mole Cricket 2010 Campaign.
Moved SAM generator at Rosh Pina so it does not spawn units on the runway.

(cherry picked from commit 5a2a89f19e)
2021-06-26 12:17:33 -07:00
Dan Albert
3b55dfad40 Revert accidental change to default pilot limit.
(cherry picked from commit 7eb4df770e)
2021-06-26 12:06:26 -07:00
Simon Clark
9d3c7a86b6 Bump campaign versions. 2021-06-26 19:29:04 +01:00
Chris Seagraves
7f68846023 Include control point name in ground object info.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/498

(cherry picked from commit ffcae66f59)
2021-06-26 11:24:20 -07:00
Dan Albert
c1534cba9e Ack campaign version update.
No scenery targets in this campaign so no work needed.

https://github.com/dcs-liberation/dcs_liberation/issues/1359
(cherry picked from commit d2df795ba7)
2021-06-26 11:19:43 -07:00
Dan Albert
eea31168c1 Remove dead campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1359
(cherry picked from commit b930e13964)
2021-06-26 11:19:42 -07:00
Dan Albert
f2de1fdac6 Fix save path for new games.
(cherry picked from commit e6bf318cdf)
2021-06-26 11:01:22 -07:00
Dan Albert
8dd29d2319 Disband unfilled incompletable transfers.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1317

(cherry picked from commit 4cfed08247)
2021-06-26 10:55:19 -07:00
Khopa
b402dad801 Mod support : Updated frenchpach to version 4.6 (Added new units VBCI and AMX-13 support) + some frenchpack units yaml tweaks 2021-06-26 19:23:27 +02:00
Khopa
7199fead00 Fixed duplicates in france 2005 faction 2021-06-26 15:23:55 +02:00
Khopa
4ff0f29fe0 Fixed yaml issue causing an issue with Leclerc MBT 2021-06-26 15:18:47 +02:00
Khopa
1b8992eb04 Updated campaign : Operation Dynamo for The Channel map 2021-06-26 14:42:42 +02:00
Khopa
ccbcf4f69a Updated campaign : Russia Small, renamed it to "From Mozdok to Maykop" 2021-06-26 13:48:55 +02:00
Khopa
0747007f58 Updated campaign : Battle for Golan Heights 2021-06-26 13:20:13 +02:00
Dan Albert
723588666f Fix save path cleanup.
(cherry picked from commit 959a13a514)
2021-06-25 23:21:49 -07:00
Chris Seagraves
94861ca477 Use directory of current save for open/save-as.
(cherry picked from commit b601d713d2)
2021-06-25 23:02:09 -07:00
Dan Albert
e8992c5bed Add "Nevada Limited Air" campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1358

(cherry picked from commit dc96d8699a)
2021-06-25 21:34:55 -07:00
Dan Albert
e841358f74 Add "Scenic Route" campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1334

(cherry picked from commit f38cdd8432)
2021-06-25 21:28:59 -07:00
Dan Albert
3c135720a0 Fix lint.
(cherry picked from commit 91655a3d5a)
2021-06-25 19:34:10 -07:00
Dan Albert
d7db290892 Move the default save game directory.
The top level DCS directory gets messy fast if we fill it with save
games.

(cherry picked from commit 7774a9b2ab)
2021-06-25 17:48:45 -07:00
Dan Albert
b7626c10da Fix targeting of carrier groups with TGOs.
The assumption that the first group is the carrier group is wrong.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1346

(cherry picked from commit 80cf8f484d)
2021-06-25 16:47:39 -07:00
Dan Albert
d79e8f46f3 State carrier requirement for Blackball.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1355

(cherry picked from commit cb7c075a61)
2021-06-25 16:35:43 -07:00
Dan Albert
278b9730cd Fix crash when buying or selling TGO units.
Updating the game destroys this window so we cannot continue with the
calls. It worked in my initial testing, so presumably it's partly
dependent on when the finalizers run.

Since the windows will be destroyed there's nothing for us to actually
update, so just remove that signal and the explicit close calls.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1344

(cherry picked from commit 4d0fb67c53)
2021-06-25 16:30:51 -07:00
Dan Albert
d187c571ea Ack campaign version bump.
Campaigns don't use scenery targets.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1341

(cherry picked from commit 380d1d4f18)
2021-06-24 18:18:07 -07:00
Dan Albert
b3705531d4 Update pydcs to use latest master.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/993

(cherry picked from commit 71832859a5)
2021-06-24 18:14:27 -07:00
docofmur
666b389821 Fixes #1337 by making ground location search look in both directions (#1338)
(cherry picked from commit a31432ad9e)
2021-06-24 13:25:41 -04:00
bgreman
ddc076b141 Implements #1331 by changing the Pass Turn button text on Turn 0. (#1333)
(cherry picked from commit 26743154d8)
2021-06-24 11:00:05 -04:00
bgreman
eee1791a79 Adds a ruler to the map (#1332)
* Adds a ruler to the map

* Updating changelog

* Updating changelog

(cherry picked from commit a50a6fa917)
2021-06-24 02:59:16 -04:00
bgreman
fb5a6d3243 Fix #1329 player loses frontline progress when skipping turn 0 (#1330)
(cherry picked from commit b43e5bac0b)
2021-06-24 02:06:26 -04:00
Dan Albert
113c00ac05 Retry reading state.json on failure.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1313

(cherry picked from commit ddaef1fb64)
2021-06-23 20:18:36 -07:00
Dan Albert
85ca85ac6d Signal game update when buying/selling TGO units.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1312

(cherry picked from commit 6f264ff5de)
2021-06-23 20:08:31 -07:00
Dan Albert
da917a7dde Fix another unit type mismatch.
(cherry picked from commit a06fc6d80f)
2021-06-23 20:02:02 -07:00
Dan Albert
b03d1599e1 Add a feature flag for pilot limits.
This doesn't currently interact very well with the auto purchase since
the procurer might by aircraft that don't have pilots available. That
should be fixed, but for the short term we should just default to not
enabling this new feature.

(cherry picked from commit 3ddfc47d3a)
2021-06-23 18:47:47 -07:00
docofmur
2b3c56ad38 Campaign version update (#1326)
Caucasus Multi part campaign version update. No map strike objects so just the version change

(cherry picked from commit 905bd05ba8)
2021-06-23 20:01:18 -04:00
bgreman
8dc35bec5a Fix empty convoys (#1327)
* Hopefully getting rid of empty convoys for good

* changing Dict to dict for type checks

(cherry picked from commit 3274f3ec35)
2021-06-23 19:51:37 -04:00
bgreman
3f4f27612b Fixes #1310 (#1325)
* Fixes #1310 by only refunding GUs if no faction CP has an attached factory.  Previously it would refund all units at the CP, including aircraft.

Also changes the CP CAPTURE cheat to work at any CP regardless of adjacency to frontline or BLUEFOR/OPFOR state.

* Fixing typing issues, changint all Dict[] types to dict[]

* Updating changelog

(cherry picked from commit c3b8c48ca2)
2021-06-23 17:19:58 -04:00
Dan Albert
17f9487fe0 Update From Caen to Evreux.
Add support for inversion and ack the version change (Normandy is
unaffected by ID updates).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1232

(cherry picked from commit d365094616)
2021-06-23 14:00:08 -07:00
Dan Albert
e15b10ae7e Ack version update for PG campaigns.
PG is unaffected by building ID changes.

(cherry picked from commit 7c76684076)
2021-06-23 13:50:01 -07:00
Dan Albert
17d56beeaa Update Vectron's Claw and Peace Spring.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1323

(cherry picked from commit 0ef27b038a)
2021-06-23 12:59:22 -07:00
Dan Albert
53c7912592 Copy initialization fix to AircraftType.
(cherry picked from commit 610a27c0e4)
2021-06-23 12:50:55 -07:00
RndName
1f318aff3c set window title empty on new game
also fixed small exception when aborting the open file dialog which lead to " as filename

fixes #1305

(cherry picked from commit 752c91a721)
2021-06-23 12:44:49 -07:00
Dan Albert
2bb1c0b3f2 Fixed missed initialization of unit data on load.
We'd only load unit data if a name lookup was done and missed it on a
type lookup. Ideally we wouldn't need to do a type lookup here until the
ground unit templates are reworked we still do.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1299

(cherry picked from commit d3d655da07)
2021-06-22 23:42:25 -07:00
Dan Albert
b057f027d5 Return pilots when canceling flight creation.
(cherry picked from commit db36cf248e)
2021-06-22 23:36:49 -07:00
Dan Albert
cc079ad44e Add the Around the Mountain campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1280

(cherry picked from commit 153d8e106e)
2021-06-22 23:29:04 -07:00
Dan Albert
974c0069e6 Add Operation Blackball campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1320

(cherry picked from commit df8829b477)
2021-06-22 23:23:04 -07:00
Dan Albert
9028109fe3 Update Syria Full campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1319

(cherry picked from commit 569bc297a8)
2021-06-22 23:20:17 -07:00
Dan Albert
db27f3b0d9 Update Northern Russia campaign.
I bumped the submitted 6.1 to 7.0 (which didn't exist when the files
were uploaded) because this campaign uses no scenery targets.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1318

(cherry picked from commit 099cbbdb64)
2021-06-22 23:17:12 -07:00
Dan Albert
cb542b6af4 Update Allied Sword.
Only change from the uploaded files is that I increased the campaign
version to 7.0 since this doesn't use any scenery targets so has no work
to do for that.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1249

(cherry picked from commit ca7469b92e)
2021-06-22 23:14:12 -07:00
Dan Albert
fcea37c340 Correct mistakenly updated campaign.
(cherry picked from commit 6db4145927)
2021-06-22 23:08:30 -07:00
Dan Albert
cf3d13f9d3 Bump campaign version to account for DCS changes.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1308

(cherry picked from commit ca93f2baff)
2021-06-22 23:04:41 -07:00
Dan Albert
6789beb4b5 Fix unit type mismatch.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1314

(cherry picked from commit 84a0a3caeb)
2021-06-22 22:55:26 -07:00
Dan Albert
8f1ec4a519 Update Operation Peace Spring.
https://github.com/dcs-liberation/dcs_liberation/issues/1303
(cherry picked from commit 7b327693e2)
2021-06-22 15:18:12 -07:00
docofmur
b8bc9d87ec Faction Audit.
Transports and mod aircraft added where needed cleaned up various
duplicates in factions.

(cherry picked from commit dba70dc6d5)
2021-06-22 15:02:04 -07:00
Mike Jones
52aff8bc30 Use pydcs has_tacan attribute to check if tankers support TACAN.
(cherry picked from commit bd1618e41d)
2021-06-22 14:35:47 -07:00
Mike Jones
5c81ac06ac Add gunfighter flag to aircraft data files.
(cherry picked from commit 08b7aff0d8)
2021-06-22 14:35:46 -07:00
Mike Jones
8364148305 Add patrol configuration to unit data files.
This allows altitude/speed of AEW&C and tankers to be configured.

(cherry picked from commit a75688f89c)
2021-06-22 14:35:45 -07:00
Mike Jones
2bcff5a5c2 Fix unit type comparisons.
When comparing UnitType against a pydcs type, use .dcs_unit_type.

(cherry picked from commit 30763b5401)
2021-06-22 14:35:43 -07:00
Chris Seagraves
c227923bdf Fix bug with file name in title with invalid save games.
(cherry picked from commit 814519248c)
2021-06-22 14:20:16 -07:00
Simon Clark
4569b1b45a Campaign clarity. 2021-06-22 17:20:35 +01:00
Simon Clark
3a193d1dd4 Add clarity for mod selection page. 2021-06-21 20:04:34 +01:00
Simon Clark
9334cba564 Add Operation Atilla campaign.
It's a Cyprus invasion campaign - what's not to like!
2021-06-21 19:45:19 +01:00
Dan Albert
4dc1daa100 Fix command line campaign generator.
(cherry picked from commit 47e038c9fa)
2021-06-20 23:46:32 -07:00
Dan Albert
0d99fc3d36 Don't order transports for incapable factions.
If these orders can't be fulfilled for the faction it will prevent the
faction from ordering any non-reserve aircraft since transports are
given priority after reserve missions, and they'll never be fulfillable.
As such, no non-reserve aircraft will ever be purchased for factions
without transport aircraft.

Factions without transport aircraft are screwed in other ways, but this
will fix their air planning for campaigns that aren't dependent on
airlift.

(cherry picked from commit e96210f48c)
2021-06-20 23:44:16 -07:00
Simon Clark
eee78288c9 Updated factions to reflect mod select changes. 2021-06-21 01:33:13 +01:00
Simon Clark
c2f112e3a6 Refactor the mod select changes, re-add accidentally deleted factions. 2021-06-21 01:14:07 +01:00
Simon Clark
ef3f7125b3 Make mod selection nicer and deprecate MB-339.
Mod selection is now done via checkbox in the new game wizard.

The MB-339 is being turned into a paid module, and the free mod no longer works, so it's been removed.
2021-06-21 00:03:22 +01:00
Dan Albert
4558088412 Revert "Don't propose missions the air wing can't plan."
This is redundant because plan_mission already checks this.

This reverts commit 3338df9836.

(cherry picked from commit d074500109)
2021-06-20 15:59:09 -07:00
Dan Albert
00ca7d0b4d Merge pull request #1208 from dcs-liberation/develop-3.x
Release 3.0.0.
2021-06-10 23:58:41 -07:00
Dan Albert
64c426653c Merge branch 'master' into develop-3.x 2021-06-10 23:46:18 -07:00
Mustang-25
a8960c9bbe Add the First Lebanon War Historical Campaign.
(cherry picked from commit 75e3b4cc84)
2021-06-10 17:39:00 -07:00
Florian
72282845e8 added missing units
(cherry picked from commit 78cd17e279)
2021-06-10 17:37:26 -07:00
Khopa
e64aff4e91 Removed helipad from golan heights campaign to avoid capture trigger error 2021-06-10 23:27:19 +02:00
Dan Albert
e192e54c90 Fix CAS commit range display.
CAS commits around the target, not its flight plan.

(cherry picked from commit 40aa7734e1)
2021-06-09 21:52:02 -07:00
Dan Albert
39b0599b7b Fix engagement distance display.
(cherry picked from commit a9dacf4a29)
2021-06-09 21:45:20 -07:00
Dan Albert
45b40e4aa3 Update Operation Mole Cricket.
https://github.com/dcs-liberation/dcs_liberation/issues/1203
(cherry picked from commit 0594e1148e)
2021-06-09 21:27:22 -07:00
Dan Albert
9887a8ff83 Add Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1202
(cherry picked from commit 9eacd1563f)
2021-06-09 21:27:21 -07:00
Dan Albert
8d3556aa4b Update mission start guidance.
(cherry picked from commit 66f82b6ff9)
2021-06-09 19:21:18 -07:00
Florian
a59c01bcfe added texts for all units
(cherry picked from commit eb6206ea57)
2021-06-09 19:10:02 -07:00
Brock Greman
fb72962f74 Fixing display of "sunny" during clear conditions at night.
(cherry picked from commit 3ad51cafa8)
2021-06-09 19:09:40 -07:00
Dan Albert
ed1dacfe7c Remove incompatible campaigns.
We have quite a few campaigns now, so removing the broken ones.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1178

(cherry picked from commit 0e68884493)
2021-06-09 19:09:12 -07:00
Florian
794de0fcbb added missing units to price table
(cherry picked from commit 366190ee99)
2021-06-09 19:04:32 -07:00
Dan Albert
9d71b2e727 Make campaign names consistent.
(cherry picked from commit 42d56a324f)
2021-06-09 18:59:05 -07:00
docofmur
5b8f626651 Campaigns for 3.0
4 campaigns updated for 3.0 1 small PG 3 for Caucasus 1 full and 2 parts based on the full

(cherry picked from commit 7d1f1ea2f7)
2021-06-09 18:59:04 -07:00
Dan Albert
461f4b82a9 Add Around the Mountain campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1199
(cherry picked from commit 30cab8e3a7)
2021-06-09 18:54:15 -07:00
Dan Albert
15653d0628 Add Operation Allied Sword campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1196
(cherry picked from commit e0e2162c6d)
2021-06-09 18:54:14 -07:00
Dan Albert
dffc631b87 Add Humble Helper campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1197
(cherry picked from commit f1582fcc10)
2021-06-09 18:54:12 -07:00
Dan Albert
17efb48b2e Make enable_and_reset not half lie.
https://github.com/dcs-liberation/dcs_liberation/issues/1185
(cherry picked from commit b8c14d69c3)
2021-06-08 21:20:15 -07:00
Dan Albert
7e85825d2b Fix typo in Incirlik runway data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1188

(cherry picked from commit 725b5083c7)
2021-06-08 21:15:04 -07:00
Dan Albert
798591b980 Fix repeated JTACs after multiple generations.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1191

(cherry picked from commit 87dd6b19bf)
2021-06-08 20:56:41 -07:00
Schneefl0cke
4a52af298c Add Recon combat role.
(cherry picked from commit e4c9d8799e)
2021-06-08 20:47:41 -07:00
dependabot[bot]
fe886a754e Bump pillow from 8.1.1 to 8.2.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.1.1 to 8.2.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit bc938db7f9)
2021-06-08 20:47:31 -07:00
bgreman
0220fa4ff6 Gripen mod support. 2021-06-08 19:47:44 -07:00
Dan Albert
e7336d8608 Fix hangar status display.
(cherry picked from commit d8881e2734)
2021-06-06 17:13:00 -07:00
Dan Albert
d77a174ac1 Label the player checkbox in the roster editor.
(cherry picked from commit 45869c428e)
2021-06-06 13:43:55 -07:00
Khopa
6348317893 Added a small WW2 campaign on Normandy map (Replacing the former Normandy Small campaign). 2021-06-06 18:49:11 +02:00
Khopa
a516cd2f80 Added a small WW2 campaign on Normandy map (Replacing the former Normandy Small campaign). 2021-06-06 18:45:02 +02:00
Dan Albert
e1aa3e9d0e Suppress events fired while rebuilding model.
(cherry picked from commit d316e13fa6)
2021-06-05 15:21:58 -07:00
Dan Albert
d1d1acf6e0 Hide incompatible campaigns by default.
https://github.com/dcs-liberation/dcs_liberation/issues/1178
(cherry picked from commit 1ea98a6ed1)
2021-06-05 15:15:53 -07:00
Dan Albert
a0e5a707fb Merge pull request #1053 from Khopa/develop_2_5_x
Release 2.5.1.
2021-05-02 13:28:45 -07:00
Dan Albert
4555a4968d Update changelog for 2.5.1. 2021-05-02 13:17:35 -07:00
SnappyComebacks
ae34e4749b Move base EWRs into their own category.
Without this we're sometimes spawning base EWRs at points far outside the base perimeter.
2021-04-28 21:07:22 -07:00
Khopa
635eee9590 Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-24 00:11:53 +02:00
Khopa
f0558c4c1e Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-23 23:45:14 +02:00
Dan Albert
637ca8fbca Stop projecting threat zones from front lines.
This is an interim improvement since we should probably be pushing the
BARCAPs into TARCAP roles when the front line is so close. This does
regress flight pathing for anything that should route around the front
(to avoid getting shot at by SHORADS and TARCAPs), but for now it's one
or the other and this is the one everyone's complaining about.

(cherry picked from commit e474748f4d)
2021-04-22 18:23:12 -07:00
Dan Albert
e4e65df976 Generalize commit range display for all patrols.
Fixes https://github.com/Khopa/dcs_liberation/issues/890

(cherry picked from commit 132ba905c7)
2021-04-22 17:55:49 -07:00
Dan Albert
29579a2aec Remove missed merge conflict marker. 2021-04-22 17:49:34 -07:00
Dan Albert
e32b43cffb Show BARCAP commit ranges by default.
BARCAP placement confuses a lot of people but this should make it more
clear.

(cherry picked from commit 208d1b82b5)
2021-04-22 17:46:29 -07:00
C. Perreau
de2e5f861b Merge pull request #1007 from Khopa/develop_2_5_x
Release 2.5.0
2021-04-22 00:08:42 +02:00
Khopa
b27a7fc71b Fixed Lint issue 2021-04-21 22:54:48 +02:00
Khopa
5861ce6146 Fixed error with Ramat David frequency (typo) 2021-04-21 22:38:08 +02:00
Khopa
c732ed556f Fixed airfields frequency on Persian Gulf 2021-04-21 22:30:08 +02:00
Khopa
be1a75e520 Fixed airfields frequency on Syria 2021-04-21 22:14:18 +02:00
Khopa
c41d10c581 Pydcs update to latest version 2021-04-21 12:57:19 +02:00
330 changed files with 4649 additions and 3959 deletions

View File

@@ -1,30 +1,88 @@
# 4.1.0
Saves from 4.0.0 are compatible with 4.1.0.
## Features/Improvements
* **[Campaign]** Air defense sites now generate a fixed number of launchers per type.
* **[Campaign]** Added support for Mariana Islands map.
* **[Campaign AI]** Adjustments to aircraft selection priorities for most mission types.
* **[Engine]** Support for DCS 2.7.4.9632 and newer, including the Marianas map, F-16 JSOWs, NASAMS, and Tin Shield EWR.
* **[Flight Planning]** CAP patrol altitudes are now set per-aircraft. By default the altitude will be set based on the aircraft's maximum speed.
* **[Flight Planning]** CAP patrol speeds are now set per-aircraft to be more suitable/sensible. By default the speed will be set based on the aircraft's maximum speed.
* **[Mission Generation]** Improvements for better support of the Skynet Plugin and long range SAMs are now acting as EWR
* **[Mods]** Support for version v1.5.0-Beta of Gripen mod. In-progress campaigns may need to re-plan Gripen flights to pick up updated loadouts.
* **[Plugins]** Increased time JTAC Autolase messages stay visible on the UI.
* **[Plugins]** Updated SkynetIADS to 2.2.0 (adds NASAMS support).
* **[UI]** Added ability to take notes and have those notes appear as a kneeboard page.
* **[UI]** Hovering over the weather information now dispalys the cloud base (meters and feet).
* **[UI]** Google search link added to unit information when there is no information provided.
* **[UI]** Control point name displayed with ground object group name on map.
* **[UI]** Buy or Replace will now show the correct price for generated ground objects like sams.
* **[UI]** Improved logging for frontline movement to be more descriptive about what happened and why.
* **[UI]** Brought ruler map module into source, which should fix file integrity issues with the module.
## Fixes
* **[Campaign]** Fixed the Silkworm generator to include launchers and not all radars.
* **[Data]** Fixed Introduction dates for targeting pods (ATFLIR and LITENING were both a few years too early).
* **[Data]** Removed SA-10 from Syria 2011 faction.
* **[Economy]** EWRs can now be bought and sold for the correct price and can no longer be used to generate money
* **[Flight Planning]** Helicopters are now correctly identified, and will fly ingress/CAS/BAI/egress and similar at low altitude.
* **[Flight Planning]** Fixed potential issue with angles > 360° or < 0° being generated when summing two angles.
* **[Mission Generation]** The lua data for other plugins is now generated correctly
* **[Mission Generation]** Fixed problem with opfor planning missions against sold ground objects like SAMs
* **[Mission Generation]** The legacy always-available tanker option no longer prevents mission creation.
* **[Mission Generation]** Prevent the creation of a transfer order with 0 units for a rare situtation when a point was captured.
* **[Mission Generation]** Planned transfers which will be impossible after a base capture will no longer prevent the mission result submit.
* **[Mission Generation]** Fix occasional KeyError preventing mission generation when all units of the same type in a convoy were killed.
* **[Mission Generation]** Fix for AAA Flak generator using Opel Blitz preventing the mission from being generated because duplicate unit names were used.
* **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft.
* **[UI]** Statistics window tick marks are now always integers.
* **[UI]** Statistics window now shows the correct info for the turn
* **[UI]** Toggling custom loadout for an aircraft with no preset loadouts no longer breaks the flight.
# 4.0.0
Saves from 3.x are not compatible with 4.0.
## Features/Improvements
* **[Campaign]** Squadrons now have a maximum size and killed pilots replenish at a limited rate.
* **[Engine]** Support for DCS 2.7.2.7910.1 and newer, including Cyprus, F-16 JDAMs, and the Hind.
* **[Campaign]** Squadrons now (optionally, off by default) have a maximum size and killed pilots replenish at a limited rate.
* **[Campaign]** Added an option to disable levelling up of AI pilots.
* **[Campaign]** Added Russian Intervention 2015 campaign on Syria, for a small and somewhat realistic Russian COIN scenario.
* **[Campaign]** Added Operation Atilla campaign on Syria, for a reasonably large invasion of Cyprus scenario.
* **[Campaign AI]** AI will plan Tanker flights.
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
* **[Factions]** Added more tankers to factions.
* **[Flight Planner]** Added ability to plan Tankers.
* **[Modding]** Campaign format version is now 7.0 to account for DCS map changes that made scenery strike targets incompatible with existing campaigns.
* **[Mods]** Added support for the Gripen mod.
* **[Mods]** Removes MB-339PAN support, as the mod is now deprecated and no longer works with DCS 2.7+.
* **[Mission Generation]** Added support for "Neutral Dot" label options.
* **[New Game Wizard]** Mods are now selected via checkboxes in the new game wizard, not as separate factions.
* **[UI]** Ctrl click and shift click now buy or sell 5 or 10 units respectively.
* **[UI]** Multiple waypoints can now be deleted simultaneously if multiple waypoints are selected.
* **[UI]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
* **[UI]** Updated intel box text for first turn.
* **[UI]** Base Capture Cheat is now usable at all bases and can also be used to transfer player-owned bases to OPFOR.
* **[UI]** Pass Turn button is relabled as "Begin Campaign" on Turn 0.
* **[UI]** Added a ruler to the map.
* **[UI]** Liberation now saves games to `<DCS user directory>/Liberation/Saves` by default to declutter the main directory.
## Fixes
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
* **[Campaign AI]** Improved pruning of unplannable missions which should improve turn cycle time and prevent the auto-planner from quitting early.
* **[Campaign AI]** Fix auto purchase of aircraft for factions that have no transport aircraft.
* **[Campaign AI]** Fix refunding of pending aircraft purchases when a side has no factory available.
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.
* **[Mission Generation]** Fixed empty convoys not being disbanded when all units are killed/removed.
* **[Mission Generation]** Fixed player losing frontline progress when skipping from turn 0 to turn 1.
* **[Mission Generation]** Fixed issue where frontline would only search to the right for valid locations.
* **[UI]** Made non-interactive map elements less obstructive.
* **[UI]** Added support for Neutral Dot difficulty label
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.

View File

@@ -25,6 +25,7 @@ class AlicCodes:
AirDefence.SNR_75V.id: 126,
AirDefence.HQ_7_LN_SP.id: 127,
AirDefence.HQ_7_STR_SP.id: 128,
AirDefence.RLS_19J6.id: 130,
AirDefence.Roland_ADS.id: 201,
AirDefence.Patriot_str.id: 202,
AirDefence.Hawk_sr.id: 203,
@@ -33,6 +34,7 @@ class AlicCodes:
AirDefence.Hawk_cwar.id: 206,
AirDefence.Gepard.id: 207,
AirDefence.Vulcan.id: 208,
AirDefence.NASAMS_Radar_MPQ64F1.id: 209,
}
@classmethod

View File

@@ -1,51 +0,0 @@
from dcs.planes import (
Bf_109K_4,
C_101CC,
FW_190A8,
FW_190D9,
F_5E_3,
F_86F_Sabre,
I_16,
L_39ZA,
MiG_15bis,
MiG_19P,
MiG_21Bis,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C
"""
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
They'll RTB when they don't have gun ammo left
"""
GUNFIGHTERS = [
# Cold War
MiG_15bis,
MiG_19P,
MiG_21Bis,
F_86F_Sabre,
A_4E_C,
F_5E_3,
# Trainers
C_101CC,
L_39ZA,
# WW2
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
I_16,
]

View File

@@ -5,14 +5,14 @@ import inspect
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, Iterator, Optional, Set, Tuple, Union, cast
from typing import Dict, Iterator, Optional, Set, Tuple, cast, Any
from dcs.unitgroup import FlyingGroup
from dcs.weapons_data import Weapons, weapon_ids
from game.dcs.aircrafttype import AircraftType
PydcsWeapon = Dict[str, Union[int, str]]
PydcsWeapon = Any
PydcsWeaponAssignment = Tuple[int, PydcsWeapon]
@@ -83,7 +83,7 @@ class Pylon:
# configuration.
return weapon in self.allowed or weapon.cls_id == "<CLEAN>"
def equip(self, group: FlyingGroup, weapon: Weapon) -> None:
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None:
if not self.can_equip(weapon):
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
@@ -888,16 +888,16 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.ALQ_184): 1989,
Weapon.from_pydcs(Weapons.AN_ALQ_164_DECM_Pod): 1984,
# TGP Pods
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1995,
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1995,
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 1993,
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod): 1999,
Weapon.from_pydcs(Weapons.AN_AAQ_28_LITENING___Targeting_Pod_): 1999,
Weapon.from_pydcs(Weapons.AN_ASQ_228_ATFLIR___Targeting_Pod): 2003,
Weapon.from_pydcs(
Weapons.AN_ASQ_173_Laser_Spot_Tracker_Strike_CAMera__LST_SCAM_
): 1993,
Weapon.from_pydcs(Weapons.AWW_13_DATALINK_POD): 1967,
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1985,
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1985,
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1985,
Weapon.from_pydcs(Weapons.LANTIRN_Targeting_Pod): 1990,
Weapon.from_pydcs(Weapons.Lantirn_F_16): 1990,
Weapon.from_pydcs(Weapons.Lantirn_Target_Pod): 1990,
Weapon.from_pydcs(Weapons.Pavetack_F_111): 1982,
# BLU-107
Weapon.from_pydcs(Weapons.BLU_107___440lb_Anti_Runway_Penetrator_Bomb): 1983,

View File

@@ -29,8 +29,9 @@ from dcs.ships import (
CV_1143_5,
)
from dcs.terrain.terrain import Airport
from dcs.unit import Ship
from dcs.unitgroup import ShipGroup, StaticGroup
from dcs.unittype import UnitType
from dcs.unittype import UnitType, FlyingType, ShipType, VehicleType
from dcs.vehicles import (
vehicle_map,
)
@@ -44,12 +45,10 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57
plane_map["A-4E-C"] = A_4E_C
plane_map["F-22A"] = F_22A
plane_map["MB-339PAN"] = MB_339PAN
plane_map["Su-57"] = Su_57
plane_map["Hercules"] = Hercules
plane_map["JAS39Gripen"] = JAS39Gripen
@@ -89,6 +88,14 @@ vehicle_map["Toyota_bleu"] = frenchpack.DIM__TOYOTA_BLUE
vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
vehicle_map["AMX1375"] = frenchpack.AMX_13_75mm
vehicle_map["AMX1390"] = frenchpack.AMX_13_90mm
vehicle_map["VBCI"] = frenchpack.VBCI
vehicle_map["T62"] = frenchpack.Char_T_62
vehicle_map["T64BV"] = frenchpack.Char_T_64BV
vehicle_map["T72M"] = frenchpack.Char_T_72A
vehicle_map["KORNET"] = frenchpack.KORNET_ATGM
vehicle_map[highdigitsams.AAA_SON_9_Fire_Can.id] = highdigitsams.AAA_SON_9_Fire_Can
vehicle_map[highdigitsams.AAA_100mm_KS_19.id] = highdigitsams.AAA_100mm_KS_19
@@ -249,7 +256,7 @@ Aircraft livery overrides. Syntax as follows:
`Identifier` is aircraft identifier (as used troughout the file) and "LiveryName" (with double quotes)
is livery name as found in mission editor.
"""
PLANE_LIVERY_OVERRIDES = {
PLANE_LIVERY_OVERRIDES: dict[Type[FlyingType], str] = {
FA_18C_hornet: "VFA-34", # default livery for the hornet is blue angels one
}
@@ -320,7 +327,7 @@ REWARDS = {
StartingPosition = Union[ShipGroup, StaticGroup, Airport, Point]
def upgrade_to_supercarrier(unit, name: str):
def upgrade_to_supercarrier(unit: Type[ShipType], name: str) -> Type[ShipType]:
if unit == Stennis:
if name == "CVN-71 Theodore Roosevelt":
return CVN_71
@@ -353,7 +360,15 @@ def unit_type_from_name(name: str) -> Optional[Type[UnitType]]:
return None
def country_id_from_name(name):
def vehicle_type_from_name(name: str) -> Type[VehicleType]:
return vehicle_map[name]
def ship_type_from_name(name: str) -> Type[ShipType]:
return ship_map[name]
def country_id_from_name(name: str) -> int:
for k, v in country_dict.items():
if v.name == name:
return k
@@ -366,7 +381,7 @@ class DefaultLiveries:
OH_58D.Liveries = DefaultLiveries
F_16C_50.Liveries = DefaultLiveries
F_16C_50.Liveries = DefaultLiveries # type: ignore
P_51D_30_NA.Liveries = DefaultLiveries
Ju_88A4.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries

View File

@@ -29,7 +29,14 @@ from game.radio.channels import (
ViggenRadioChannelAllocator,
NoOpChannelAllocator,
)
from game.utils import Speed, kph
from game.utils import (
Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL,
Speed,
feet,
kph,
knots,
)
if TYPE_CHECKING:
from gen.aircraft import FlightData
@@ -91,11 +98,34 @@ class RadioConfig:
@dataclass(frozen=True)
class AircraftType(UnitType[FlyingType]):
class PatrolConfig:
altitude: Optional[Distance]
speed: Optional[Speed]
@classmethod
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
altitude = data.get("altitude", None)
speed = data.get("speed", None)
return PatrolConfig(
feet(altitude) if altitude is not None else None,
knots(speed) if speed is not None else None,
)
# TODO: Split into PlaneType and HelicopterType?
@dataclass(frozen=True)
class AircraftType(UnitType[Type[FlyingType]]):
carrier_capable: bool
lha_capable: bool
always_keeps_gun: bool
# If true, the aircraft does not use the guns as the last resort weapons, but as a main weapon.
# It'll RTB when it doesn't have gun ammo left.
gunfighter: bool
max_group_size: int
patrol_altitude: Optional[Distance]
patrol_speed: Optional[Speed]
intra_flight_radio: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
@@ -121,13 +151,86 @@ class AircraftType(UnitType[FlyingType]):
def max_speed(self) -> Speed:
return kph(self.dcs_unit_type.max_speed)
@property
def preferred_patrol_altitude(self) -> Distance:
if self.patrol_altitude is not None:
return self.patrol_altitude
else:
# Estimate based on max speed.
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
altitude_for_lowest_speed = feet(10 * 1000)
altitude_for_highest_speed = feet(33 * 1000)
lowest_speed = kph(600)
highest_speed = kph(2800)
factor = (self.max_speed - lowest_speed).kph / (
highest_speed - lowest_speed
).kph
altitude = (
altitude_for_lowest_speed
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
)
logging.debug(
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
)
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
return max(
altitude_for_lowest_speed,
min(altitude_for_highest_speed, rounded_altitude),
)
def preferred_patrol_speed(self, altitude: Distance) -> Speed:
"""Preferred true airspeed when patrolling"""
if self.patrol_speed is not None:
return self.patrol_speed
else:
# Estimate based on max speed.
max_speed = self.max_speed
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.6:
# Fast airplanes, should manage pretty high patrol speed
return (
Speed.from_mach(0.85, altitude)
if altitude.feet > 20000
else Speed.from_mach(0.7, altitude)
)
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.2:
# Medium-fast like F/A-18C
return (
Speed.from_mach(0.8, altitude)
if altitude.feet > 20000
else Speed.from_mach(0.65, altitude)
)
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 0.7:
# Semi-fast like airliners or similar
return (
Speed.from_mach(0.5, altitude)
if altitude.feet > 20000
else Speed.from_mach(0.4, altitude)
)
else:
# Slow like warbirds or helicopters
# Use whichever is slowest - mach 0.35 or 70% of max speed
logging.debug(f"{self.name} max_speed * 0.7 is {max_speed * 0.7}")
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
from gen.radios import ChannelInUseError, MHz
from gen.radios import ChannelInUseError, kHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)
freq = MHz(self.dcs_unit_type.radio_frequency)
# The default radio frequency is set in megahertz. For some aircraft, it is a
# floating point value. For all current aircraft, adjusting to kilohertz will be
# sufficient to convert to an integer.
in_khz = float(self.dcs_unit_type.radio_frequency) * 1000
if not in_khz.is_integer():
logging.warning(
f"Found unexpected sub-kHz default radio for {self}: {in_khz} kHz. "
"Truncating to integer. The truncated frequency may not be valid for "
"the aircraft."
)
freq = kHz(int(in_khz))
try:
radio_registry.reserve(freq)
except ChannelInUseError:
@@ -162,6 +265,8 @@ class AircraftType(UnitType[FlyingType]):
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
@@ -192,6 +297,7 @@ class AircraftType(UnitType[FlyingType]):
raise KeyError(f"Missing required price field: {data_path}") from ex
radio_config = RadioConfig.from_data(data.get("radios", {}))
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
try:
introduction = data["introduced"]
@@ -204,7 +310,10 @@ class AircraftType(UnitType[FlyingType]):
yield AircraftType(
dcs_unit_type=aircraft,
name=variant,
description=data.get("description", "No data."),
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),
@@ -213,7 +322,10 @@ class AircraftType(UnitType[FlyingType]):
carrier_capable=data.get("carrier_capable", False),
lha_capable=data.get("lha_capable", False),
always_keeps_gun=data.get("always_keeps_gun", False),
gunfighter=data.get("gunfighter", False),
max_group_size=data.get("max_group_size", aircraft.group_size_max),
patrol_altitude=patrol_config.altitude,
patrol_speed=patrol_config.speed,
intra_flight_radio=radio_config.intra_flight,
channel_allocator=radio_config.channel_allocator,
channel_namer=radio_config.channel_namer,

View File

@@ -15,7 +15,7 @@ from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[VehicleType]):
class GroundUnitType(UnitType[Type[VehicleType]]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int
@@ -45,6 +45,8 @@ class GroundUnitType(UnitType[VehicleType]):
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
@staticmethod
@@ -86,7 +88,10 @@ class GroundUnitType(UnitType[VehicleType]):
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
name=variant,
description=data.get("description", "No data."),
description=data.get(
"description",
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
),
year_introduced=introduction,
country_of_origin=data.get("origin", "No data."),
manufacturer=data.get("manufacturer", "No data."),

View File

@@ -4,12 +4,12 @@ from typing import TypeVar, Generic, Type
from dcs.unittype import UnitType as DcsUnitType
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=DcsUnitType)
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
dcs_unit_type: Type[DcsUnitTypeT]
dcs_unit_type: DcsUnitTypeT
name: str
description: str
year_introduced: str

View File

@@ -15,6 +15,7 @@ from typing import (
Iterator,
List,
TYPE_CHECKING,
Union,
)
from game import db
@@ -77,8 +78,8 @@ class GroundLosses:
player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
player_buildings: List[Building] = field(default_factory=list)
enemy_buildings: List[Building] = field(default_factory=list)
@@ -104,8 +105,9 @@ class StateData:
#: Names of vehicle (and ship) units that were killed during the mission.
killed_ground_units: List[str]
#: Names of static units that were destroyed during the mission.
destroyed_statics: List[str]
#: List of descriptions of destroyed statics. Format of each element is a mapping of
#: the coordinate type ("x", "y", "z", "type", "orientation") to the value.
destroyed_statics: List[dict[str, Union[float, str]]]
#: Mangled names of bases that were captured during the mission.
base_capture_events: List[str]
@@ -164,7 +166,7 @@ class Debriefing:
yield from self.ground_losses.enemy_airlifts
@property
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
yield from self.ground_losses.player_ground_objects
yield from self.ground_losses.enemy_ground_objects
@@ -370,32 +372,38 @@ class PollDebriefingFileThread(threading.Thread):
self.game = game
self.unit_map = unit_map
def stop(self):
def stop(self) -> None:
self._stop_event.set()
def stopped(self):
def stopped(self) -> bool:
return self._stop_event.is_set()
def run(self):
def run(self) -> None:
if os.path.isfile("state.json"):
last_modified = os.path.getmtime("state.json")
else:
last_modified = 0
while not self.stopped():
if (
os.path.isfile("state.json")
and os.path.getmtime("state.json") > last_modified
):
with open("state.json", "r") as json_file:
json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game, self.unit_map)
self.callback(debriefing)
break
try:
if (
os.path.isfile("state.json")
and os.path.getmtime("state.json") > last_modified
):
with open("state.json", "r") as json_file:
json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game, self.unit_map)
self.callback(debriefing)
break
except json.JSONDecodeError:
logging.exception(
"Failed to decode state.json. Probably attempted read while DCS "
"was still writing the file. Will retry in 5 seconds."
)
time.sleep(5)
def wait_for_debriefing(
callback: Callable[[Debriefing], None], game: Game, unit_map
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
) -> PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game, unit_map)
thread.start()

View File

@@ -1,14 +1,10 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from .event import Event
if TYPE_CHECKING:
from game.theater import ConflictTheater
class AirWarEvent(Event):
"""Event handler for the air battle"""
def __str__(self):
def __str__(self) -> str:
return "AirWar"

View File

@@ -5,7 +5,6 @@ from typing import List, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.task import Task
from dcs.unittype import VehicleType
from game import persistency
from game.debriefing import AirLosses, Debriefing
@@ -38,13 +37,13 @@ class Event:
def __init__(
self,
game,
game: Game,
from_cp: ControlPoint,
target_cp: ControlPoint,
location: Point,
attacker_name: str,
defender_name: str,
):
) -> None:
self.game = game
self.from_cp = from_cp
self.to_cp = target_cp
@@ -54,7 +53,7 @@ class Event:
@property
def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player_name
return self.attacker_name == self.game.player_faction.name
@property
def tasks(self) -> List[Type[Task]]:
@@ -220,10 +219,10 @@ class Event:
for loss in debriefing.ground_object_losses:
# TODO: This should be stored in the TGO, not in the pydcs Group.
if not hasattr(loss.group, "units_losts"):
loss.group.units_losts = []
loss.group.units_losts = [] # type: ignore
loss.group.units.remove(loss.unit)
loss.group.units_losts.append(loss.unit)
loss.group.units_losts.append(loss.unit) # type: ignore
def commit_building_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.building_losses:
@@ -265,7 +264,7 @@ class Event:
except Exception:
logging.exception(f"Could not process base capture {captured}")
def commit(self, debriefing: Debriefing):
def commit(self, debriefing: Debriefing) -> None:
logging.info("Committing mission results")
self.commit_air_losses(debriefing)
@@ -298,15 +297,16 @@ class Event:
delta = 0.0
player_won = True
status_msg: str = ""
ally_casualties = debriefing.casualty_count(cp)
enemy_casualties = debriefing.casualty_count(enemy_cp)
ally_units_alive = cp.base.total_armor
enemy_units_alive = enemy_cp.base.total_armor
print(ally_units_alive)
print(enemy_units_alive)
print(ally_casualties)
print(enemy_casualties)
print(f"Remaining allied units: {ally_units_alive}")
print(f"Remaining enemy units: {enemy_units_alive}")
print(f"Allied casualties {ally_casualties}")
print(f"Enemy casualties {enemy_casualties}")
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
@@ -319,24 +319,31 @@ class Event:
if ally_units_alive == 0:
player_won = False
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"No allied units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces suffer a strong defeat."
elif enemy_units_alive == 0:
player_won = True
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"No enemy units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces win a strong victory."
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
player_won = False
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat."
else:
if enemy_casualties > ally_casualties:
player_won = True
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory"
else:
if ratio > 3:
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Enemy casualties massively outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a strong victory."
elif ratio < 1.5:
delta = MINOR_DEFEAT_INFLUENCE
status_msg = f"Enemy casualties minorly outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a minor victory."
else:
delta = DEFEAT_INFLUENCE
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
elif ally_casualties > enemy_casualties:
if (
@@ -346,54 +353,66 @@ class Event:
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
player_won = True
delta = MINOR_DEFEAT_INFLUENCE
status_msg = f"Despite suffering losses, allied forces still outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a minor victory."
elif (
ally_units_alive > 3 * enemy_units_alive
and player_aggresive
):
player_won = True
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Despite suffering losses, allied forces still heavily outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a major victory."
else:
# But is the enemy is not outnumbered, we lose
# But if the enemy is not outnumbered, we lose
player_won = False
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
delta = STRONG_DEFEAT_INFLUENCE
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces have overextended themselves, suffering a major defeat."
else:
delta = STRONG_DEFEAT_INFLUENCE
delta = DEFEAT_INFLUENCE
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces suffer a defeat."
# No progress with defensive strategies
if player_won and cp.stances[enemy_cp.id] in [
CombatStance.DEFENSIVE,
CombatStance.AMBUSH,
]:
print("Defensive stance, progress is limited")
print(
f"Allied forces have adopted a defensive stance along the {cp.name}-{enemy_cp.name} "
f"frontline, making only limited progress."
)
delta = MINOR_DEFEAT_INFLUENCE
if player_won:
print(cp.name + " won ! factor > " + str(delta))
cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta)
# Handle the case where there are no casualties at all on either side but both sides still have units
if delta == 0.0:
print(status_msg)
info = Information(
"Frontline Report",
"Our ground forces from "
+ cp.name
+ " are making progress toward "
+ enemy_cp.name,
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
self.game.turn,
)
self.game.informations.append(info)
else:
print(cp.name + " lost ! factor > " + str(delta))
enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta)
info = Information(
"Frontline Report",
"Our ground forces from "
+ cp.name
+ " are losing ground against the enemy forces from "
+ enemy_cp.name,
self.game.turn,
)
self.game.informations.append(info)
if player_won:
print(status_msg)
cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta)
info = Information(
"Frontline Report",
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
self.game.turn,
)
self.game.informations.append(info)
else:
print(status_msg)
enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta)
info = Information(
"Frontline Report",
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
f"{enemy_cp.name}. {status_msg}",
self.game.turn,
)
self.game.informations.append(info)
def redeploy_units(self, cp: ControlPoint) -> None:
""" "

View File

@@ -8,5 +8,5 @@ class FrontlineAttackEvent(Event):
future unique Event handling
"""
def __str__(self):
def __str__(self) -> str:
return "Frontline attack"

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import itertools
import logging
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, Iterator
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
@@ -25,6 +25,9 @@ from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@dataclass
class Faction:
@@ -81,10 +84,10 @@ class Faction:
requirements: Dict[str, str] = field(default_factory=dict)
# possible aircraft carrier units
aircraft_carrier: List[Type[UnitType]] = field(default_factory=list)
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
# possible helicopter carrier units
helicopter_carrier: List[Type[UnitType]] = field(default_factory=list)
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
# Possible carrier names
carrier_names: List[str] = field(default_factory=list)
@@ -257,6 +260,83 @@ class Faction:
if unit.unit_class is unit_class:
yield unit
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
# aircraft
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
if not mod_settings.hercules:
self.remove_aircraft("Hercules")
if not mod_settings.f22_raptor:
self.remove_aircraft("F-22A")
if not mod_settings.jas39_gripen:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_AG")
if not mod_settings.su57_felon:
self.remove_aircraft("Su-57")
# frenchpack
if not mod_settings.frenchpack:
self.remove_vehicle("AMX10RCR")
self.remove_vehicle("SEPAR")
self.remove_vehicle("ERC")
self.remove_vehicle("M120")
self.remove_vehicle("AA20")
self.remove_vehicle("TRM2000")
self.remove_vehicle("TRM2000_Citerne")
self.remove_vehicle("TRM2000_AA20")
self.remove_vehicle("TRMMISTRAL")
self.remove_vehicle("VABH")
self.remove_vehicle("VAB_RADIO")
self.remove_vehicle("VAB_50")
self.remove_vehicle("VIB_VBR")
self.remove_vehicle("VAB_HOT")
self.remove_vehicle("VAB_MORTIER")
self.remove_vehicle("VBL50")
self.remove_vehicle("VBLANF1")
self.remove_vehicle("VBL-radio")
self.remove_vehicle("VBAE")
self.remove_vehicle("VBAE_MMP")
self.remove_vehicle("AMX-30B2")
self.remove_vehicle("Tracma")
self.remove_vehicle("JTACFP")
self.remove_vehicle("SHERIDAN")
self.remove_vehicle("Leclerc_XXI")
self.remove_vehicle("Toyota_bleu")
self.remove_vehicle("Toyota_vert")
self.remove_vehicle("Toyota_desert")
self.remove_vehicle("Kamikaze")
self.remove_vehicle("AMX1375")
self.remove_vehicle("AMX1390")
self.remove_vehicle("VBCI")
self.remove_vehicle("T62")
self.remove_vehicle("T64BV")
self.remove_vehicle("T72M")
self.remove_vehicle("KORNET")
# high digit sams
if not mod_settings.high_digit_sams:
self.remove_air_defenses("SA10BGenerator")
self.remove_air_defenses("SA12Generator")
self.remove_air_defenses("SA20Generator")
self.remove_air_defenses("SA20BGenerator")
self.remove_air_defenses("SA23Generator")
self.remove_air_defenses("SA17Generator")
self.remove_air_defenses("KS19Generator")
return self
def remove_aircraft(self, name: str) -> None:
for i in self.aircrafts:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_air_defenses(self, name: str) -> None:
for i in self.air_defenses:
if i == name:
self.air_defenses.remove(i)
def remove_vehicle(self, name: str) -> None:
for i in self.frontline_units:
if i.dcs_unit_type.id == name:
self.frontline_units.remove(i)
def load_ship(name: str) -> Optional[Type[ShipType]]:
if (ship := getattr(dcs.ships, name, None)) is not None:
@@ -265,7 +345,7 @@ def load_ship(name: str) -> Optional[Type[ShipType]]:
return None
def load_all_ships(data) -> List[Type[ShipType]]:
def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
items = []
for name in data:
item = load_ship(name)

View File

@@ -1,10 +1,11 @@
import itertools
import logging
import math
import random
import sys
from datetime import date, datetime, timedelta
from enum import Enum
from typing import Any, Dict, List
from typing import Any, List, Type, Union, cast
from dcs.action import Coalition
from dcs.mapping import Point
@@ -12,7 +13,6 @@ from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence
from faker import Faker
from game import db
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
@@ -35,7 +35,7 @@ from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration
from .settings import Settings, AutoAtoBehavior
from .squadrons import AirWing
from .theater import ConflictTheater
from .theater import ConflictTheater, ControlPoint
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones
@@ -86,8 +86,8 @@ class TurnState(Enum):
class Game:
def __init__(
self,
player_name: str,
enemy_name: str,
player_faction: Faction,
enemy_faction: Faction,
theater: ConflictTheater,
start_date: datetime,
settings: Settings,
@@ -97,23 +97,23 @@ class Game:
self.settings = settings
self.events: List[Event] = []
self.theater = theater
self.player_name = player_name
self.player_country = db.FACTIONS[player_name].country
self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name].country
self.player_faction = player_faction
self.player_country = player_faction.country
self.enemy_faction = enemy_faction
self.enemy_country = enemy_faction.country
# pass_turn() will be called when initialization is complete which will
# increment this to turn 0 before it reaches the player.
self.turn = -1
# NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
self.ground_planners: Dict[int, GroundPlanner] = {}
self.notes = ""
self.ground_planners: dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = []
self.__destroyed_units: List[str] = []
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
self.savepath = ""
self.budget = player_budget
self.enemy_budget = enemy_budget
@@ -149,7 +149,7 @@ class Game:
self.on_load(game_still_initializing=True)
def __getstate__(self) -> Dict[str, Any]:
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
# Avoid persisting any volatile types that can be deterministically
# recomputed on load for the sake of save compatibility.
@@ -161,7 +161,7 @@ class Game:
del state["red_faker"]
return state
def __setstate__(self, state: Dict[str, Any]) -> None:
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
# Regenerate any state that was not persisted.
self.on_load()
@@ -188,7 +188,7 @@ class Game:
self.theater, self.current_day, self.current_turn_time_of_day, self.settings
)
def sanitize_sides(self):
def sanitize_sides(self) -> None:
"""
Make sure the opposing factions are using different countries
:return:
@@ -201,14 +201,6 @@ class Game:
else:
self.enemy_country = "Russia"
@property
def player_faction(self) -> Faction:
return db.FACTIONS[self.player_name]
@property
def enemy_faction(self) -> Faction:
return db.FACTIONS[self.enemy_name]
def faction_for(self, player: bool) -> Faction:
if player:
return self.player_faction
@@ -234,26 +226,21 @@ class Game:
return self.blue_bullseye
return self.red_bullseye
def _roll(self, prob, mult):
if self.settings.version == "dev":
# always generate all events for dev
return 100
else:
return random.randint(1, 100) <= prob * mult
def _generate_player_event(self, event_class, player_cp, enemy_cp):
def _generate_player_event(
self, event_class: Type[Event], player_cp: ControlPoint, enemy_cp: ControlPoint
) -> None:
self.events.append(
event_class(
self,
player_cp,
enemy_cp,
enemy_cp.position,
self.player_name,
self.enemy_name,
self.player_faction.name,
self.enemy_faction.name,
)
)
def _generate_events(self):
def _generate_events(self) -> None:
for front_line in self.theater.conflicts():
self._generate_player_event(
FrontlineAttackEvent,
@@ -267,21 +254,22 @@ class Game:
else:
self.enemy_budget += amount
def process_player_income(self):
def process_player_income(self) -> None:
self.budget += Income(self, player=True).total
def process_enemy_income(self):
def process_enemy_income(self) -> None:
# TODO: Clean up save compat.
if not hasattr(self, "enemy_budget"):
self.enemy_budget = 0
self.enemy_budget += Income(self, player=False).total
def initiate_event(self, event: Event) -> UnitMap:
@staticmethod
def initiate_event(event: Event) -> UnitMap:
# assert event in self.events
logging.info("Generating {} (regular)".format(event))
return event.generate()
def finish_event(self, event: Event, debriefing: Debriefing):
def finish_event(self, event: Event, debriefing: Debriefing) -> None:
logging.info("Finishing event {}".format(event))
event.commit(debriefing)
@@ -290,16 +278,6 @@ class Game:
else:
logging.info("finish_event: event not in the events!")
def is_player_attack(self, event):
if isinstance(event, Event):
return (
event
and event.attacker_name
and event.attacker_name == self.player_name
)
else:
raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self, game_still_initializing: bool = False) -> None:
if not hasattr(self, "name_generator"):
self.name_generator = naming.namegen
@@ -322,6 +300,33 @@ class Game:
self.red_ato.clear()
def finish_turn(self, skipped: bool = False) -> None:
"""Finalizes the current turn and advances to the next turn.
This handles the turn-end portion of passing a turn. Initialization of the next
turn is handled by `initialize_turn`. These are separate processes because while
turns may be initialized more than once under some circumstances (see the
documentation for `initialize_turn`), `finish_turn` performs the work that
should be guaranteed to happen only once per turn:
* Turn counter increment.
* Delivering units ordered the previous turn.
* Transfer progress.
* Squadron replenishment.
* Income distribution.
* Base strength (front line position) adjustment.
* Weather/time-of-day generation.
Some actions (like transit network assembly) will happen both here and in
`initialize_turn`. We need the network to be up to date so we can account for
base captures when processing the transfers that occurred last turn, but we also
need it to be up to date in the case of a re-initialization in `initialize_turn`
(such as to account for a cheat base capture) so that orders are only placed
where a supply route exists to the destination. This is a relatively cheap
operation so duplicating the effort is not a problem.
Args:
skipped: True if the turn was skipped.
"""
self.informations.append(
Information("End of turn #" + str(self.turn), "-" * 40, 0)
)
@@ -344,10 +349,10 @@ class Game:
self.blue_air_wing.replenish()
self.red_air_wing.replenish()
if not skipped and self.turn > 1:
if not skipped:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
else:
elif self.turn > 1:
for cp in self.theater.player_points():
if not cp.is_carrier and not cp.is_lha:
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
@@ -358,10 +363,18 @@ class Game:
self.process_player_income()
def begin_turn_0(self) -> None:
"""Initialization for the first turn of the game."""
self.turn = 0
self.initialize_turn()
def pass_turn(self, no_action: bool = False) -> None:
"""Ends the current turn and initializes the new turn.
Called both when skipping a turn or by ending the turn as the result of combat.
Args:
no_action: True if the turn was skipped.
"""
logging.info("Pass turn")
with logged_duration("Turn finalization"):
self.finish_turn(no_action)
@@ -371,7 +384,7 @@ class Game:
# Autosave progress
persistency.autosave(self)
def check_win_loss(self):
def check_win_loss(self) -> TurnState:
player_airbases = {
cp for cp in self.theater.player_points() if cp.runway_is_operational()
}
@@ -391,26 +404,90 @@ class Game:
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
def initialize_turn(self) -> None:
def initialize_turn(self, for_red: bool = True, for_blue: bool = True) -> None:
"""Performs turn initialization for the specified players.
Turn initialization performs all of the beginning-of-turn actions. *End-of-turn*
processing happens in `pass_turn` (despite the name, it's called both for
skipping the turn and ending the turn after combat).
Special care needs to be taken here because initialization can occur more than
once per turn. A number of events can require re-initializing a turn:
* Cheat capture. Bases changing hands invalidates many missions in both ATOs,
purchase orders, threat zones, transit networks, etc. Practically speaking,
after a base capture the turn needs to be treated as fully new. The game might
even be over after a capture.
* Cheat front line position. CAS missions are no longer in the correct location,
and the ground planner may also need changes.
* Selling/buying units at TGOs. Selling a TGO might leave missions in the ATO
with invalid targets. Buying a new SAM (or even replacing some units in a SAM)
potentially changes the threat zone and may alter mission priorities and
flight planning.
Most of the work is delegated to initialize_turn_for, which handles the
coalition-specific turn initialization. In some cases only one coalition will be
(re-) initialized. This is the case when buying or selling TGO units, since we
don't want to force the player to redo all their planning just because they
repaired a SAM, but should replan opfor when that happens. On the other hand,
base captures are significant enough (and likely enough to be the first thing
the player does in a turn) that we replan blue as well. Front lines are less
impactful but also likely to be early, so they also cause a blue replan.
Args:
for_red: True if opfor should be re-initialized.
for_blue: True if the player coalition should be re-initialized.
"""
self.events = []
self._generate_events()
self.set_bullseye()
# Update statistics
self.game_stats.update(self)
self.blue_air_wing.reset()
self.red_air_wing.reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Check for win or loss condition
turn_state = self.check_win_loss()
if turn_state in (TurnState.LOSS, TurnState.WIN):
return self.process_win_loss(turn_state)
# Plan Coalition specific turn
if for_red:
self.initialize_turn_for(player=False)
if for_blue:
self.initialize_turn_for(player=True)
# Plan GroundWar
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
def initialize_turn_for(self, player: bool) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
for `Game.initialize_turn`.
Args:
player: True if the player coalition is being initialized. False for opfor
initialization.
"""
self.ato_for(player).clear()
self.air_wing_for(player).reset()
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Refund all pending deliveries for opfor and if player
# has automate_aircraft_reinforcements
if (not player and not cp.captured) or (
player
and cp.captured
and self.settings.automate_aircraft_reinforcements
):
cp.pending_unit_deliveries.refund_all(self)
# Plan flights & combat for next turn
with logged_duration("Computing conflict positions"):
self.compute_conflicts_position()
@@ -420,55 +497,48 @@ class Game:
self.compute_transit_networks()
self.ground_planners = {}
self.blue_procurement_requests.clear()
self.red_procurement_requests.clear()
self.procurement_requests_for(player).clear()
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
with logged_duration("Blue mission planning"):
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
if not player or (
player and self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled
):
color = "Blue" if player else "Red"
with logged_duration(f"{color} mission planning"):
mission_planner = CoalitionMissionPlanner(self, player)
mission_planner.plan_missions()
with logged_duration("Red mission planning"):
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
self.plan_procurement_for(player)
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
self.plan_procurement()
def plan_procurement(self) -> None:
def plan_procurement_for(self, for_player: bool) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft. After that the budget will be spend proportionally based on how much is already invested
self.budget = ProcurementAi(
self,
for_player=True,
faction=self.player_faction,
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements,
).spend_budget(self.budget)
self.enemy_budget = ProcurementAi(
self,
for_player=False,
faction=self.enemy_faction,
manage_runways=True,
manage_front_line=True,
manage_aircraft=True,
).spend_budget(self.enemy_budget)
if for_player:
self.budget = ProcurementAi(
self,
for_player=True,
faction=self.player_faction,
manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements,
).spend_budget(self.budget)
else:
self.enemy_budget = ProcurementAi(
self,
for_player=False,
faction=self.enemy_faction,
manage_runways=True,
manage_front_line=True,
manage_aircraft=True,
).spend_budget(self.enemy_budget)
def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn))
@@ -481,14 +551,14 @@ class Game:
def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4)
def next_unit_id(self):
def next_unit_id(self) -> int:
"""
Next unit id for pre-generated units
"""
self.current_unit_id += 1
return self.current_unit_id
def next_group_id(self):
def next_group_id(self) -> int:
"""
Next unit id for pre-generated units
"""
@@ -522,7 +592,7 @@ class Game:
return self.blue_navmesh
return self.red_navmesh
def compute_conflicts_position(self):
def compute_conflicts_position(self) -> None:
"""
Compute the current conflict center position(s), mainly used for culling calculation
:return: List of points of interests
@@ -545,7 +615,7 @@ class Game:
# If there is no conflict take the center point between the two nearest opposing bases
if len(zones) == 0:
cpoint = None
min_distance = sys.maxsize
min_distance = math.inf
for cp in self.theater.player_points():
for cp2 in self.theater.enemy_points():
d = cp.position.distance_to_point(cp2.position)
@@ -581,15 +651,15 @@ class Game:
self.__culling_zones = zones
def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"])
def add_destroyed_units(self, data: dict[str, Union[float, str]]) -> None:
pos = Point(cast(float, data["x"]), cast(float, data["z"]))
if self.theater.is_on_land(pos):
self.__destroyed_units.append(data)
def get_destroyed_units(self):
def get_destroyed_units(self) -> list[dict[str, Union[float, str]]]:
return self.__destroyed_units
def position_culled(self, pos):
def position_culled(self, pos: Point) -> bool:
"""
Check if unit can be generated at given position depending on culling performance settings
:param pos: Position you are tryng to spawn stuff at
@@ -602,7 +672,7 @@ class Game:
return False
return True
def get_culling_zones(self):
def get_culling_zones(self) -> list[Point]:
"""
Check culling points
:return: List of culling zones
@@ -610,30 +680,28 @@ class Game:
return self.__culling_zones
# 1 = red, 2 = blue
def get_player_coalition_id(self):
def get_player_coalition_id(self) -> int:
return 2
def get_enemy_coalition_id(self):
def get_enemy_coalition_id(self) -> int:
return 1
def get_player_coalition(self):
def get_player_coalition(self) -> Coalition:
return Coalition.Blue
def get_enemy_coalition(self):
def get_enemy_coalition(self) -> Coalition:
return Coalition.Red
def get_player_color(self):
def get_player_color(self) -> str:
return "blue"
def get_enemy_color(self):
def get_enemy_color(self) -> str:
return "red"
def process_win_loss(self, turn_state: TurnState):
def process_win_loss(self, turn_state: TurnState) -> None:
if turn_state is TurnState.WIN:
return self.message(
"Congratulations, you are victorious! Start a new campaign to continue."
self.message(
"Congratulations, you are victorious! Start a new campaign to continue."
)
elif turn_state is TurnState.LOSS:
return self.message(
"Game Over, you lose. Start a new campaign to continue."
)
self.message("Game Over, you lose. Start a new campaign to continue.")

View File

@@ -2,13 +2,13 @@ import datetime
class Information:
def __init__(self, title="", text="", turn=0):
def __init__(self, title: str = "", text: str = "", turn: int = 0) -> None:
self.title = title
self.text = text
self.turn = turn
self.timestamp = datetime.datetime.now()
def __str__(self):
def __str__(self) -> str:
return "[{}][{}] {} {}".format(
self.timestamp.strftime("%Y-%m-%d %H:%M:%S")
if self.timestamp is not None

View File

@@ -1,13 +0,0 @@
class DestroyedUnit:
"""
Store info about a destroyed unit
"""
x: int
y: int
name: str
def __init__(self, x, y, name):
self.x = x
self.y = y
self.name = name

View File

@@ -1,4 +1,9 @@
from typing import List
from __future__ import annotations
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
class FactionTurnMetadata:
@@ -10,7 +15,7 @@ class FactionTurnMetadata:
vehicles_count: int = 0
sam_count: int = 0
def __init__(self):
def __init__(self) -> None:
self.aircraft_count = 0
self.vehicles_count = 0
self.sam_count = 0
@@ -24,7 +29,7 @@ class GameTurnMetadata:
allied_units: FactionTurnMetadata
enemy_units: FactionTurnMetadata
def __init__(self):
def __init__(self) -> None:
self.allied_units = FactionTurnMetadata()
self.enemy_units = FactionTurnMetadata()
@@ -34,15 +39,19 @@ class GameStats:
Store statistics for the current game
"""
def __init__(self):
def __init__(self) -> None:
self.data_per_turn: List[GameTurnMetadata] = []
def update(self, game):
def update(self, game: Game) -> None:
"""
Save data for current turn
:param game: Game we want to save the data about
"""
# Remove the current turn if its just an update for this turn
if 0 < game.turn < len(self.data_per_turn):
del self.data_per_turn[-1]
turn_data = GameTurnMetadata()
for cp in game.theater.controlpoints:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
import os
from pathlib import Path
from typing import Iterable, List, Set, TYPE_CHECKING
from typing import Iterable, List, Set, TYPE_CHECKING, cast
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@@ -62,7 +62,7 @@ class Operation:
plugin_scripts: List[str] = []
@classmethod
def prepare(cls, game: Game):
def prepare(cls, game: Game) -> None:
with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"]
cls._set_mission(Mission(game.theater.terrain))
@@ -70,20 +70,6 @@ class Operation:
cls._setup_mission_coalitions()
cls.current_mission.options.load_from_dict(options_dict)
@classmethod
def conflicts(cls) -> Iterable[Conflict]:
assert cls.game
for frontline in cls.game.theater.conflicts():
yield Conflict(
cls.game.theater,
frontline,
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
frontline.position,
)
@classmethod
def air_conflict(cls) -> Conflict:
assert cls.game
@@ -95,10 +81,10 @@ class Operation:
return Conflict(
cls.game.theater,
FrontLine(player_cp, enemy_cp),
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
cls.game.player_faction.name,
cls.game.enemy_faction.name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
mid_point,
)
@@ -107,7 +93,7 @@ class Operation:
cls.current_mission = mission
@classmethod
def _setup_mission_coalitions(cls):
def _setup_mission_coalitions(cls) -> None:
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
)
@@ -163,7 +149,7 @@ class Operation:
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator,
):
) -> None:
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [
@@ -251,7 +237,7 @@ class Operation:
# beacon list.
@classmethod
def _generate_ground_units(cls):
def _generate_ground_units(cls) -> None:
cls.groundobjectgen = GroundObjectsGenerator(
cls.current_mission,
cls.game,
@@ -266,11 +252,16 @@ class Operation:
"""Add destroyed units to the Mission"""
for d in cls.game.get_destroyed_units():
try:
utype = db.unit_type_from_name(d["type"])
type_name = d["type"]
if not isinstance(type_name, str):
raise TypeError(
"Expected the type of the destroyed static to be a string"
)
utype = db.unit_type_from_name(type_name)
except KeyError:
continue
pos = Point(d["x"], d["z"])
pos = Point(cast(float, d["x"]), cast(float, d["z"]))
if (
utype is not None
and not cls.game.position_culled(pos)
@@ -389,8 +380,8 @@ class Operation:
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict(
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_faction.name,
cls.game.enemy_faction.name,
cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country),
front_line,
@@ -418,7 +409,7 @@ class Operation:
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
@classmethod
def reset_naming_ids(cls):
def reset_naming_ids(cls) -> None:
namegen.reset_numbers()
@classmethod
@@ -439,8 +430,8 @@ class Operation:
"BlueAA": {},
} # type: ignore
for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = {
for i, tanker in enumerate(airsupportgen.air_support.tankers):
luaData["Tankers"][i] = {
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
@@ -448,23 +439,22 @@ class Operation:
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name,
}
if airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs:
luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz,
}
for i, awacs in enumerate(airsupportgen.air_support.awacs):
luaData["AWACs"][i] = {
"dcsGroupName": awacs.group_name,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz,
}
for jtac in jtacs:
luaData["JTACs"][jtac.callsign] = {
for i, jtac in enumerate(jtacs):
luaData["JTACs"][i] = {
"dcsGroupName": jtac.group_name,
"callsign": jtac.callsign,
"zone": jtac.region,
"dcsUnit": jtac.unit_name,
"laserCode": jtac.code,
}
flight_count = 0
for flight in airgen.flights:
if flight.friendly and flight.flight_type in [
FlightType.ANTISHIP,
@@ -485,7 +475,7 @@ class Operation:
elif hasattr(flightTarget, "name"):
flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flightTargetName] = {
luaData["TargetPoints"][flight_count] = {
"name": flightTargetName,
"type": flightTargetType,
"position": {
@@ -493,6 +483,7 @@ class Operation:
"y": flightTarget.position.y,
},
}
flight_count += 1
for cp in cls.game.theater.controlpoints:
for ground_object in cp.ground_objects:

View File

@@ -1,17 +1,23 @@
from __future__ import annotations
import logging
import os
import pickle
import shutil
from typing import Optional
from pathlib import Path
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from game import Game
_dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None
def setup(user_folder: str):
def setup(user_folder: str) -> None:
global _dcs_saved_game_folder
_dcs_saved_game_folder = user_folder
_file_abs_path = os.path.join(base_path(), "default.liberation")
if not save_dir().exists():
save_dir().mkdir(parents=True)
def base_path() -> str:
@@ -20,19 +26,23 @@ def base_path() -> str:
return _dcs_saved_game_folder
def save_dir() -> Path:
return Path(base_path()) / "Liberation" / "Saves"
def _temporary_save_file() -> str:
return os.path.join(base_path(), "tmpsave.liberation")
return str(save_dir() / "tmpsave.liberation")
def _autosave_path() -> str:
return os.path.join(base_path(), "autosave.liberation")
return str(save_dir() / "autosave.liberation")
def mission_path_for(name: str) -> str:
return os.path.join(base_path(), "Missions", "{}".format(name))
return os.path.join(base_path(), "Missions", name)
def load_game(path):
def load_game(path: str) -> Optional[Game]:
with open(path, "rb") as f:
try:
save = pickle.load(f)
@@ -43,7 +53,7 @@ def load_game(path):
return None
def save_game(game) -> bool:
def save_game(game: Game) -> bool:
try:
with open(_temporary_save_file(), "wb") as f:
pickle.dump(game, f)
@@ -54,7 +64,7 @@ def save_game(game) -> bool:
return False
def autosave(game) -> bool:
def autosave(game: Game) -> bool:
"""
Autosave to the autosave location
:param game: Game to save

View File

@@ -38,7 +38,7 @@ class PluginSettings:
self.settings = Settings()
self.initialize_settings()
def set_settings(self, settings: Settings):
def set_settings(self, settings: Settings) -> None:
self.settings = settings
self.initialize_settings()
@@ -146,7 +146,7 @@ class LuaPlugin(PluginSettings):
return cls(definition)
def set_settings(self, settings: Settings):
def set_settings(self, settings: Settings) -> None:
super().set_settings(settings)
for option in self.definition.options:
option.set_settings(self.settings)

View File

@@ -1,13 +1,15 @@
from __future__ import annotations
from dcs import Point
class PointWithHeading(Point):
def __init__(self):
def __init__(self) -> None:
super(PointWithHeading, self).__init__(0, 0)
self.heading = 0
@staticmethod
def from_point(point: Point, heading: int):
def from_point(point: Point, heading: int) -> PointWithHeading:
p = PointWithHeading()
p.x = point.x
p.y = point.y

9
game/positioned.py Normal file
View File

@@ -0,0 +1,9 @@
from typing import Protocol
from dcs import Point
class Positioned(Protocol):
@property
def position(self) -> Point:
raise NotImplementedError

View File

@@ -5,7 +5,8 @@ import timeit
from collections import defaultdict
from contextlib import contextmanager
from datetime import timedelta
from typing import Iterator
from types import TracebackType
from typing import Iterator, Optional, Type
@contextmanager
@@ -23,7 +24,12 @@ class MultiEventTracer:
def __enter__(self) -> MultiEventTracer:
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
def __exit__(
self,
exc_type: Optional[Type[BaseException]],
exc_val: Optional[BaseException],
exc_tb: Optional[TracebackType],
) -> None:
for event, duration in self.events.items():
logging.debug("%s took %s", event, duration)

48
game/savecompat.py Normal file
View File

@@ -0,0 +1,48 @@
"""Tools for aiding in save compat removal after compatibility breaks."""
from collections import Callable
from typing import TypeVar
from game.version import MAJOR_VERSION
ReturnT = TypeVar("ReturnT")
class DeprecatedSaveCompatError(RuntimeError):
def __init__(self, function_name: str) -> None:
super().__init__(
f"{function_name} has save compat code for a different major version."
)
def has_save_compat_for(
major: int,
) -> Callable[[Callable[..., ReturnT]], Callable[..., ReturnT]]:
"""Declares a function or method as having save compat code for a given version.
If the function has save compatibility for the current major version, there is no
change in behavior.
If the function has save compatibility for a *different* (future or past) major
version, DeprecatedSaveCompatError will be raised during startup. Since a break in
save compatibility is the definition of a major version break, there's no need to
keep around old save compat code; it only serves to mask initialization bugs.
Args:
major: The major version for which the decorated function has save
compatibility.
Returns:
The decorated function or method.
Raises:
DeprecatedSaveCompatError: The decorated function has save compat code for
another version of liberation, and that code (and the decorator declaring it)
should be removed from this branch.
"""
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
if major != MAJOR_VERSION:
raise DeprecatedSaveCompatError(func.__name__)
return func
return decorator

View File

@@ -1,7 +1,7 @@
from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum, unique
from typing import Dict, Optional
from typing import Dict, Optional, Any
from dcs.forcedoptions import ForcedOptions
@@ -34,11 +34,14 @@ class Settings:
player_income_multiplier: float = 1.0
enemy_income_multiplier: float = 1.0
#: Feature flag for squadron limits.
enable_squadron_pilot_limits: bool = False
#: The maximum number of pilots a squadron can have at one time. Changing this after
#: the campaign has started will have no immediate effect; pilots already in the
#: squadron will not be removed if the limit is lowered and pilots will not be
#: immediately created if the limit is raised.
squadron_pilot_limit: int = 24
squadron_pilot_limit: int = 12
#: The number of pilots a squadron can replace per turn.
squadron_replenishment_rate: int = 4
@@ -101,7 +104,7 @@ class Settings:
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
self.plugins[self.plugin_settings_key(identifier)] = enabled
def __setstate__(self, state) -> None:
def __setstate__(self, state: dict[str, Any]) -> None:
# __setstate__ is called with the dict of the object being unpickled. We
# can provide save compatibility for new settings options (which
# normally would not be present in the unpickled object) by creating a

View File

@@ -13,6 +13,7 @@ from typing import (
Optional,
Iterator,
Sequence,
Any,
)
import yaml
@@ -112,9 +113,19 @@ class Squadron:
return self.name
return f'{self.name} "{self.nickname}"'
@property
def pilot_limits_enabled(self) -> bool:
return self.game.settings.enable_squadron_pilot_limits
def claim_new_pilot_if_allowed(self) -> Optional[Pilot]:
if self.pilot_limits_enabled:
return None
self._recruit_pilots(1)
return self.available_pilots.pop()
def claim_available_pilot(self) -> Optional[Pilot]:
if not self.available_pilots:
return None
return self.claim_new_pilot_if_allowed()
# For opfor, so player/AI option is irrelevant.
if not self.player:
@@ -140,7 +151,7 @@ class Squadron:
# If they only *prefer* players and we're out of players, just return an AI
# pilot.
if not prefer_players:
return None
return self.claim_new_pilot_if_allowed()
return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None:
@@ -169,9 +180,12 @@ class Squadron:
self.available_pilots.extend(new_pilots)
def replenish_lost_pilots(self) -> None:
if not self.pilot_limits_enabled:
return
replenish_count = min(
self.game.settings.squadron_replenishment_rate,
self.number_of_unfilled_pilot_slots,
self._number_of_unfilled_pilot_slots,
)
if replenish_count > 0:
self._recruit_pilots(replenish_count)
@@ -183,7 +197,7 @@ class Squadron:
def send_on_leave(pilot: Pilot) -> None:
pilot.send_on_leave()
def return_from_leave(self, pilot: Pilot):
def return_from_leave(self, pilot: Pilot) -> None:
if not self.has_unfilled_pilot_slots:
raise RuntimeError(
f"Cannot return {pilot} from leave because {self} is full"
@@ -213,20 +227,23 @@ class Squadron:
return len(self.current_roster)
@property
def number_of_unfilled_pilot_slots(self) -> int:
def _number_of_unfilled_pilot_slots(self) -> int:
return self.game.settings.squadron_pilot_limit - len(self.active_pilots)
@property
def number_of_available_pilots(self) -> int:
return len(self.available_pilots)
def can_provide_pilots(self, count: int) -> bool:
return not self.pilot_limits_enabled or self.number_of_available_pilots >= count
@property
def has_available_pilots(self) -> bool:
return bool(self.available_pilots)
return not self.pilot_limits_enabled or bool(self.available_pilots)
@property
def has_unfilled_pilot_slots(self) -> bool:
return self.number_of_unfilled_pilot_slots > 0
return not self.pilot_limits_enabled or self._number_of_unfilled_pilot_slots > 0
def can_auto_assign(self, task: FlightType) -> bool:
return task in self.auto_assignable_mission_types
@@ -274,7 +291,7 @@ class Squadron:
player=player,
)
def __setstate__(self, state) -> None:
def __setstate__(self, state: dict[str, Any]) -> None:
# TODO: Remove save compat.
if "auto_assignable_mission_types" not in state:
state["auto_assignable_mission_types"] = set(state["mission_types"])
@@ -368,6 +385,13 @@ class AirWing:
def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]:
return self.squadrons[aircraft]
def can_auto_plan(self, task: FlightType) -> bool:
try:
next(self.auto_assignable_for_task(task))
return True
except StopIteration:
return False
def auto_assignable_for_task(self, task: FlightType) -> Iterator[Squadron]:
for squadron in self.iter_squadrons():
if squadron.can_auto_assign(task):

View File

@@ -6,15 +6,15 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.unittype import UnitType
BASE_MAX_STRENGTH = 1
BASE_MIN_STRENGTH = 0
BASE_MAX_STRENGTH = 1.0
BASE_MIN_STRENGTH = 0.0
class Base:
def __init__(self):
def __init__(self) -> None:
self.aircraft: dict[AircraftType, int] = {}
self.armor: dict[GroundUnitType, int] = {}
self.strength = 1
self.strength = 1.0
@property
def total_aircraft(self) -> int:
@@ -31,7 +31,7 @@ class Base:
total += unit_type.price * count
return total
def total_units_of_type(self, unit_type: UnitType) -> int:
def total_units_of_type(self, unit_type: UnitType[Any]) -> int:
return sum(
[
c
@@ -40,7 +40,7 @@ class Base:
]
)
def commission_units(self, units: dict[Any, int]):
def commission_units(self, units: dict[Any, int]) -> None:
for unit_type, unit_count in units.items():
if unit_count <= 0:
continue
@@ -56,7 +56,7 @@ class Base:
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
def commit_losses(self, units_lost: dict[Any, int]):
def commit_losses(self, units_lost: dict[Any, int]) -> None:
for unit_type, count in units_lost.items():
target_dict: dict[Any, int]
if unit_type in self.aircraft:
@@ -75,7 +75,7 @@ class Base:
if target_dict[unit_type] == 0:
del target_dict[unit_type]
def affect_strength(self, amount):
def affect_strength(self, amount: float) -> None:
self.strength += amount
if self.strength > BASE_MAX_STRENGTH:
self.strength = BASE_MAX_STRENGTH

View File

@@ -5,7 +5,7 @@ import math
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple
from typing import Any, Dict, Iterator, List, Optional, Tuple, TYPE_CHECKING
from dcs import Mission
from dcs.countries import (
@@ -29,14 +29,14 @@ from dcs.terrain import (
persiangulf,
syria,
thechannel,
marianaislands,
)
from dcs.terrain.terrain import Airport, Terrain
from dcs.unitgroup import (
FlyingGroup,
Group,
ShipGroup,
StaticGroup,
VehicleGroup,
PlaneGroup,
)
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from pyproj import CRS, Transformer
@@ -56,10 +56,14 @@ from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon
from .projections import TransverseMercator
from ..point_with_heading import PointWithHeading
from ..positioned import Positioned
from ..profiling import logged_duration
from ..scenery_group import SceneryGroup
from ..utils import Distance, meters
if TYPE_CHECKING:
from . import TheaterGroundObject
SIZE_TINY = 150
SIZE_SMALL = 600
SIZE_REGULAR = 1000
@@ -181,7 +185,7 @@ class MizCampaignLoader:
def red(self) -> Country:
return self.country(blue=False)
def off_map_spawns(self, blue: bool) -> Iterator[FlyingGroup]:
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
for group in self.country(blue).plane_group:
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
yield group
@@ -305,26 +309,26 @@ class MizCampaignLoader:
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point
for group in self.carriers(blue):
for ship in self.carriers(blue):
# TODO: Name the carrier.
control_point = Carrier(
"carrier", group.position, next(self.control_point_id)
"carrier", ship.position, next(self.control_point_id)
)
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point
for group in self.lhas(blue):
for ship in self.lhas(blue):
# TODO: Name the LHA.db
control_point = Lha("lha", group.position, next(self.control_point_id))
control_point = Lha("lha", ship.position, next(self.control_point_id))
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_point.captured_invert = ship.late_activation
control_points[control_point.id] = control_point
for group in self.fobs(blue):
for fob in self.fobs(blue):
control_point = Fob(
str(group.name), group.position, next(self.control_point_id)
str(fob.name), fob.position, next(self.control_point_id)
)
control_point.captured = blue
control_point.captured_invert = group.late_activation
control_point.captured_invert = fob.late_activation
control_points[control_point.id] = control_point
return control_points
@@ -385,22 +389,22 @@ class MizCampaignLoader:
origin, list(reversed(waypoints))
)
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(group.position)
distance = meters(closest.position.distance_to_point(group.position))
def objective_info(self, near: Positioned) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(near.position)
distance = meters(closest.position.distance_to_point(near.position))
return closest, distance
def add_preset_locations(self) -> None:
for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group)
for static in self.offshore_strike_targets:
closest, distance = self.objective_info(static)
closest.preset_locations.offshore_strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
PointWithHeading.from_point(static.position, static.units[0].heading)
)
for group in self.ships:
closest, distance = self.objective_info(group)
for ship in self.ships:
closest, distance = self.objective_info(ship)
closest.preset_locations.ships.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
PointWithHeading.from_point(ship.position, ship.units[0].heading)
)
for group in self.missile_sites:
@@ -451,33 +455,33 @@ class MizCampaignLoader:
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.helipads:
closest, distance = self.objective_info(group)
for static in self.helipads:
closest, distance = self.objective_info(static)
closest.helipads.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
PointWithHeading.from_point(static.position, static.units[0].heading)
)
for group in self.factories:
closest, distance = self.objective_info(group)
for static in self.factories:
closest, distance = self.objective_info(static)
closest.preset_locations.factories.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
PointWithHeading.from_point(static.position, static.units[0].heading)
)
for group in self.ammunition_depots:
closest, distance = self.objective_info(group)
for static in self.ammunition_depots:
closest, distance = self.objective_info(static)
closest.preset_locations.ammunition_depots.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
PointWithHeading.from_point(static.position, static.units[0].heading)
)
for group in self.strike_targets:
closest, distance = self.objective_info(group)
for static in self.strike_targets:
closest, distance = self.objective_info(static)
closest.preset_locations.strike_locations.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
PointWithHeading.from_point(static.position, static.units[0].heading)
)
for group in self.scenery:
closest, distance = self.objective_info(group)
closest.preset_locations.scenery.append(group)
for scenery_group in self.scenery:
closest, distance = self.objective_info(scenery_group)
closest.preset_locations.scenery.append(scenery_group)
def populate_theater(self) -> None:
for control_point in self.control_points.values():
@@ -504,7 +508,7 @@ class ConflictTheater:
"""
daytime_map: Dict[str, Tuple[int, int]]
def __init__(self):
def __init__(self) -> None:
self.controlpoints: List[ControlPoint] = []
self.point_to_ll_transformer = Transformer.from_crs(
self.projection_parameters.to_crs(), CRS("WGS84")
@@ -536,10 +540,12 @@ class ConflictTheater:
CRS("WGS84"), self.projection_parameters.to_crs()
)
def add_controlpoint(self, point: ControlPoint):
def add_controlpoint(self, point: ControlPoint) -> None:
self.controlpoints.append(point)
def find_ground_objects_by_obj_name(self, obj_name):
def find_ground_objects_by_obj_name(
self, obj_name: str
) -> list[TheaterGroundObject[Any]]:
found = []
for cp in self.controlpoints:
for g in cp.ground_objects:
@@ -581,12 +587,12 @@ class ConflictTheater:
return True
def nearest_land_pos(self, point: Point, extend_dist: int = 50) -> Point:
def nearest_land_pos(self, near: Point, extend_dist: int = 50) -> Point:
"""Returns the nearest point inside a land exclusion zone from point
`extend_dist` determines how far inside the zone the point should be placed"""
if self.is_on_land(point):
return point
point = geometry.Point(point.x, point.y)
if self.is_on_land(near):
return near
point = geometry.Point(near.x, near.y)
nearest_points = []
if not self.landmap:
raise RuntimeError("Landmap not initialized")
@@ -856,3 +862,22 @@ class SyriaTheater(ConflictTheater):
from .syria import PARAMETERS
return PARAMETERS
class MarianaIslandsTheater(ConflictTheater):
terrain = marianaislands.MarianaIslands()
overview_image = "marianaislands.gif"
landmap = load_landmap("resources\\marianaislandslandmap.p")
daytime_map = {
"dawn": (6, 8),
"day": (8, 16),
"dusk": (16, 18),
"night": (0, 5),
}
@property
def projection_parameters(self) -> TransverseMercator:
from .marianaislands import PARAMETERS
return PARAMETERS

View File

@@ -43,6 +43,7 @@ from .missiontarget import MissionTarget
from .theatergroundobject import (
GenericCarrierGroundObject,
TheaterGroundObject,
NavalGroundObject,
)
from ..dcs.aircrafttype import AircraftType
from ..dcs.groundunittype import GroundUnitType
@@ -290,15 +291,15 @@ class ControlPoint(MissionTarget, ABC):
at: db.StartingPosition,
size: int,
importance: float,
has_frontline=True,
cptype=ControlPointType.AIRBASE,
):
has_frontline: bool = True,
cptype: ControlPointType = ControlPointType.AIRBASE,
) -> None:
super().__init__(name, position)
# TODO: Should be Airbase specific.
self.id = cp_id
self.full_name = name
self.at = at
self.connected_objectives: List[TheaterGroundObject] = []
self.connected_objectives: List[TheaterGroundObject[Any]] = []
self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = []
@@ -322,11 +323,11 @@ class ControlPoint(MissionTarget, ABC):
self.target_position: Optional[Point] = None
def __repr__(self):
return f"<{__class__}: {self.name}>"
def __repr__(self) -> str:
return f"<{self.__class__}: {self.name}>"
@property
def ground_objects(self) -> List[TheaterGroundObject]:
def ground_objects(self) -> List[TheaterGroundObject[Any]]:
return list(self.connected_objectives)
@property
@@ -334,11 +335,11 @@ class ControlPoint(MissionTarget, ABC):
def heading(self) -> int:
...
def __str__(self):
def __str__(self) -> str:
return self.name
@property
def is_global(self):
def is_global(self) -> bool:
return not self.connected_points
def transitive_connected_friendly_points(
@@ -405,21 +406,21 @@ class ControlPoint(MissionTarget, ABC):
return False
@property
def is_carrier(self):
def is_carrier(self) -> bool:
"""
:return: Whether this control point is an aircraft carrier
"""
return False
@property
def is_fleet(self):
def is_fleet(self) -> bool:
"""
:return: Whether this control point is a boat (mobile)
"""
return False
@property
def is_lha(self):
def is_lha(self) -> bool:
"""
:return: Whether this control point is an LHA
"""
@@ -439,7 +440,7 @@ class ControlPoint(MissionTarget, ABC):
@property
@abstractmethod
def total_aircraft_parking(self):
def total_aircraft_parking(self) -> int:
"""
:return: The maximum number of aircraft that can be stored in this
control point
@@ -471,7 +472,7 @@ class ControlPoint(MissionTarget, ABC):
...
# TODO: Should be naval specific.
def get_carrier_group_name(self):
def get_carrier_group_name(self) -> Optional[str]:
"""
Get the carrier group name if the airbase is a carrier
:return: Carrier group name
@@ -497,10 +498,12 @@ class ControlPoint(MissionTarget, ABC):
return None
# TODO: Should be Airbase specific.
def is_connected(self, to) -> bool:
def is_connected(self, to: ControlPoint) -> bool:
return to in self.connected_points
def find_ground_objects_by_obj_name(self, obj_name):
def find_ground_objects_by_obj_name(
self, obj_name: str
) -> list[TheaterGroundObject[Any]]:
found = []
for g in self.ground_objects:
if g.obj_name == obj_name:
@@ -522,7 +525,7 @@ class ControlPoint(MissionTarget, ABC):
f"vehicles have been captured and sold for ${total}M."
)
def retreat_ground_units(self, game: Game):
def retreat_ground_units(self, game: Game) -> None:
# When there are multiple valid destinations, deliver units to whichever
# base is least defended first. The closest approximation of unit
# strength we have is price
@@ -748,7 +751,7 @@ class ControlPoint(MissionTarget, ABC):
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return []
@property
@@ -764,8 +767,8 @@ class ControlPoint(MissionTarget, ABC):
class Airfield(ControlPoint):
def __init__(
self, airport: Airport, size: int, importance: float, has_frontline=True
):
self, airport: Airport, size: int, importance: float, has_frontline: bool = True
) -> None:
super().__init__(
airport.id,
airport.name,
@@ -879,9 +882,12 @@ class NavalControlPoint(ControlPoint, ABC):
def heading(self) -> int:
return 0 # TODO compute heading
def find_main_tgo(self) -> TheaterGroundObject:
def find_main_tgo(self) -> GenericCarrierGroundObject:
for g in self.ground_objects:
if g.dcs_identifier in ["CARRIER", "LHA"]:
if isinstance(g, GenericCarrierGroundObject) and g.dcs_identifier in [
"CARRIER",
"LHA",
]:
return g
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
@@ -960,7 +966,7 @@ class Carrier(NavalControlPoint):
raise RuntimeError("Carriers cannot be captured")
@property
def is_carrier(self):
def is_carrier(self) -> bool:
return True
def can_operate(self, aircraft: AircraftType) -> bool:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, List, Tuple
from typing import Iterator, List, Tuple, Any
from dcs.mapping import Point
@@ -66,7 +66,15 @@ class FrontLine(MissionTarget):
self.segments: List[FrontLineSegment] = [
FrontLineSegment(a, b) for a, b in pairwise(route)
]
self.name = f"Front line {blue_point}/{red_point}"
super().__init__(
f"Front line {blue_point}/{red_point}",
self.point_from_a(self._position_distance),
)
def __setstate__(self, state: dict[str, Any]) -> None:
self.__dict__.update(state)
if not hasattr(self, "position"):
self.position = self.point_from_a(self._position_distance)
def control_point_hostile_to(self, player: bool) -> ControlPoint:
if player:
@@ -87,14 +95,6 @@ class FrontLine(MissionTarget):
]
yield from super().mission_types(for_player)
@property
def position(self):
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
@property
def points(self) -> Iterator[Point]:
yield self.segments[0].point_a
@@ -107,12 +107,12 @@ class FrontLine(MissionTarget):
return self.blue_cp, self.red_cp
@property
def attack_distance(self):
def attack_distance(self) -> float:
"""The total distance of all segments"""
return sum(i.attack_distance for i in self.segments)
@property
def attack_heading(self):
def attack_heading(self) -> float:
"""The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading
@@ -149,6 +149,9 @@ class FrontLine(MissionTarget):
)
else:
remaining_dist -= segment.attack_distance
raise RuntimeError(
f"Could not find front line point {distance} from {self.blue_cp}"
)
@property
def _position_distance(self) -> float:

View File

@@ -14,7 +14,7 @@ class Landmap:
exclusion_zones: MultiPolygon
sea_zones: MultiPolygon
def __post_init__(self):
def __post_init__(self) -> None:
if not self.inclusion_zones.is_valid:
raise RuntimeError("Inclusion zones not valid")
if not self.exclusion_zones.is_valid:
@@ -36,13 +36,5 @@ def load_landmap(filename: str) -> Optional[Landmap]:
return None
def poly_contains(x, y, poly: Union[MultiPolygon, Polygon]):
def poly_contains(x: float, y: float, poly: Union[MultiPolygon, Polygon]) -> bool:
return poly.contains(geometry.Point(x, y))
def poly_centroid(poly) -> Tuple[float, float]:
x_list = [vertex[0] for vertex in poly]
y_list = [vertex[1] for vertex in poly]
x = sum(x_list) / len(poly)
y = sum(y_list) / len(poly)
return (x, y)

View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=147,
false_easting=238417.99999989968,
false_northing=-1491840.000000048,
scale_factor=0.9996,
)

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
from collections import Sequence
from typing import Iterator, TYPE_CHECKING, List, Union
from dcs.mapping import Point
@@ -20,7 +21,7 @@ class MissionTarget:
self.name = name
self.position = position
def distance_to(self, other: MissionTarget) -> int:
def distance_to(self, other: MissionTarget) -> float:
"""Computes the distance to the given mission target."""
return self.position.distance_to_point(other.position)
@@ -45,5 +46,5 @@ class MissionTarget:
]
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return []

View File

@@ -78,20 +78,33 @@ class GeneratorSettings:
no_enemy_navy: bool
@dataclass
class ModSettings:
a4_skyhawk: bool = False
f22_raptor: bool = False
hercules: bool = False
jas39_gripen: bool = False
su57_felon: bool = False
frenchpack: bool = False
high_digit_sams: bool = False
class GameGenerator:
def __init__(
self,
player: str,
enemy: str,
player: Faction,
enemy: Faction,
theater: ConflictTheater,
settings: Settings,
generator_settings: GeneratorSettings,
mod_settings: ModSettings,
) -> None:
self.player = player
self.enemy = enemy
self.theater = theater
self.settings = settings
self.generator_settings = generator_settings
self.mod_settings = mod_settings
def generate(self) -> Game:
with logged_duration("TGO population"):
@@ -99,8 +112,8 @@ class GameGenerator:
namegen.reset()
self.prepare_theater()
game = Game(
player_name=self.player,
enemy_name=self.enemy,
player_faction=self.player.apply_mod_settings(self.mod_settings),
enemy_faction=self.enemy.apply_mod_settings(self.mod_settings),
theater=self.theater,
start_date=self.generator_settings.start_date,
settings=self.settings,
@@ -159,9 +172,9 @@ class ControlPointGroundObjectGenerator:
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
return self.game.player_faction.name
else:
return self.game.enemy_name
return self.game.enemy_faction.name
@property
def faction(self) -> Faction:

View File

@@ -2,13 +2,13 @@ from __future__ import annotations
import itertools
import logging
from typing import Iterator, List, TYPE_CHECKING, Union
from collections import Sequence
from typing import Iterator, List, TYPE_CHECKING, Union, Generic, TypeVar, Any
from dcs.mapping import Point
from dcs.triggers import TriggerZone
from dcs.unit import Unit
from dcs.unitgroup import Group
from dcs.unittype import VehicleType
from dcs.unitgroup import ShipGroup, VehicleGroup
from .. import db
from ..data.radar_db import (
@@ -47,7 +47,10 @@ NAME_BY_CATEGORY = {
}
class TheaterGroundObject(MissionTarget):
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup)
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def __init__(
self,
name: str,
@@ -66,7 +69,7 @@ class TheaterGroundObject(MissionTarget):
self.control_point = control_point
self.dcs_identifier = dcs_identifier
self.sea_object = sea_object
self.groups: List[Group] = []
self.groups: List[GroupT] = []
@property
def is_dead(self) -> bool:
@@ -147,7 +150,7 @@ class TheaterGroundObject(MissionTarget):
return True
return False
def _max_range_of_type(self, group: Group, range_type: str) -> Distance:
def _max_range_of_type(self, group: GroupT, range_type: str) -> Distance:
if not self.might_have_aa:
return meters(0)
@@ -168,13 +171,13 @@ class TheaterGroundObject(MissionTarget):
def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups)
def detection_range(self, group: Group) -> Distance:
def detection_range(self, group: GroupT) -> Distance:
return self._max_range_of_type(group, "detection_range")
def max_threat_range(self) -> Distance:
return max(self.threat_range(g) for g in self.groups)
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range")
@property
@@ -187,7 +190,7 @@ class TheaterGroundObject(MissionTarget):
return False
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return self.units
@property
@@ -206,7 +209,7 @@ class TheaterGroundObject(MissionTarget):
raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject):
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
@@ -217,7 +220,7 @@ class BuildingGroundObject(TheaterGroundObject):
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
is_fob_structure=False,
is_fob_structure: bool = False,
) -> None:
super().__init__(
name=name,
@@ -253,7 +256,7 @@ class BuildingGroundObject(TheaterGroundObject):
def kill(self) -> None:
self._dead = True
def iter_building_group(self) -> Iterator[TheaterGroundObject]:
def iter_building_group(self) -> Iterator[TheaterGroundObject[Any]]:
for tgo in self.control_point.ground_objects:
if tgo.obj_name == self.obj_name and not tgo.is_dead:
yield tgo
@@ -338,7 +341,7 @@ class FactoryGroundObject(BuildingGroundObject):
)
class NavalGroundObject(TheaterGroundObject):
class NavalGroundObject(TheaterGroundObject[ShipGroup]):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
@@ -407,7 +410,7 @@ class LhaGroundObject(GenericCarrierGroundObject):
return f"{self.faction_color}|EWR|{super().group_name}"
class MissileSiteGroundObject(TheaterGroundObject):
class MissileSiteGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self, name: str, group_id: int, position: Point, control_point: ControlPoint
) -> None:
@@ -431,14 +434,14 @@ class MissileSiteGroundObject(TheaterGroundObject):
return False
class CoastalSiteGroundObject(TheaterGroundObject):
class CoastalSiteGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
heading,
heading: int,
) -> None:
super().__init__(
name=name,
@@ -460,10 +463,10 @@ class CoastalSiteGroundObject(TheaterGroundObject):
return False
# TODO: Differentiate types.
# This type gets used both for AA sites (SAM, AAA, or SHORAD). These should each
# be split into their own types.
class SamGroundObject(TheaterGroundObject):
# The SamGroundObject represents all type of AA
# The TGO can have multiple types of units (AAA,SAM,Support...)
# Differentiation can be made during generation with the airdefensegroupgenerator
class SamGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
@@ -481,18 +484,6 @@ class SamGroundObject(TheaterGroundObject):
dcs_identifier="AA",
sea_object=False,
)
# Set by the SAM unit generator if the generated group is compatible
# with Skynet.
self.skynet_capable = False
@property
def group_name(self) -> str:
if self.skynet_capable:
# Prefix the group names of SAM sites with the side color so Skynet
# can find them.
return f"{self.faction_color}|SAM|{self.group_id}"
else:
return super().group_name
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
@@ -506,33 +497,25 @@ class SamGroundObject(TheaterGroundObject):
def might_have_aa(self) -> bool:
return True
def threat_range(self, group: Group, radar_only: bool = False) -> Distance:
def threat_range(self, group: VehicleGroup, radar_only: bool = False) -> Distance:
max_non_radar = meters(0)
live_trs = set()
max_telar_range = meters(0)
launchers = set()
for unit in group.units:
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None or not issubclass(unit_type, VehicleType):
continue
unit_type = db.vehicle_type_from_name(unit.type)
if unit_type in TRACK_RADARS:
live_trs.add(unit_type)
elif unit_type in TELARS:
max_telar_range = max(
max_telar_range, meters(getattr(unit_type, "threat_range", 0))
)
max_telar_range = max(max_telar_range, meters(unit_type.threat_range))
elif unit_type in LAUNCHER_TRACKER_PAIRS:
launchers.add(unit_type)
else:
max_non_radar = max(
max_non_radar, meters(getattr(unit_type, "threat_range", 0))
)
max_non_radar = max(max_non_radar, meters(unit_type.threat_range))
max_tel_range = meters(0)
for launcher in launchers:
if LAUNCHER_TRACKER_PAIRS[launcher] in live_trs:
max_tel_range = max(
max_tel_range, meters(getattr(launcher, "threat_range"))
)
max_tel_range = max(max_tel_range, meters(unit_type.threat_range))
if radar_only:
return max(max_tel_range, max_telar_range)
else:
@@ -547,7 +530,7 @@ class SamGroundObject(TheaterGroundObject):
return True
class VehicleGroupGroundObject(TheaterGroundObject):
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
@@ -575,7 +558,7 @@ class VehicleGroupGroundObject(TheaterGroundObject):
return True
class EwrGroundObject(TheaterGroundObject):
class EwrGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,

View File

@@ -27,7 +27,10 @@ ThreatPoly = Union[MultiPolygon, Polygon]
class ThreatZones:
def __init__(
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
self,
airbases: ThreatPoly,
air_defenses: ThreatPoly,
radar_sam_threats: ThreatPoly,
) -> None:
self.airbases = airbases
self.air_defenses = air_defenses
@@ -44,8 +47,10 @@ class ThreatZones:
boundary = self.closest_boundary(point)
return meters(boundary.distance_to_point(point))
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod
def threatened(self, position) -> bool:
def threatened(self, position) -> bool: # type: ignore
raise NotImplementedError
@threatened.register
@@ -61,8 +66,10 @@ class ThreatZones:
LineString([self.dcs_to_shapely_point(a), self.dcs_to_shapely_point(b)])
)
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod
def threatened_by_aircraft(self, target) -> bool:
def threatened_by_aircraft(self, target) -> bool: # type: ignore
raise NotImplementedError
@threatened_by_aircraft.register
@@ -82,8 +89,10 @@ class ThreatZones:
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
)
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod
def threatened_by_air_defense(self, target) -> bool:
def threatened_by_air_defense(self, target) -> bool: # type: ignore
raise NotImplementedError
@threatened_by_air_defense.register
@@ -102,8 +111,10 @@ class ThreatZones:
self.dcs_to_shapely_point(target.position)
)
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod
def threatened_by_radar_sam(self, target) -> bool:
def threatened_by_radar_sam(self, target) -> bool: # type: ignore
raise NotImplementedError
@threatened_by_radar_sam.register

View File

@@ -6,7 +6,6 @@ from collections import defaultdict
from dataclasses import dataclass, field
from functools import singledispatchmethod
from typing import (
Dict,
Generic,
Iterator,
List,
@@ -72,10 +71,18 @@ class TransferOrder:
player: bool = field(init=False)
#: The units being transferred.
units: Dict[GroundUnitType, int]
units: dict[GroundUnitType, int]
transport: Optional[Transport] = field(default=None)
def __str__(self) -> str:
"""Returns the text that should be displayed for the transfer."""
count = self.size
origin = self.origin.name
destination = self.destination.name
description = "Transfer" if self.player else "Enemy transfer"
return f"{description} of {count} units from {origin} to {destination}"
def __post_init__(self) -> None:
self.position = self.origin
self.player = self.origin.is_friendly(to_player=True)
@@ -91,12 +98,15 @@ class TransferOrder:
def kill_unit(self, unit_type: GroundUnitType) -> None:
if unit_type not in self.units or not self.units[unit_type]:
raise KeyError(f"{self.destination} has no {unit_type} remaining")
self.units[unit_type] -= 1
raise KeyError(f"{self} has no {unit_type} remaining")
if self.units[unit_type] == 1:
del self.units[unit_type]
else:
self.units[unit_type] -= 1
@property
def size(self) -> int:
return sum(c for c in self.units.values())
return sum(self.units.values())
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
@@ -105,7 +115,7 @@ class TransferOrder:
@property
def completed(self) -> bool:
return self.destination == self.position or not self.units
return self.destination == self.position or not self.size
def disband_at(self, location: ControlPoint) -> None:
logging.info(f"Units halting at {location}.")
@@ -120,22 +130,64 @@ class TransferOrder:
)
return self.transport.destination
def proceed(self) -> None:
if self.transport is None:
return
def find_escape_route(self) -> Optional[ControlPoint]:
if self.transport is not None:
return self.transport.find_escape_route()
return None
if not self.destination.is_friendly(self.player):
logging.info(f"Transfer destination {self.destination} was captured.")
if self.position.is_friendly(self.player):
self.disband_at(self.position)
elif (escape_route := self.transport.find_escape_route()) is not None:
self.disband_at(escape_route)
else:
def disband(self) -> None:
"""
Disbands the specific transfer at the current position if friendly, at a
possible escape route or kills all units if none is possible
"""
if self.position.is_friendly(self.player):
self.disband_at(self.position)
elif (escape_route := self.find_escape_route()) is not None:
self.disband_at(escape_route)
else:
logging.info(
f"No escape route available. Units were surrounded and destroyed "
"during transfer."
)
self.kill_all()
def is_completable(self, network: TransitNetwork) -> bool:
"""
Checks if the transfer can be completed with the current theater state / transit
network to ensure that there is possible route between the current position and
the planned destination. This also ensures that the points are friendly.
"""
if self.transport is None:
# Check if unplanned transfers could be completed
if not self.position.is_friendly(self.player):
logging.info(
f"No escape route available. Units were surrounded and destroyed "
"during transfer."
f"Current position ({self.position}) "
f"of the halting transfer was captured."
)
self.kill_all()
return False
if not network.has_path_between(self.position, self.destination):
logging.info(
f"Destination of transfer ({self.destination}) "
f"can not be reached anymore."
)
return False
if self.transport is not None and not self.next_stop.is_friendly(self.player):
# check if already proceeding transfers can reach the next stop
logging.info(
f"The next stop of the transfer ({self.next_stop}) "
f"was captured while transfer was on route."
)
return False
return True
def proceed(self) -> None:
"""
Let the transfer proceed to the next stop and disbands it if the next stop
is the destination
"""
if self.transport is None:
return
self.position = self.next_stop
@@ -156,7 +208,7 @@ class Airlift(Transport):
self.flight = flight
@property
def units(self) -> Dict[GroundUnitType, int]:
def units(self) -> dict[GroundUnitType, int]:
return self.transfer.units
@property
@@ -261,8 +313,12 @@ class AirliftPlanner:
required,
available_aircraft,
squadron.aircraft.dcs_unit_type.group_size_max,
squadron.number_of_available_pilots,
)
# TODO: Use number_of_available_pilots directly once feature flag is gone.
# The number of currently available pilots is not relevant when pilot limits
# are disabled.
if not squadron.can_provide_pilots(flight_size):
flight_size = squadron.number_of_available_pilots
capacity = flight_size * capacity_each
if capacity < self.transfer.size:
@@ -334,11 +390,11 @@ class MultiGroupTransport(MissionTarget, Transport):
@property
def size(self) -> int:
return sum(sum(t.units.values()) for t in self.transfers)
return sum(t.size for t in self.transfers)
@property
def units(self) -> dict[GroundUnitType, int]:
units: Dict[GroundUnitType, int] = defaultdict(int)
units: dict[GroundUnitType, int] = defaultdict(int)
for transfer in self.transfers:
for unit_type, count in transfer.units.items():
units[unit_type] += count
@@ -414,8 +470,8 @@ TransportType = TypeVar("TransportType", bound=MultiGroupTransport)
class TransportMap(Generic[TransportType]):
def __init__(self) -> None:
# Dict of origin -> destination -> transport.
self.transports: Dict[
ControlPoint, Dict[ControlPoint, TransportType]
self.transports: dict[
ControlPoint, dict[ControlPoint, TransportType]
] = defaultdict(dict)
def create_transport(
@@ -469,14 +525,14 @@ class TransportMap(Generic[TransportType]):
yield from destination_dict.values()
class ConvoyMap(TransportMap):
class ConvoyMap(TransportMap[Convoy]):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> Convoy:
return Convoy(origin, destination)
class CargoShipMap(TransportMap):
class CargoShipMap(TransportMap[CargoShip]):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> CargoShip:
@@ -542,8 +598,14 @@ class PendingTransfers:
self.pending_transfers.append(new_transfer)
return new_transfer
# Type checking ignored because singledispatchmethod doesn't work with required type
# definitions. The implementation methods are all typed, so should be fine.
@singledispatchmethod
def cancel_transport(self, transport, transfer: TransferOrder) -> None:
def cancel_transport( # type: ignore
self,
transport,
transfer: TransferOrder,
) -> None:
pass
@cancel_transport.register
@@ -576,6 +638,12 @@ class PendingTransfers:
transfer.origin.base.commission_units(transfer.units)
def perform_transfers(self) -> None:
"""
Performs completable transfers from the list of pending transfers and adds
uncompleted transfers which are en route back to the list of pending transfers.
Disbands all convoys and cargo ships
"""
self.disband_uncompletable_transfers()
incomplete = []
for transfer in self.pending_transfers:
transfer.proceed()
@@ -586,17 +654,71 @@ class PendingTransfers:
self.cargo_ships.disband_all()
def plan_transports(self) -> None:
"""
Plan transports for all pending and completable transfers which don't have a
transport assigned already. This calculates the shortest path between current
position and destination on every execution to ensure the route is adopted to
recent changes in the theater state / transit network.
"""
self.disband_uncompletable_transfers()
for transfer in self.pending_transfers:
if transfer.transport is None:
self.arrange_transport(transfer)
def disband_uncompletable_transfers(self) -> None:
"""
Disbands all transfers from the list of pending_transfers which can not be
completed anymore because the theater state changed or the transit network does
not allow a route to the destination anymore
"""
completable_transfers = []
for transfer in self.pending_transfers:
if not transfer.is_completable(self.network_for(transfer.position)):
transfer.disband()
else:
completable_transfers.append(transfer)
self.pending_transfers = completable_transfers
def order_airlift_assets(self) -> None:
for control_point in self.game.theater.controlpoints:
self.order_airlift_assets_at(control_point)
if self.game.air_wing_for(control_point.captured).can_auto_plan(
FlightType.TRANSPORT
):
self.order_airlift_assets_at(control_point)
@staticmethod
def desired_airlift_capacity(control_point: ControlPoint) -> int:
return 4 if control_point.has_factory else 0
def desired_airlift_capacity(self, control_point: ControlPoint) -> int:
if control_point.has_factory:
is_major_hub = control_point.total_aircraft_parking > 0
# Check if there is a CP which is only reachable via Airlift
transit_network = self.network_for(control_point)
for cp in self.game.theater.control_points_for(control_point.captured):
# check if the CP has no factory, is reachable from the current
# position and can only be reached with airlift connections
if (
cp.can_deploy_ground_units
and not cp.has_factory
and transit_network.has_link(control_point, cp)
and not any(
link_type
for link, link_type in transit_network.nodes[cp].items()
if not link_type == TransitConnection.Airlift
)
):
return 4
if (
is_major_hub
and cp.has_factory
and cp.total_aircraft_parking > control_point.total_aircraft_parking
):
is_major_hub = False
if is_major_hub:
# If the current CP is a major hub keep always 2 planes on reserve
return 2
return 0
def current_airlift_capacity(self, control_point: ControlPoint) -> int:
inventory = self.game.aircraft_inventory.for_control_point(control_point)
@@ -611,9 +733,16 @@ class PendingTransfers:
)
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
gap = self.desired_airlift_capacity(
control_point
) - self.current_airlift_capacity(control_point)
unclaimed_parking = control_point.unclaimed_parking(self.game)
# Buy a maximum of unclaimed_parking only to prevent that aircraft procurement
# take place at another base
gap = min(
[
self.desired_airlift_capacity(control_point)
- self.current_airlift_capacity(control_point),
unclaimed_parking,
]
)
if gap <= 0:
return
@@ -623,6 +752,10 @@ class PendingTransfers:
# aesthetic.
gap += 1
if gap > unclaimed_parking:
# Prevent to buy more aircraft than possible
return
self.game.procurement_requests_for(player=control_point.captured).append(
AircraftProcurementRequest(
control_point, nautical_miles(200), FlightType.TRANSPORT, gap

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Optional, TYPE_CHECKING, Any
from typing import Optional, TYPE_CHECKING, Any
from game.theater import ControlPoint
from .dcs.groundunittype import GroundUnitType
@@ -28,37 +28,48 @@ class PendingUnitDeliveries:
self.destination = destination
# Maps unit type to order quantity.
self.units: Dict[UnitType, int] = defaultdict(int)
self.units: dict[UnitType[Any], int] = defaultdict(int)
def __str__(self) -> str:
return f"Pending delivery to {self.destination}"
def order(self, units: Dict[UnitType, int]) -> None:
def order(self, units: dict[UnitType[Any], int]) -> None:
for k, v in units.items():
self.units[k] += v
def sell(self, units: Dict[UnitType, int]) -> None:
def sell(self, units: dict[UnitType[Any], int]) -> None:
for k, v in units.items():
self.units[k] -= v
if self.units[k] > v:
self.units[k] -= v
else:
del self.units[k]
def refund_all(self, game: Game) -> None:
self.refund(game, self.units)
self.units = defaultdict(int)
def refund(self, game: Game, units: Dict[UnitType, int]) -> None:
def refund_ground_units(self, game: Game) -> None:
ground_units: dict[UnitType[Any], int] = {
u: self.units[u] for u in self.units.keys() if isinstance(u, GroundUnitType)
}
self.refund(game, ground_units)
for gu in ground_units.keys():
del self.units[gu]
def refund(self, game: Game, units: dict[UnitType[Any], int]) -> None:
for unit_type, count in units.items():
logging.info(f"Refunding {count} {unit_type} at {self.destination.name}")
game.adjust_budget(
unit_type.price * count, player=self.destination.captured
)
def pending_orders(self, unit_type: UnitType) -> int:
def pending_orders(self, unit_type: UnitType[Any]) -> int:
pending_units = self.units.get(unit_type)
if pending_units is None:
pending_units = 0
return pending_units
def available_next_turn(self, unit_type: UnitType) -> int:
def available_next_turn(self, unit_type: UnitType[Any]) -> int:
current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units
@@ -69,12 +80,11 @@ class PendingUnitDeliveries:
f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price."
)
self.refund_all(game)
return
self.refund_ground_units(game)
bought_units: Dict[UnitType, int] = {}
units_needing_transfer: Dict[GroundUnitType, int] = {}
sold_units: Dict[UnitType, int] = {}
bought_units: dict[UnitType[Any], int] = {}
units_needing_transfer: dict[GroundUnitType, int] = {}
sold_units: dict[UnitType[Any], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
d: dict[Any, int]
@@ -102,11 +112,16 @@ class PendingUnitDeliveries:
self.destination.base.commit_losses(sold_units)
if units_needing_transfer:
if ground_unit_source is None:
raise RuntimeError(
f"ground unit source could not be found for {self.destination} but still tried to "
f"transfer units to there"
)
ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(game, ground_unit_source, units_needing_transfer)
def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[GroundUnitType, int]
self, game: Game, source: ControlPoint, units: dict[GroundUnitType, int]
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))

View File

@@ -2,10 +2,10 @@
import itertools
import math
from dataclasses import dataclass
from typing import Dict, Optional
from typing import Dict, Optional, Any, Union, TypeVar, Generic
from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unit import Vehicle, Ship
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot
@@ -27,11 +27,14 @@ class FrontLineUnit:
origin: ControlPoint
UnitT = TypeVar("UnitT", Ship, Vehicle)
@dataclass(frozen=True)
class GroundObjectUnit:
ground_object: TheaterGroundObject
group: Group
unit: Unit
class GroundObjectUnit(Generic[UnitT]):
ground_object: TheaterGroundObject[Any]
group: MovingGroup[UnitT]
unit: UnitT
@dataclass(frozen=True)
@@ -56,13 +59,13 @@ class UnitMap:
self.aircraft: Dict[str, FlyingUnit] = {}
self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit[Any]] = {}
self.buildings: Dict[str, Building] = {}
self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {}
self.airlifts: Dict[str, AirliftUnits] = {}
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
def add_aircraft(self, group: FlyingGroup[Any], flight: Flight) -> None:
for pilot, unit in zip(flight.roster.pilots, group.units):
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
@@ -85,7 +88,7 @@ class UnitMap:
return self.airfields.get(name, None)
def add_front_line_units(
self, group: Group, origin: ControlPoint, unit_type: GroundUnitType
self, group: VehicleGroup, origin: ControlPoint, unit_type: GroundUnitType
) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
@@ -100,9 +103,9 @@ class UnitMap:
def add_ground_object_units(
self,
ground_object: TheaterGroundObject,
persistence_group: Group,
miz_group: Group,
ground_object: TheaterGroundObject[Any],
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
) -> None:
"""Adds a group associated with a TGO to the unit map.
@@ -131,10 +134,10 @@ class UnitMap:
ground_object, persistence_group, persistent_unit
)
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit[Any]]:
return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
def add_convoy_units(self, group: VehicleGroup, convoy: Convoy) -> None:
for unit, unit_type in zip(group.units, convoy.iter_units()):
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
@@ -146,7 +149,7 @@ class UnitMap:
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
return self.convoys.get(name, None)
def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
def add_cargo_ship(self, group: ShipGroup, ship: CargoShip) -> None:
if len(group.units) > 1:
# Cargo ship "groups" are single units. Killing the one ship kills the whole
# transfer. If we ever want to add escorts or create multiple cargo ships in
@@ -163,7 +166,9 @@ class UnitMap:
def cargo_ship(self, name: str) -> Optional[CargoShip]:
return self.cargo_ships.get(name, None)
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
def add_airlift_units(
self, group: FlyingGroup[Any], transfer: TransferOrder
) -> None:
capacity_each = math.ceil(transfer.size / len(group.units))
for idx, transport in enumerate(group.units):
# Slice the units in groups based on the capacity of each unit. Cargo is
@@ -186,7 +191,9 @@ class UnitMap:
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None)
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
def add_building(
self, ground_object: BuildingGroundObject, group: StaticGroup
) -> None:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
# The name of the initiator in the DCS dead event will have " object"

View File

@@ -2,8 +2,9 @@ from __future__ import annotations
import itertools
import math
from collections import Iterable
from dataclasses import dataclass
from typing import Union
from typing import Union, Any
METERS_TO_FEET = 3.28084
FEET_TO_METERS = 1 / METERS_TO_FEET
@@ -16,17 +17,12 @@ MS_TO_KPH = 3.6
KPH_TO_MS = 1 / MS_TO_KPH
def heading_sum(h, a) -> int:
def heading_sum(h: int, a: int) -> int:
h += a
if h > 360:
return h - 360
elif h < 0:
return 360 + h
else:
return h
return h % 360
def opposite_heading(h):
def opposite_heading(h: int) -> int:
return heading_sum(h, 180)
@@ -185,7 +181,7 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
def pairwise(iterable):
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...

View File

@@ -1,8 +1,15 @@
from pathlib import Path
MAJOR_VERSION = 4
MINOR_VERSION = 1
MICRO_VERSION = 0
def _build_version_string() -> str:
components = ["4.0.0"]
components = [
".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:
@@ -90,4 +97,17 @@ VERSION = _build_version_string()
#:
#: Version 6.1
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
CAMPAIGN_FORMAT_VERSION = (6, 1)
#:
#: Version 7.0
#: * DCS 2.7.2.7910.1 (Cyprus update) changed the IDs of scenery strike targets. Any
#: mission using map buildings as strike targets must check and potentially recreate
#: all those objectives. This definitely affects all Syria campaigns, other maps are
#: not yet verified.
#:
#: Version 7.1
#: * Support for Mariana Islands terrain
#:
#: Version 8.0
#: * DCS 2.7.4.9632 changed scenery target IDs. Any mission using map buildings as
#: strike targets must check and potentially recreate all those objectives.
CAMPAIGN_FORMAT_VERSION = (8, 0)

View File

@@ -83,7 +83,7 @@ class Weather:
raise NotImplementedError
@staticmethod
def random_wind(minimum: int, maximum) -> WindConditions:
def random_wind(minimum: int, maximum: int) -> WindConditions:
wind_direction = random.randint(0, 360)
at_0m_factor = 1
at_2000m_factor = 2

View File

@@ -1,11 +1,12 @@
from __future__ import annotations
from game.savecompat import has_save_compat_for
import logging
import random
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable, Any
from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup
@@ -26,7 +27,6 @@ from dcs.planes import (
Su_33,
Tu_22M3,
)
from dcs.planes import IL_78M
from dcs.point import MovingPoint, PointAction
from dcs.task import (
AWACS,
@@ -66,7 +66,6 @@ from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType
from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.data.weapons import Pylon
from game.dcs.aircrafttype import AircraftType
from game.factions.faction import Faction
@@ -83,7 +82,7 @@ from game.theater.missiontarget import MissionTarget
from game.theater.theatergroundobject import TheaterGroundObject
from game.transfers import MultiGroupTransport
from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles
from game.utils import Distance, kph, meters, nautical_miles
from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.flight import (
@@ -323,7 +322,7 @@ class AircraftConflictGenerator:
@staticmethod
def livery_from_db(flight: Flight) -> Optional[str]:
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type)
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type.dcs_unit_type)
def livery_from_faction(self, flight: Flight) -> Optional[str]:
faction = self.game.faction_for(player=flight.departure.captured)
@@ -344,7 +343,7 @@ class AircraftConflictGenerator:
return livery
return None
def _setup_livery(self, flight: Flight, group: FlyingGroup) -> None:
def _setup_livery(self, flight: Flight, group: FlyingGroup[Any]) -> None:
livery = self.livery_for(flight)
if livery is None:
return
@@ -353,7 +352,7 @@ class AircraftConflictGenerator:
def _setup_group(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -385,7 +384,18 @@ class AircraftConflictGenerator:
channel = self.radio_registry.alloc_uhf()
else:
channel = flight.unit_type.alloc_flight_radio(self.radio_registry)
group.set_frequency(channel.mhz)
try:
group.set_frequency(channel.mhz)
except TypeError:
# TODO: Remote try/except when pydcs bug is fixed.
# https://github.com/pydcs/dcs/issues/175
# pydcs now emits an error when attempting to set a preset channel for an
# aircraft that doesn't support them. We're not choosing to set a preset
# here, we're just trying to set the AI's frequency. pydcs automatically
# tries to set channel 1 when it does that and doesn't suppress this new
# error.
pass
divert = None
if flight.divert is not None:
@@ -460,8 +470,8 @@ class AircraftConflictGenerator:
unit_type: Type[FlyingType],
count: int,
start_type: str,
airport: Optional[Airport] = None,
) -> FlyingGroup:
airport: Airport,
) -> FlyingGroup[Any]:
assert count > 0
logging.info("airgen: {} for {} at {}".format(unit_type, side.id, airport))
@@ -478,7 +488,7 @@ class AircraftConflictGenerator:
def _generate_inflight(
self, name: str, side: Country, flight: Flight, origin: ControlPoint
) -> FlyingGroup:
) -> FlyingGroup[Any]:
assert flight.count > 0
at = origin.position
@@ -523,7 +533,7 @@ class AircraftConflictGenerator:
count: int,
start_type: str,
at: Union[ShipGroup, StaticGroup],
) -> FlyingGroup:
) -> FlyingGroup[Any]:
assert count > 0
logging.info("airgen: {} for {} at unit {}".format(unit_type, side.id, at))
@@ -538,34 +548,18 @@ class AircraftConflictGenerator:
)
def _add_radio_waypoint(
self, group: FlyingGroup, position, altitude: Distance, airspeed: int = 600
self,
group: FlyingGroup[Any],
position: Point,
altitude: Distance,
airspeed: int = 600,
) -> MovingPoint:
point = group.add_waypoint(position, altitude.meters, airspeed)
point.alt_type = "RADIO"
return point
def _rtb_for(
self,
group: FlyingGroup,
cp: ControlPoint,
at: Optional[db.StartingPosition] = None,
):
if at is None:
at = cp.at
position = at if isinstance(at, Point) else at.position
last_waypoint = group.points[-1]
if last_waypoint is not None:
heading = position.heading_between_point(last_waypoint.position)
tod_location = position.point_from_heading(heading, RTB_DISTANCE)
self._add_radio_waypoint(group, tod_location, last_waypoint.alt)
destination_waypoint = self._add_radio_waypoint(group, position, RTB_ALTITUDE)
if isinstance(at, Airport):
group.land_at(at)
return destination_waypoint
def _at_position(self, at) -> Point:
@staticmethod
def _at_position(at: Union[Point, ShipGroup, Type[Airport]]) -> Point:
if isinstance(at, Point):
return at
elif isinstance(at, ShipGroup):
@@ -575,7 +569,7 @@ class AircraftConflictGenerator:
else:
assert False
def _setup_payload(self, flight: Flight, group: FlyingGroup) -> None:
def _setup_payload(self, flight: Flight, group: FlyingGroup[Any]) -> None:
for p in group.units:
p.pylons.clear()
@@ -595,7 +589,10 @@ class AircraftConflictGenerator:
parking_slot.unit_id = None
def generate_flights(
self, country, ato: AirTaskingOrder, dynamic_runways: Dict[str, RunwayData]
self,
country: Country,
ato: AirTaskingOrder,
dynamic_runways: Dict[str, RunwayData],
) -> None:
for package in ato.packages:
@@ -674,7 +671,7 @@ class AircraftConflictGenerator:
self.unit_map.add_aircraft(group, flight)
def set_activation_time(
self, flight: Flight, group: FlyingGroup, delay: timedelta
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the
# mission editor. Waypoint times will be relative to the group
@@ -693,7 +690,7 @@ class AircraftConflictGenerator:
self.m.triggerrules.triggers.append(activation_trigger)
def set_startup_time(
self, flight: Flight, group: FlyingGroup, delay: timedelta
self, flight: Flight, group: FlyingGroup[Any], delay: timedelta
) -> None:
# Uncontrolled causes the AI unit to spawn, but not begin startup.
group.uncontrolled = True
@@ -721,7 +718,9 @@ class AircraftConflictGenerator:
trigger.add_condition(CoalitionHasAirdrome(coalition, flight.from_cp.id))
def generate_planned_flight(self, cp, country, flight: Flight):
def generate_planned_flight(
self, cp: ControlPoint, country: Country, flight: Flight
) -> FlyingGroup[Any]:
name = namegen.next_aircraft_name(country, cp.id, flight)
try:
if flight.start_type == "In Flight":
@@ -730,13 +729,19 @@ class AircraftConflictGenerator:
)
elif isinstance(cp, NavalControlPoint):
group_name = cp.get_carrier_group_name()
carrier_group = self.m.find_group(group_name)
if not isinstance(carrier_group, ShipGroup):
raise RuntimeError(
f"Carrier group {carrier_group} is a "
"{carrier_group.__class__.__name__}, expected a ShipGroup"
)
group = self._generate_at_group(
name=name,
side=country,
unit_type=flight.unit_type.dcs_unit_type,
count=flight.count,
start_type=flight.start_type,
at=self.m.find_group(group_name),
at=carrier_group,
)
else:
if not isinstance(cp, Airfield):
@@ -767,7 +772,7 @@ class AircraftConflictGenerator:
@staticmethod
def set_reduced_fuel(
flight: Flight, group: FlyingGroup, unit_type: Type[PlaneType]
flight: Flight, group: FlyingGroup[Any], unit_type: Type[FlyingType]
) -> None:
if unit_type is Su_33:
for unit in group.units:
@@ -793,9 +798,9 @@ class AircraftConflictGenerator:
def configure_behavior(
self,
flight: Flight,
group: FlyingGroup,
group: FlyingGroup[Any],
react_on_threat: Optional[OptReactOnThreat.Values] = None,
roe: Optional[OptROE.Values] = None,
roe: Optional[int] = None,
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
restrict_jettison: Optional[bool] = None,
mission_uses_gun: bool = True,
@@ -826,13 +831,13 @@ class AircraftConflictGenerator:
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
@staticmethod
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
def configure_eplrs(group: FlyingGroup[Any], flight: Flight) -> None:
if flight.unit_type.eplrs_capable:
group.points[0].tasks.append(EPLRS(group.id))
def configure_cap(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -840,7 +845,7 @@ class AircraftConflictGenerator:
group.task = CAP.name
self._setup_group(group, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS:
if not flight.unit_type.gunfighter:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
@@ -849,7 +854,7 @@ class AircraftConflictGenerator:
def configure_sweep(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -857,7 +862,7 @@ class AircraftConflictGenerator:
group.task = FighterSweep.name
self._setup_group(group, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS:
if not flight.unit_type.gunfighter:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
@@ -866,7 +871,7 @@ class AircraftConflictGenerator:
def configure_cas(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -884,7 +889,7 @@ class AircraftConflictGenerator:
def configure_dead(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -909,7 +914,7 @@ class AircraftConflictGenerator:
def configure_sead(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -933,7 +938,7 @@ class AircraftConflictGenerator:
def configure_strike(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -951,7 +956,7 @@ class AircraftConflictGenerator:
def configure_anti_ship(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -969,7 +974,7 @@ class AircraftConflictGenerator:
def configure_runway_attack(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -987,7 +992,7 @@ class AircraftConflictGenerator:
def configure_oca_strike(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1004,7 +1009,7 @@ class AircraftConflictGenerator:
def configure_awacs(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1032,7 +1037,7 @@ class AircraftConflictGenerator:
def configure_refueling(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1058,7 +1063,7 @@ class AircraftConflictGenerator:
def configure_escort(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1074,7 +1079,7 @@ class AircraftConflictGenerator:
def configure_sead_escort(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1097,7 +1102,7 @@ class AircraftConflictGenerator:
def configure_transport(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1112,13 +1117,13 @@ class AircraftConflictGenerator:
restrict_jettison=True,
)
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(flight, group)
def setup_flight_group(
self,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
@@ -1162,7 +1167,7 @@ class AircraftConflictGenerator:
self.configure_eplrs(group, flight)
def create_waypoints(
self, group: FlyingGroup, package: Package, flight: Flight
self, group: FlyingGroup[Any], package: Package, flight: Flight
) -> None:
for waypoint in flight.points:
@@ -1182,7 +1187,7 @@ class AircraftConflictGenerator:
# under the current flight plans.
# TODO: Make this smarter, it currently selects a random unit in the group for target,
# this could be updated to make it pick the "best" two targets in the group.
if flight.unit_type is AJS37 and flight.client_count:
if flight.unit_type.dcs_unit_type is AJS37 and flight.client_count:
viggen_target_points = [
(idx, point)
for idx, point in enumerate(filtered_points)
@@ -1230,7 +1235,7 @@ class AircraftConflictGenerator:
waypoint: FlightWaypoint,
package: Package,
flight: Flight,
group: FlyingGroup,
group: FlyingGroup[Any],
) -> None:
estimator = TotEstimator(package)
start_time = estimator.mission_start_time(flight)
@@ -1273,7 +1278,7 @@ class PydcsWaypointBuilder:
def __init__(
self,
waypoint: FlightWaypoint,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
mission: Mission,
@@ -1316,7 +1321,7 @@ class PydcsWaypointBuilder:
def for_waypoint(
cls,
waypoint: FlightWaypoint,
group: FlyingGroup,
group: FlyingGroup[Any],
package: Package,
flight: Flight,
mission: Mission,
@@ -1346,9 +1351,10 @@ class PydcsWaypointBuilder:
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
"""
if (self.flight.client_count > 0 and self.flight.unit_type == AJS37) and (
self.waypoint.waypoint_type not in TARGET_WAYPOINTS
):
if (
self.flight.client_count > 0
and self.flight.unit_type.dcs_unit_type == AJS37
) and (self.waypoint.waypoint_type not in TARGET_WAYPOINTS):
return True
else:
return False
@@ -1429,7 +1435,7 @@ class CasIngressBuilder(PydcsWaypointBuilder):
if isinstance(self.flight.flight_plan, CasFlightPlan):
waypoint.add_task(
EngageTargetsInZone(
position=self.flight.flight_plan.target,
position=self.flight.flight_plan.target.position,
radius=int(self.flight.flight_plan.engagement_distance.meters),
targets=[
Targets.All.GroundUnits.GroundVehicles,
@@ -1705,6 +1711,7 @@ class CargoStopBuilder(PydcsWaypointBuilder):
class RaceTrackBuilder(PydcsWaypointBuilder):
@has_save_compat_for(4)
def build(self) -> MovingPoint:
waypoint = super().build()
@@ -1739,17 +1746,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
)
)
# TODO: Set orbit speeds for all race tracks and remove this special case.
if isinstance(flight_plan, RefuelingFlightPlan):
orbit = OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack,
speed=int(flight_plan.patrol_speed.kph),
)
else:
orbit = OrbitAction(
altitude=waypoint.alt, pattern=OrbitAction.OrbitPattern.RaceTrack
)
orbit = OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack,
speed=int(getattr(flight_plan, "patrol_speed", kph(600)).kph),
)
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
@@ -1761,7 +1762,7 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:
waypoint.add_task(Tanker())
if self.flight.unit_type != IL_78M:
if self.flight.unit_type.dcs_unit_type.tacan:
tanker_info = self.air_support.tankers[-1]
tacan = tanker_info.tacan
tacan_callsign = {

View File

@@ -1521,4 +1521,47 @@ AIRFIELD_DATA = {
runway_length=3953,
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
),
"Antonio B. Won Pat Intl": AirfieldData(
theater="MarianaIslands",
icao="PGUM",
elevation=255,
runway_length=9359,
atc=AtcData(MHz(3, 825), MHz(118, 100), MHz(38, 550), MHz(340, 200)),
ils={
"06": ("IGUM", MHz(110, 30)),
},
),
"Andersen AFB": AirfieldData(
theater="MarianaIslands",
icao="PGUA",
elevation=545,
runway_length=10490,
tacan=TacanChannel(54, TacanBand.X),
tacan_callsign="UAM",
atc=AtcData(MHz(3, 850), MHz(126, 200), MHz(38, 600), MHz(250, 100)),
),
"Rota Intl": AirfieldData(
theater="MarianaIslands",
icao="PGRO",
elevation=568,
runway_length=6105,
atc=AtcData(MHz(3, 750), MHz(123, 600), MHz(38, 400), MHz(250, 0)),
),
"Tinian Intl": AirfieldData(
theater="MarianaIslands",
icao="PGWT",
elevation=240,
runway_length=7777,
atc=AtcData(MHz(3, 800), MHz(123, 650), MHz(38, 500), MHz(250, 50)),
),
"Saipan Intl": AirfieldData(
theater="MarianaIslands",
icao="PGSN",
elevation=213,
runway_length=7790,
atc=AtcData(MHz(3, 775), MHz(125, 700), MHz(38, 450), MHz(256, 900)),
ils={
"07": ("IGSN", MHz(109, 90)),
},
),
}

View File

@@ -1,11 +1,12 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from datetime import timedelta
from typing import List, Type, Tuple, Optional
from typing import List, Type, Tuple, Optional, TYPE_CHECKING
from dcs.mission import Mission, StartType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
from dcs.unittype import UnitType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135, PlaneType
from dcs.task import (
AWACS,
ActivateBeaconCommand,
@@ -14,15 +15,17 @@ from dcs.task import (
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.unittype import UnitType
from game import db
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .flights.ai_flight_planner_db import AEWC_CAPABLE
from .naming import namegen
from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
if TYPE_CHECKING:
from game import Game
TANKER_DISTANCE = 15000
TANKER_ALT = 4572
@@ -70,7 +73,7 @@ class AirSupportConflictGenerator:
self,
mission: Mission,
conflict: Conflict,
game,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
) -> None:
@@ -95,7 +98,7 @@ class AirSupportConflictGenerator:
return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574)
def generate(self):
def generate(self) -> None:
player_cp = (
self.conflict.blue_cp
if self.conflict.blue_cp.captured
@@ -108,6 +111,11 @@ class AirSupportConflictGenerator:
for i, tanker_unit_type in enumerate(
self.game.faction_for(player=True).tankers
):
unit_type = tanker_unit_type.dcs_unit_type
if not issubclass(unit_type, PlaneType):
logging.warning(f"Refueling aircraft {unit_type} must be a plane")
continue
# TODO: Make loiter altitude a property of the unit type.
alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type)
freq = self.radio_registry.alloc_uhf()
@@ -127,7 +135,7 @@ class AirSupportConflictGenerator:
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None,
plane_type=tanker_unit_type,
plane_type=unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
@@ -177,6 +185,8 @@ class AirSupportConflictGenerator:
tanker_unit_type.name,
freq,
tacan,
start_time=None,
end_time=None,
blue=True,
)
)
@@ -195,12 +205,17 @@ class AirSupportConflictGenerator:
awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf()
unit_type = awacs_unit.dcs_unit_type
if not issubclass(unit_type, PlaneType):
logging.warning(f"AWACS aircraft {unit_type} must be a plane")
return
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(
self.mission.country(self.game.player_country)
),
plane_type=awacs_unit,
plane_type=unit_type,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
import math
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple
@@ -23,7 +24,7 @@ from dcs.task import (
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle
from dcs.unit import Vehicle, Skill
from dcs.unitgroup import VehicleGroup
from game.data.groundunitclass import GroundUnitClass
@@ -97,7 +98,7 @@ class GroundConflictGenerator:
self.unit_map = unit_map
self.jtacs: List[JtacInfo] = []
def _enemy_stance(self):
def _enemy_stance(self) -> CombatStance:
"""Picks the enemy stance according to the number of planned groups on the frontline for each side"""
if len(self.enemy_planned_combat_groups) > len(
self.player_planned_combat_groups
@@ -122,20 +123,11 @@ class GroundConflictGenerator:
]
)
@staticmethod
def _group_point(point: Point, base_distance) -> Point:
distance = random.randint(
int(base_distance * SPREAD_DISTANCE_FACTOR[0]),
int(base_distance * SPREAD_DISTANCE_FACTOR[1]),
)
return point.random_point_within(
distance, base_distance * SPREAD_DISTANCE_SIZE_FACTOR
)
def generate(self):
def generate(self) -> None:
position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater
)
frontline_vector = Conflict.frontline_vector(
self.conflict.front_line, self.game.theater
)
@@ -150,6 +142,13 @@ class GroundConflictGenerator:
self.enemy_planned_combat_groups, frontline_vector, False
)
# TODO: Differentiate AirConflict and GroundConflict classes.
if self.conflict.heading is None:
raise RuntimeError(
"Cannot generate ground units for non-ground conflict. Ground unit "
"conflicts cannot have the heading `None`."
)
# Plan combat actions for groups
self.plan_action_for_groups(
self.player_stance,
@@ -174,7 +173,7 @@ class GroundConflictGenerator:
code = 1688 - len(self.jtacs)
utype = self.game.player_faction.jtac_unit
if self.game.player_faction.jtac_unit is None:
if utype is None:
utype = AircraftType.named("MQ-9 Reaper")
jtac = self.mission.flight_group(
@@ -361,7 +360,6 @@ class GroundConflictGenerator:
self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units:
u.initial = True
u.heading = forward_heading + random.randint(-5, 5)
return True
return False
@@ -570,10 +568,10 @@ class GroundConflictGenerator:
)
# Fallback task
fallback = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
fallback.enabled = False
task = ControlledTask(GoToWaypoint(to_index=len(dcs_group.points)))
task.enabled = False
dcs_group.add_trigger_action(Hold())
dcs_group.add_trigger_action(fallback)
dcs_group.add_trigger_action(task)
# Create trigger
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
@@ -634,7 +632,7 @@ class GroundConflictGenerator:
@param enemy_groups Potential enemy groups
@param n number of nearby groups to take
"""
targets = [] # type: List[Optional[VehicleGroup]]
targets = [] # type: List[VehicleGroup]
sorted_list = sorted(
enemy_groups,
key=lambda group: player_group.points[0].position.distance_to_point(
@@ -658,7 +656,7 @@ class GroundConflictGenerator:
@param group Group for which we should find the nearest ennemy
@param enemy_groups Potential enemy groups
"""
min_distance = 99999999
min_distance = math.inf
target = None
for dcs_group, _ in enemy_groups:
dist = player_group.points[0].position.distance_to_point(
@@ -696,7 +694,7 @@ class GroundConflictGenerator:
"""
For artilery group, decide the distance from frontline with the range of the unit
"""
rg = getattr(group.unit_type.dcs_unit_type, "threat_range", 0) - 7500
rg = group.unit_type.dcs_unit_type.threat_range - 7500
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][1]:
rg = random.randint(
DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY][0],
@@ -716,7 +714,7 @@ class GroundConflictGenerator:
distance_from_frontline: int,
heading: int,
spawn_heading: int,
):
) -> Optional[Point]:
shifted = conflict_position.point_from_heading(
heading, random.randint(0, combat_width)
)
@@ -766,9 +764,9 @@ class GroundConflictGenerator:
heading=opposite_heading(spawn_heading),
)
if is_player:
g.set_skill(self.game.settings.player_skill)
g.set_skill(Skill(self.game.settings.player_skill))
else:
g.set_skill(self.game.settings.enemy_vehicle_skill)
g.set_skill(Skill(self.game.settings.enemy_vehicle_skill))
positioned_groups.append((g, group))
if group.role in [CombatGroupRole.APC, CombatGroupRole.IFV]:
@@ -790,7 +788,7 @@ class GroundConflictGenerator:
count: int,
at: Point,
move_formation: PointAction = PointAction.OffRoad,
heading=0,
heading: int = 0,
) -> VehicleGroup:
if side == self.conflict.attackers_country:

View File

@@ -1,12 +1,13 @@
"""Support for working with DCS group callsigns."""
import logging
import re
from typing import Any
from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit
def callsign_for_support_unit(group: FlyingGroup) -> str:
def callsign_for_support_unit(group: FlyingGroup[Any]) -> str:
# Either something like Overlord11 for Western AWACS, or else just a number.
# Convert to either "Overlord" or "Flight 123".
lead = group.units[0]

View File

@@ -1,6 +1,11 @@
import logging
import random
from game import db
from typing import Optional
from dcs.unitgroup import VehicleGroup
from game import db, Game
from game.theater.theatergroundobject import CoastalSiteGroundObject
from gen.coastal.silkworm import SilkwormGenerator
COASTAL_MAP = {
@@ -8,10 +13,13 @@ COASTAL_MAP = {
}
def generate_coastal_group(game, ground_object, faction_name: str):
def generate_coastal_group(
game: Game, ground_object: CoastalSiteGroundObject, faction_name: str
) -> Optional[VehicleGroup]:
"""
This generate a coastal defenses group
:return: Nothing, but put the group reference inside the ground object
:return: The generated group, or None if this faction does not support coastal
defenses.
"""
faction = db.FACTIONS[faction_name]
if len(faction.coastal_defenses) > 0:

View File

@@ -1,14 +1,19 @@
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import CoastalSiteGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class SilkwormGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
class SilkwormGenerator(VehicleGroupGenerator[CoastalSiteGroundObject]):
def __init__(
self, game: Game, ground_object: CoastalSiteGroundObject, faction: Faction
) -> None:
super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
def generate(self) -> None:
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
@@ -23,7 +28,7 @@ class SilkwormGenerator(GroupGenerator):
# Launchers
for i, p in enumerate(positions):
self.add_unit(
MissilesSS.Silkworm_SR,
MissilesSS.Hy_launcher,
"Missile#" + str(i),
p[0],
p[1],

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
from typing import Tuple, Optional
@@ -54,13 +56,15 @@ class Conflict:
def frontline_position(
cls, frontline: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int]:
attack_heading = frontline.attack_heading
attack_heading = int(frontline.attack_heading)
position = cls.find_ground_position(
frontline.position,
FRONTLINE_LENGTH,
heading_sum(attack_heading, 90),
theater,
)
if position is None:
raise RuntimeError("Could not find front line position")
return position, opposite_heading(attack_heading)
@classmethod
@@ -91,7 +95,7 @@ class Conflict:
defender: Country,
front_line: FrontLine,
theater: ConflictTheater,
):
) -> Conflict:
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp)
position, heading, distance = cls.frontline_vector(front_line, theater)
conflict = cls(
@@ -138,7 +142,7 @@ class Conflict:
max_distance: int,
heading: int,
theater: ConflictTheater,
coerce=True,
coerce: bool = True,
) -> Optional[Point]:
"""
Finds the nearest valid ground position along a provided heading and it's inverse up to max_distance.
@@ -153,6 +157,8 @@ class Conflict:
if theater.is_on_land(pos):
return pos
pos = initial.point_from_heading(opposite_heading(heading), distance)
if theater.is_on_land(pos):
return pos
if coerce:
pos = theater.nearest_land_pos(initial)
return pos

View File

@@ -1,4 +1,5 @@
import random
from typing import Optional
from dcs.unitgroup import VehicleGroup
@@ -12,7 +13,9 @@ from gen.defenses.armored_group_generator import (
)
def generate_armor_group(faction: str, game, ground_object):
def generate_armor_group(
faction: str, game: Game, ground_object: VehicleGroupGroundObject
) -> Optional[VehicleGroup]:
"""
This generate a group of ground units
:return: Generated group

View File

@@ -3,10 +3,10 @@ import random
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.sam.group_generator import GroupGenerator
from gen.sam.group_generator import VehicleGroupGenerator
class ArmoredGroupGenerator(GroupGenerator):
class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
def __init__(
self,
game: Game,
@@ -35,7 +35,7 @@ class ArmoredGroupGenerator(GroupGenerator):
)
class FixedSizeArmorGroupGenerator(GroupGenerator):
class FixedSizeArmorGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
def __init__(
self,
game: Game,
@@ -47,7 +47,7 @@ class FixedSizeArmorGroupGenerator(GroupGenerator):
self.unit_type = unit_type
self.size = size
def generate(self):
def generate(self) -> None:
spacing = random.randint(20, 70)
index = 0

View File

@@ -22,7 +22,7 @@ class EnvironmentGenerator:
def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None:
return
self.mission.weather.fog_visibility = fog.visibility.meters
self.mission.weather.fog_visibility = int(fog.visibility.meters)
self.mission.weather.fog_thickness = fog.thickness
def set_wind(self, wind: WindConditions) -> None:
@@ -30,7 +30,7 @@ class EnvironmentGenerator:
self.mission.weather.wind_at_2000 = wind.at_2000m
self.mission.weather.wind_at_8000 = wind.at_8000m
def generate(self):
def generate(self) -> None:
self.mission.start_time = self.conditions.start_time
self.set_clouds(self.conditions.weather.clouds)
self.set_fog(self.conditions.weather.fog)

View File

@@ -6,7 +6,7 @@ from dcs.ships import USS_Arleigh_Burke_IIa, TICONDEROG
class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Carrier Strike Group 8
if self.faction.carrier_names[0] == "Carrier Strike Group 8":

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
Type_052C,
Type_052B,
@@ -11,16 +10,16 @@ from dcs.ships import (
)
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False])
@@ -65,9 +64,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
class Type54GroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(Type54GroupGenerator, self).__init__(
game, ground_object, faction, Type_054A
)

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Type
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType
from dcs.ships import PERRY, USS_Arleigh_Burke_IIa
from dcs.unittype import ShipType
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.sam.group_generator import ShipGroupGenerator
if TYPE_CHECKING:
from game.game import Game
@@ -16,14 +17,14 @@ class DDGroupGenerator(ShipGroupGenerator):
def __init__(
self,
game: Game,
ground_object: TheaterGroundObject,
ground_object: ShipGroundObject,
faction: Faction,
ddtype: Type[ShipType],
):
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
self.ddtype = ddtype
def generate(self):
def generate(self) -> None:
self.add_unit(
self.ddtype,
"DD1",
@@ -42,18 +43,14 @@ class DDGroupGenerator(ShipGroupGenerator):
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, PERRY
)
class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, USS_Arleigh_Burke_IIa
)

View File

@@ -1,12 +1,13 @@
from dcs.ships import La_Combattante_II
from game import Game
from game.factions.faction import Faction
from game.theater import TheaterGroundObject
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
def __init__(self, game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(LaCombattanteIIGroupGenerator, self).__init__(
game, ground_object, faction, La_Combattante_II
)

View File

@@ -4,7 +4,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class LHAGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Add carrier
if len(self.faction.helicopter_carrier) > 0:

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
@@ -12,18 +13,17 @@ from dcs.ships import (
SOM,
)
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False])
@@ -85,32 +85,24 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
class GrishaGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, ALBATROS
)
class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, MOLNIYA
)
class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, KILO)
class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)

View File

@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class SchnellbootGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
for i in range(random.randint(2, 4)):
self.add_unit(

View File

@@ -1,7 +1,17 @@
from __future__ import annotations
import logging
import random
from typing import TYPE_CHECKING, Optional
from dcs.unitgroup import ShipGroup
from game import db
from game.theater.theatergroundobject import (
LhaGroundObject,
CarrierGroundObject,
ShipGroundObject,
)
from gen.fleet.carrier_group import CarrierGroupGenerator
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
from gen.fleet.dd_group import (
@@ -21,6 +31,9 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator
if TYPE_CHECKING:
from game import Game
SHIP_MAP = {
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
@@ -39,10 +52,12 @@ SHIP_MAP = {
}
def generate_ship_group(game, ground_object, faction_name: str):
def generate_ship_group(
game: Game, ground_object: ShipGroundObject, faction_name: str
) -> Optional[ShipGroup]:
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
:return: The generated group, or None if this faction does not support ships.
"""
faction = db.FACTIONS[faction_name]
if len(faction.navy_generators) > 0:
@@ -61,26 +76,30 @@ def generate_ship_group(game, ground_object, faction_name: str):
return None
def generate_carrier_group(faction: str, game, ground_object):
"""
This generate a carrier group
:param parentCp: The parent control point
def generate_carrier_group(
faction: str, game: Game, ground_object: CarrierGroundObject
) -> ShipGroup:
"""Generates a carrier group.
:param faction: The faction the TGO belongs to.
:param game: The Game the group is being generated for.
:param ground_object: The ground object which will own the ship group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
:return: The generated group.
"""
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()
return generator.get_generated_group()
def generate_lha_group(faction: str, game, ground_object):
"""
This generate a lha carrier group
:param parentCp: The parent control point
def generate_lha_group(
faction: str, game: Game, ground_object: LhaGroundObject
) -> ShipGroup:
"""Generate an LHA group.
:param faction: The faction the TGO belongs to.
:param game: The Game the group is being generated for.
:param ground_object: The ground object which will own the ship group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
:return: The generated group.
"""
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()

View File

@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
for i in range(random.randint(1, 4)):
self.add_unit(

View File

@@ -6,7 +6,7 @@ from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Add LS Samuel Chase
self.add_unit(

View File

@@ -18,6 +18,7 @@ from typing import (
TYPE_CHECKING,
Tuple,
TypeVar,
Any,
)
from game.dcs.aircrafttype import AircraftType
@@ -178,7 +179,7 @@ class AircraftAllocator:
aircraft, task
)
for squadron in squadrons:
if squadron.number_of_available_pilots >= flight.num_aircraft:
if squadron.can_provide_pilots(flight.num_aircraft):
inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, squadron
return None
@@ -284,7 +285,7 @@ class ObjectiveFinder:
self.game = game
self.is_player = is_player
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]:
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject[Any], Distance]]:
"""Iterates over all enemy SAM sites."""
doctrine = self.game.faction_for(self.is_player).doctrine
threat_zones = self.game.threat_zone_for(not self.is_player)
@@ -314,14 +315,14 @@ class ObjectiveFinder:
yield ground_object, target_range
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]:
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject[Any]]:
"""Iterates over enemy SAMs in threat range of friendly control points.
SAM sites are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
target_ranges: list[tuple[TheaterGroundObject[Any], Distance]] = []
for target, threat_range in self.enemy_air_defenses():
ranges: list[Distance] = []
for cp in self.friendly_control_points():
@@ -374,9 +375,9 @@ class ObjectiveFinder:
def _targets_by_range(
self, targets: Iterable[MissionTargetType]
) -> Iterator[MissionTargetType]:
target_ranges: List[Tuple[MissionTargetType, int]] = []
target_ranges: list[tuple[MissionTargetType, float]] = []
for target in targets:
ranges: List[int] = []
ranges: list[float] = []
for cp in self.friendly_control_points():
ranges.append(target.distance_to(cp))
target_ranges.append((target, min(ranges)))
@@ -385,13 +386,13 @@ class ObjectiveFinder:
for target, _range in target_ranges:
yield target
def strike_targets(self) -> Iterator[TheaterGroundObject]:
def strike_targets(self) -> Iterator[TheaterGroundObject[Any]]:
"""Iterates over enemy strike targets.
Targets are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
targets: List[Tuple[TheaterGroundObject, int]] = []
targets: list[tuple[TheaterGroundObject[Any], float]] = []
# Building objectives are made of several individual TGOs (one per
# building).
found_targets: Set[str] = set()
@@ -430,7 +431,7 @@ class ObjectiveFinder:
continue
if ground_object.name in found_targets:
continue
ranges: List[int] = []
ranges: list[float] = []
for friendly_cp in self.friendly_control_points():
ranges.append(ground_object.distance_to(friendly_cp))
targets.append((ground_object, min(ranges)))
@@ -604,27 +605,35 @@ class CoalitionMissionPlanner:
also possible for the player to exclude mission types from their squadron
designs.
"""
all_compatible = aircraft_for_task(mission_type)
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
if (
squadron.aircraft in all_compatible
and mission_type in squadron.auto_assignable_mission_types
):
return True
return False
return self.game.air_wing_for(self.is_player).can_auto_plan(mission_type)
@property
def oca_aircraft_plannable(self) -> bool:
return (
self.air_wing_can_plan(FlightType.OCA_AIRCRAFT)
and self.game.settings.default_start_type == "Cold"
def critical_missions(self) -> Iterator[ProposedMission]:
"""Identifies the most important missions to plan this turn.
Non-critical missions that cannot be fulfilled will create purchase
orders for the next turn. Critical missions will create a purchase order
unless the mission can be doubly fulfilled. In other words, the AI will
attempt to have *double* the aircraft it needs for these missions to
ensure that they can be planned again next turn even if all aircraft are
eliminated this turn.
"""
# Find farthest, friendly CP for AEWC.
yield ProposedMission(
self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP.
asap=True,
)
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
)
def propose_barcap(self) -> Iterator[ProposedMission]:
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points():
# Plan CAP in such a way, that it is established during the whole desired
# mission length.
# Plan CAP in such a way, that it is established during the whole desired mission length
for _ in range(
0,
int(self.game.settings.desired_player_mission_duration.total_seconds()),
@@ -637,31 +646,36 @@ class CoalitionMissionPlanner:
],
)
def propose_cas(self) -> Iterator[ProposedMission]:
# Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines():
flights = [ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE)]
if self.air_wing_can_plan(FlightType.TARCAP):
# This is *not* an escort because front lines don't create a threat
# zone. Generating threat zones from front lines causes the front
# line to push back BARCAPs as it gets closer to the base. While
# front lines do have the same problem of potentially pulling
# BARCAPs off bases to engage a front line TARCAP, that's probably
# the one time where we do want that.
#
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
# We don't have intercept missions yet so this isn't something we
# can do today, but we should probably return to having the front
# line project a threat zone (so that strike missions will route
# around it) and instead *not plan* a BARCAP at bases near the
# front, since there isn't a place to put a barrier. Instead, the
# aircraft that would have been a BARCAP could be used as additional
# interceptors and TARCAPs which will defend the base but won't be
# trying to avoid front line contacts.
flights.append(ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE))
yield ProposedMission(front_line, flights)
yield ProposedMission(
front_line,
[
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
# This is *not* an escort because front lines don't create a threat
# zone. Generating threat zones from front lines causes the front
# line to push back BARCAPs as it gets closer to the base. While
# front lines do have the same problem of potentially pulling
# BARCAPs off bases to engage a front line TARCAP, that's probably
# the one time where we do want that.
#
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
# We don't have intercept missions yet so this isn't something we
# can do today, but we should probably return to having the front
# line project a threat zone (so that strike missions will route
# around it) and instead *not plan* a BARCAP at bases near the
# front, since there isn't a place to put a barrier. Instead, the
# aircraft that would have been a BARCAP could be used as additional
# interceptors and TARCAPs which will defend the base but won't be
# trying to avoid front line contacts.
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
],
)
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
yield from self.critical_missions()
def propose_dead(self) -> Iterator[ProposedMission]:
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
# or objects, plan DEAD.
# Find enemy SAM sites with ranges that extend to within 50 nmi of
@@ -686,10 +700,7 @@ class CoalitionMissionPlanner:
else:
flights.append(
ProposedFlight(
FlightType.SEAD_ESCORT,
2,
self.MAX_SEAD_RANGE,
EscortType.Sead,
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead
)
)
# TODO: Max escort range.
@@ -700,7 +711,6 @@ class CoalitionMissionPlanner:
)
yield ProposedMission(sam, flights)
def propose_convoy_interdiction(self) -> Iterator[ProposedMission]:
# These will only rarely get planned. When a convoy is travelling multiple legs,
# they're targetable after the first leg. The reason for this is that
# procurement happens *after* mission planning so that the missions that could
@@ -729,7 +739,6 @@ class CoalitionMissionPlanner:
],
)
def propose_shipping_interdiction(self) -> Iterator[ProposedMission]:
for ship in self.objective_finder.cargo_ships():
yield ProposedMission(
ship,
@@ -745,7 +754,6 @@ class CoalitionMissionPlanner:
],
)
def propose_naval_strikes(self) -> Iterator[ProposedMission]:
for group in self.objective_finder.threatening_ships():
yield ProposedMission(
group,
@@ -761,7 +769,6 @@ class CoalitionMissionPlanner:
],
)
def propose_bai(self) -> Iterator[ProposedMission]:
for group in self.objective_finder.threatening_vehicle_groups():
yield ProposedMission(
group,
@@ -777,25 +784,16 @@ class CoalitionMissionPlanner:
],
)
def propose_oca_strikes(self) -> Iterator[ProposedMission]:
for target in self.objective_finder.oca_targets(min_aircraft=20):
flights = []
if self.air_wing_can_plan(FlightType.OCA_RUNWAY):
flights.append(
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE)
)
if self.oca_aircraft_plannable:
flights = [
ProposedFlight(FlightType.OCA_RUNWAY, 2, self.MAX_OCA_RANGE),
]
if self.game.settings.default_start_type == "Cold":
# Only schedule if the default start type is Cold. If the player
# has set anything else there are no targets to hit.
flights.append(
ProposedFlight(FlightType.OCA_AIRCRAFT, 2, self.MAX_OCA_RANGE)
)
if not flights:
raise RuntimeError(
"Attempted planning of OCA strikes but neither OCA/Runway nor "
f"OCA/Aircraft are plannable for {self.faction.name} with the "
"current game settings."
)
flights.extend(
[
# TODO: Max escort range.
@@ -809,7 +807,7 @@ class CoalitionMissionPlanner:
)
yield ProposedMission(target, flights)
def propose_building_strikes(self) -> Iterator[ProposedMission]:
# Plan strike missions.
for target in self.objective_finder.strike_targets():
yield ProposedMission(
target,
@@ -828,48 +826,6 @@ class CoalitionMissionPlanner:
],
)
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
# Find farthest, friendly CP for AEWC.
if self.air_wing_can_plan(FlightType.AEWC):
yield ProposedMission(
self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP.
asap=True,
)
if self.air_wing_can_plan(FlightType.REFUELING):
yield ProposedMission(
self.objective_finder.closest_friendly_control_point(),
[ProposedFlight(FlightType.REFUELING, 1, self.MAX_TANKER_RANGE)],
)
if self.air_wing_can_plan(FlightType.BARCAP):
yield from self.propose_barcap()
if self.air_wing_can_plan(FlightType.CAS):
yield from self.propose_cas()
if self.air_wing_can_plan(FlightType.DEAD):
yield from self.propose_dead()
if self.air_wing_can_plan(FlightType.BAI):
yield from self.propose_convoy_interdiction()
if self.air_wing_can_plan(FlightType.ANTISHIP):
yield from self.propose_shipping_interdiction()
yield from self.propose_naval_strikes()
if self.air_wing_can_plan(FlightType.BAI):
yield from self.propose_bai()
if self.air_wing_can_plan(FlightType.OCA_RUNWAY) or self.oca_aircraft_plannable:
yield from self.propose_oca_strikes()
if self.air_wing_can_plan(FlightType.STRIKE):
yield from self.propose_building_strikes()
def plan_missions(self) -> None:
"""Identifies and plans mission for the turn."""
player = "Blue" if self.is_player else "Red"
@@ -878,6 +834,11 @@ class CoalitionMissionPlanner:
for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission, tracer)
with logged_duration(f"{player} reserve mission planning"):
with MultiEventTracer() as tracer:
for critical_mission in self.critical_missions():
self.plan_mission(critical_mission, tracer, reserves=True)
with logged_duration(f"{player} mission scheduling"):
self.stagger_missions()
@@ -1097,7 +1058,7 @@ class CoalitionMissionPlanner:
# delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) + tot
def message(self, title, text) -> None:
def message(self, title: str, text: str) -> None:
"""Emits a planning message to the player.
If the mission planner belongs to the players coalition, this emits a

View File

@@ -1,5 +1,6 @@
import logging
from typing import List, Type
from collections import Sequence
from typing import Type
from dcs.helicopters import (
AH_1W,
@@ -111,7 +112,6 @@ from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.su57.su57 import Su_57
# All aircraft lists are in priority order. Aircraft higher in the list will be
@@ -125,29 +125,30 @@ from pydcs_extensions.su57.su57 import Su_57
CAP_CAPABLE = [
Su_57,
F_22A,
MiG_31,
F_15C,
F_14B,
F_14A_135_GR,
MiG_25PD,
Su_33,
Su_34,
J_11A,
Su_30,
Su_27,
J_11A,
F_15C,
MiG_29S,
MiG_29G,
MiG_29A,
F_16C_50,
FA_18C_hornet,
JF_17,
JAS39Gripen,
F_16A,
F_4E,
JAS39Gripen,
JF_17,
MiG_31,
MiG_25PD,
MiG_29G,
MiG_29A,
MiG_23MLD,
MiG_21Bis,
Mirage_2000_5,
M_2000C,
F_15E,
M_2000C,
F_5E_3,
MiG_19P,
A_4E_C,
@@ -174,6 +175,7 @@ CAS_CAPABLE = [
A_10C_2,
A_10C,
Hercules,
Su_34,
Su_25TM,
Su_25T,
Su_25,
@@ -191,17 +193,16 @@ CAS_CAPABLE = [
F_14B,
F_14A_135_GR,
AJS37,
Su_24MR,
Su_24M,
Su_17M4,
Su_33,
F_4E,
S_3B,
Su_34,
Su_30,
MiG_19P,
MiG_29S,
MiG_27K,
MiG_29A,
MiG_21Bis,
AH_64D,
AH_64A,
AH_1W,
@@ -213,14 +214,14 @@ CAS_CAPABLE = [
Mi_24P,
Mi_24V,
Mi_8MT,
UH_1H,
MiG_19P,
MiG_15bis,
M_2000C,
F_5E_3,
F_86F_Sabre,
C_101CC,
MB_339PAN,
L_39ZA,
UH_1H,
A_20G,
Ju_88A4,
P_47D_40,
@@ -301,13 +302,14 @@ STRIKE_CAPABLE = [
Tornado_GR4,
F_16C_50,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24MR,
Su_24M,
Su_25TM,
Su_25T,
@@ -319,11 +321,9 @@ STRIKE_CAPABLE = [
MiG_29S,
MiG_29G,
MiG_29A,
JF_17,
F_4E,
A_10C_2,
A_10C,
AV8BNA,
S_3B,
A_4E_C,
M_2000C,
@@ -332,7 +332,6 @@ STRIKE_CAPABLE = [
MiG_15bis,
F_5E_3,
F_86F_Sabre,
MB_339PAN,
C_101CC,
L_39ZA,
B_17G,
@@ -378,6 +377,7 @@ RUNWAY_ATTACK_CAPABLE = [
Su_34,
Su_30,
Tornado_IDS,
M_2000C,
] + STRIKE_CAPABLE
# For any aircraft that isn't necessarily directly involved in strike
@@ -418,7 +418,7 @@ REFUELING_CAPABALE = [
]
def dcs_types_for_task(task: FlightType) -> list[Type[FlyingType]]:
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP)
if task in cap_missions:
return CAP_CAPABLE

View File

@@ -2,13 +2,12 @@ from __future__ import annotations
from datetime import timedelta
from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Union
from typing import List, Optional, TYPE_CHECKING, Union, Sequence
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit
from game import db
from game.dcs.aircrafttype import AircraftType
from game.squadrons import Pilot, Squadron
from game.theater.controlpoint import ControlPoint, MissionTarget
@@ -141,8 +140,8 @@ class FlightWaypoint:
waypoint_type: The waypoint type.
x: X cooidinate of the waypoint.
y: Y coordinate of the waypoint.
alt: Altitude of the waypoint. By default this is AGL, but it can be
changed to MSL by setting alt_type to "RADIO".
alt: Altitude of the waypoint. By default this is MSL, but it can be
changed to AGL by setting alt_type to "RADIO"
"""
self.waypoint_type = waypoint_type
self.x = x
@@ -154,7 +153,7 @@ class FlightWaypoint:
# Only used in the waypoint list in the flight edit page. No sense
# having three names. A short and long form is enough.
self.description = ""
self.targets: List[Union[MissionTarget, Unit]] = []
self.targets: Sequence[Union[MissionTarget, Unit]] = []
self.obj_name = ""
self.pretty_name = ""
self.only_for_player = False
@@ -323,12 +322,12 @@ class Flight:
def clear_roster(self) -> None:
self.roster.clear()
def __repr__(self):
def __repr__(self) -> str:
if self.custom_name:
return f"{self.custom_name} {self.count} x {self.unit_type}"
return f"[{self.flight_type}] {self.count} x {self.unit_type}"
def __str__(self):
def __str__(self) -> str:
if self.custom_name:
return f"{self.custom_name} {self.count} x {self.unit_type}"
return f"[{self.flight_type}] {self.count} x {self.unit_type}"

View File

@@ -16,17 +16,6 @@ from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point
from dcs.planes import (
E_3A,
E_2C,
A_50,
IL_78M,
KC130,
KC135MPRS,
KC_135,
KJ_2000,
S_3B_Tanker,
)
from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint
@@ -38,8 +27,9 @@ from game.theater import (
MissionTarget,
SamGroundObject,
TheaterGroundObject,
NavalControlPoint,
)
from game.theater.theatergroundobject import EwrGroundObject
from game.theater.theatergroundobject import EwrGroundObject, NavalGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles, knots
from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
@@ -229,11 +219,7 @@ class FlightPlan:
tot_waypoint = self.tot_waypoint
if tot_waypoint is None:
return None
time = self.tot
if time is None:
return None
return time - self._travel_time_to_waypoint(tot_waypoint)
return self.tot - self._travel_time_to_waypoint(tot_waypoint)
def startup_time(self) -> Optional[timedelta]:
takeoff_time = self.takeoff_time()
@@ -404,6 +390,9 @@ class PatrollingFlightPlan(FlightPlan):
#: Maximum time to remain on station.
patrol_duration: timedelta
#: Racetrack speed TAS.
patrol_speed: Speed
#: The engagement range of any Search Then Engage task, or the radius of a
#: Search Then Engage in Zone task. Any enemies of the appropriate type for
#: this mission within this range of the flight's current position (or the
@@ -785,9 +774,6 @@ class RefuelingFlightPlan(PatrollingFlightPlan):
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
#: Racetrack speed.
patrol_speed: Speed
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
@@ -1092,35 +1078,28 @@ class FlightPlanBuilder:
orbit_location = self.aewc_orbit(location)
# As high as possible to maximize detection and on-station time.
if flight.unit_type == E_2C:
patrol_alt = feet(30000)
elif flight.unit_type == E_3A:
patrol_alt = feet(35000)
elif flight.unit_type == A_50:
patrol_alt = feet(33000)
elif flight.unit_type == KJ_2000:
patrol_alt = feet(40000)
if flight.unit_type.patrol_altitude is not None:
patrol_alt = flight.unit_type.patrol_altitude
else:
patrol_alt = feet(25000)
builder = WaypointBuilder(flight, self.game, self.is_player)
orbit_location = builder.orbit(orbit_location, patrol_alt)
orbit = builder.orbit(orbit_location, patrol_alt)
return AwacsFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, orbit_location.position, patrol_alt
flight.departure.position, orbit.position, patrol_alt
),
nav_from=builder.nav_path(
orbit_location.position, flight.arrival.position, patrol_alt
orbit.position, flight.arrival.position, patrol_alt
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
hold=orbit_location,
hold=orbit,
hold_duration=timedelta(hours=4),
)
@@ -1151,7 +1130,7 @@ class FlightPlanBuilder:
)
@staticmethod
def anti_ship_targets_for_tgo(tgo: TheaterGroundObject) -> List[StrikeTarget]:
def anti_ship_targets_for_tgo(tgo: NavalGroundObject) -> List[StrikeTarget]:
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
@@ -1164,12 +1143,9 @@ class FlightPlanBuilder:
from game.transfers import CargoShip
if isinstance(location, ControlPoint):
if not location.is_fleet:
raise InvalidObjectiveLocation(flight.flight_type, location)
# The first group generated will be the carrier group itself.
targets = self.anti_ship_targets_for_tgo(location.ground_objects[0])
elif isinstance(location, TheaterGroundObject):
if isinstance(location, NavalControlPoint):
targets = self.anti_ship_targets_for_tgo(location.find_main_tgo())
elif isinstance(location, NavalGroundObject):
targets = self.anti_ship_targets_for_tgo(location)
elif isinstance(location, CargoShip):
targets = [StrikeTarget(location.name, location)]
@@ -1191,21 +1167,28 @@ class FlightPlanBuilder:
if isinstance(location, FrontLine):
raise InvalidObjectiveLocation(flight.flight_type, location)
start, end = self.racetrack_for_objective(location, barcap=True)
patrol_alt = meters(
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
start_pos, end_pos = self.racetrack_for_objective(location, barcap=True)
preferred_alt = flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
logging.debug(
f"BARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.race_track(start, end, patrol_alt)
start, end = builder.race_track(start_pos, end_pos, patrol_alt)
return BarCapFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
@@ -1231,10 +1214,12 @@ class FlightPlanBuilder:
target = self.package.target.position
heading = self.package.waypoints.join.heading_between_point(target)
start = target.point_from_heading(heading, -self.doctrine.sweep_distance.meters)
start_pos = target.point_from_heading(
heading, -self.doctrine.sweep_distance.meters
)
builder = WaypointBuilder(flight, self.game, self.is_player)
start, end = builder.sweep(start, target, self.doctrine.ingress_altitude)
start, end = builder.sweep(start_pos, target, self.doctrine.ingress_altitude)
hold = builder.hold(self._hold_point(flight))
@@ -1428,11 +1413,15 @@ class FlightPlanBuilder:
"""
location = self.package.target
patrol_alt = meters(
random.randint(
int(self.doctrine.min_patrol_altitude.meters),
int(self.doctrine.max_patrol_altitude.meters),
)
preferred_alt = flight.unit_type.preferred_patrol_altitude
randomized_alt = preferred_alt + feet(random.randint(-2, 1) * 1000)
patrol_alt = max(
self.doctrine.min_patrol_altitude,
min(self.doctrine.max_patrol_altitude, randomized_alt),
)
patrol_speed = flight.unit_type.preferred_patrol_speed(patrol_alt)
logging.debug(
f"TARCAP patrol speed for {flight.unit_type.name} at {patrol_alt.feet}ft: {patrol_speed.knots} KTAS"
)
# Create points
@@ -1455,6 +1444,7 @@ class FlightPlanBuilder:
# requests an escort the CAP flight will remain on station for the
# duration of the escorted mission, or until it is winchester/bingo.
patrol_duration=self.doctrine.cap_duration,
patrol_speed=patrol_speed,
engagement_distance=self.doctrine.cap_engagement_range,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(flight.departure.position, orbit0p, patrol_alt),
@@ -1623,16 +1613,33 @@ class FlightPlanBuilder:
builder = WaypointBuilder(flight, self.game, self.is_player)
# 2021-08-02: patrol_speed will currently have no effect because
# CAS doesn't use OrbitAction. But all PatrollingFlightPlan are expected
# to have patrol_speed
is_helo = flight.unit_type.dcs_unit_type.helicopter
ingress_egress_altitude = (
self.doctrine.ingress_altitude if not is_helo else meters(50)
)
patrol_speed = flight.unit_type.preferred_patrol_speed(ingress_egress_altitude)
use_agl_ingress_egress = is_helo
return CasFlightPlan(
package=self.package,
flight=flight,
patrol_duration=self.doctrine.cas_duration,
patrol_speed=patrol_speed,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, ingress, self.doctrine.ingress_altitude
flight.departure.position,
ingress,
ingress_egress_altitude,
use_agl_ingress_egress,
),
nav_from=builder.nav_path(
egress, flight.arrival.position, self.doctrine.ingress_altitude
egress,
flight.arrival.position,
ingress_egress_altitude,
use_agl_ingress_egress,
),
patrol_start=builder.ingress(
FlightWaypointType.INGRESS_CAS, ingress, location
@@ -1680,31 +1687,18 @@ class FlightPlanBuilder:
builder = WaypointBuilder(flight, self.game, self.is_player)
tanker_type = flight.unit_type
if tanker_type is KC_135:
# ~300 knots IAS.
speed = knots(445)
altitude = feet(24000)
elif tanker_type is KC135MPRS:
# ~300 knots IAS.
speed = knots(440)
altitude = feet(23000)
elif tanker_type is KC130:
# ~210 knots IAS, roughly the max for the KC-130 at altitude.
speed = knots(370)
altitude = feet(22000)
elif tanker_type is S_3B_Tanker:
# ~265 knots IAS.
speed = knots(320)
altitude = feet(12000)
elif tanker_type is IL_78M:
# ~280 knots IAS.
speed = knots(400)
altitude = feet(21000)
if tanker_type.patrol_altitude is not None:
altitude = tanker_type.patrol_altitude
else:
# ~280 knots IAS.
speed = knots(400)
altitude = feet(21000)
# TODO: Could use flight.unit_type.preferred_patrol_speed(altitude) instead.
if tanker_type.patrol_speed is not None:
speed = tanker_type.patrol_speed
else:
# ~280 knots IAS at 21000.
speed = knots(400)
racetrack = builder.race_track(racetrack_start, racetrack_end, altitude)
return RefuelingFlightPlan(
@@ -1903,23 +1897,23 @@ class FlightPlanBuilder:
return self._retreating_rendezvous_point(attack_transition)
return self._advancing_rendezvous_point(attack_transition)
def _ingress_point(self, heading: int) -> Point:
def _ingress_point(self, heading: float) -> Point:
return self.package.target.position.point_from_heading(
heading - 180 + 15, self.doctrine.ingress_egress_distance.meters
)
def _egress_point(self, heading: int) -> Point:
def _egress_point(self, heading: float) -> Point:
return self.package.target.position.point_from_heading(
heading - 180 - 15, self.doctrine.ingress_egress_distance.meters
)
def _target_heading_to_package_airfield(self) -> int:
def _target_heading_to_package_airfield(self) -> float:
return self._heading_to_package_airfield(self.package.target.position)
def _heading_to_package_airfield(self, point: Point) -> int:
def _heading_to_package_airfield(self, point: Point) -> float:
return self.package_airfield().position.heading_between_point(point)
def _distance_to_package_airfield(self, point: Point) -> int:
def _distance_to_package_airfield(self, point: Point) -> float:
return self.package_airfield().position.distance_to_point(point)
def package_airfield(self) -> ControlPoint:

View File

@@ -19,7 +19,11 @@ class Loadout:
is_custom: bool = False,
) -> None:
self.name = name
self.pylons = {k: v for k, v in pylons.items() if v is not None}
# We clear unused pylon entries on initialization, but UI actions can still
# cause a pylon to be emptied, so make the optional type explicit.
self.pylons: Mapping[int, Optional[Weapon]] = {
k: v for k, v in pylons.items() if v is not None
}
self.date = date
self.is_custom = is_custom
@@ -92,6 +96,7 @@ class Loadout:
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
FlightType.STRIKE: ("STRIKE",),
FlightType.ANTISHIP: ("ANTISHIP",),
FlightType.DEAD: ("DEAD",),
FlightType.SEAD: ("SEAD",),
FlightType.BAI: ("BAI",),
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
@@ -133,4 +138,8 @@ class Loadout:
)
# TODO: Try group.load_task_default_loadout(loadout_for_task)
return cls.empty_loadout()
@classmethod
def empty_loadout(cls) -> Loadout:
return Loadout("Empty", {}, date=None)

View File

@@ -1,4 +1,5 @@
from __future__ import annotations
import logging
import random
from dataclasses import dataclass
@@ -10,11 +11,12 @@ from typing import (
TYPE_CHECKING,
Tuple,
Union,
Any,
)
from dcs.mapping import Point
from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup
from dcs.unitgroup import Group, VehicleGroup, ShipGroup
if TYPE_CHECKING:
from game import Game
@@ -33,7 +35,9 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True)
class StrikeTarget:
name: str
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, MultiGroupTransport]
target: Union[
VehicleGroup, TheaterGroundObject[Any], Unit, ShipGroup, MultiGroupTransport
]
class WaypointBuilder:
@@ -54,7 +58,7 @@ class WaypointBuilder:
@property
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
return self.flight.unit_type.dcs_unit_type.helicopter
def takeoff(self, departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
@@ -441,7 +445,7 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
ingress = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
ingress_wp = self.ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
@@ -455,8 +459,8 @@ class WaypointBuilder:
waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area"
egress = self.egress(egress, target)
return ingress, waypoint, egress
egress_wp = self.egress(egress, target)
return ingress_wp, waypoint, egress_wp
@staticmethod
def pickup(control_point: ControlPoint) -> FlightWaypoint:

View File

@@ -43,7 +43,7 @@ class ForcedOptionsGenerator:
if blue.unrestricted_satnav or red.unrestricted_satnav:
self.mission.forced_options.unrestricted_satnav = True
def generate(self):
def generate(self) -> None:
self._set_options_view()
self._set_external_views()
self._set_labels()

View File

@@ -1,13 +1,18 @@
from __future__ import annotations
import logging
import random
from enum import Enum
from typing import Dict, List
from typing import Dict, List, TYPE_CHECKING
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater import ControlPoint
from gen.ground_forces.combat_stance import CombatStance
if TYPE_CHECKING:
from game import Game
MAX_COMBAT_GROUP_PER_CP = 10
@@ -52,10 +57,9 @@ class CombatGroup:
self.unit_type = unit_type
self.size = size
self.role = role
self.assigned_enemy_cp = None
self.start_position = None
def __str__(self):
def __str__(self) -> str:
s = f"ROLE : {self.role}\n"
if self.size:
s += f"UNITS {self.unit_type} * {self.size}"
@@ -63,7 +67,7 @@ class CombatGroup:
class GroundPlanner:
def __init__(self, cp: ControlPoint, game):
def __init__(self, cp: ControlPoint, game: Game) -> None:
self.cp = cp
self.game = game
self.connected_enemy_cp = [
@@ -83,17 +87,15 @@ class GroundPlanner:
self.units_per_cp[cp.id] = []
self.reserve: List[CombatGroup] = []
def plan_groundwar(self):
def plan_groundwar(self) -> None:
ground_unit_limit = self.cp.frontline_unit_count_limit
remaining_available_frontline_units = ground_unit_limit
if hasattr(self.cp, "stance"):
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
else:
self.cp.stance = CombatStance.DEFENSIVE
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# TODO: Fix to handle the per-front stances.
# https://github.com/dcs-liberation/dcs_liberation/issues/1417
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor:
@@ -152,20 +154,9 @@ class GroundPlanner:
if len(self.connected_enemy_cp) > 0:
enemy_cp = random.choice(self.connected_enemy_cp).id
self.units_per_cp[enemy_cp].append(group)
group.assigned_enemy_cp = enemy_cp
else:
self.reserve.append(group)
group.assigned_enemy_cp = "__reserve__"
collection.append(group)
if remaining_available_frontline_units == 0:
break
print("------------------")
print("Ground Planner : ")
print(self.cp.name)
print("------------------")
for unit_type in self.units_per_cp.keys():
print("For : #" + str(unit_type))
for group in self.units_per_cp[unit_type]:
print(str(group))

View File

@@ -9,12 +9,23 @@ from __future__ import annotations
import logging
import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
from typing import (
Dict,
Iterator,
Optional,
TYPE_CHECKING,
Type,
List,
TypeVar,
Any,
Generic,
Union,
)
from dcs import Mission, Point, unitgroup
from dcs.action import SceneryDestructionZone
from dcs.country import Country
from dcs.point import StaticPoint
from dcs.point import StaticPoint, MovingPoint
from dcs.statics import Fortification, fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
@@ -26,12 +37,12 @@ from dcs.task import (
from dcs.triggers import TriggerStart, TriggerZone
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType
from dcs.unittype import StaticType, UnitType, ShipType, VehicleType
from dcs.vehicles import vehicle_map
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
from game.db import unit_type_from_name, ship_type_from_name, vehicle_type_from_name
from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import (
BuildingGroundObject,
@@ -56,7 +67,10 @@ FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
class GenericGroundObjectGenerator:
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
class GenericGroundObjectGenerator(Generic[TgoT]):
"""An unspecialized ground object generator.
Currently used only for SAM
@@ -64,7 +78,7 @@ class GenericGroundObjectGenerator:
def __init__(
self,
ground_object: TheaterGroundObject,
ground_object: TgoT,
country: Country,
game: Game,
mission: Mission,
@@ -89,10 +103,7 @@ class GenericGroundObjectGenerator:
logging.warning(f"Found empty group in {self.ground_object}")
continue
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
unit_type = vehicle_type_from_name(group.units[0].type)
vg = self.m.vehicle_group(
self.country,
group.name,
@@ -116,24 +127,27 @@ class GenericGroundObjectGenerator:
self._register_unit_group(group, vg)
@staticmethod
def enable_eplrs(group: Group, unit_type: Type[UnitType]) -> None:
if hasattr(unit_type, "eplrs"):
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def enable_eplrs(group: VehicleGroup, unit_type: Type[VehicleType]) -> None:
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: Group) -> None:
def set_alarm_state(self, group: Union[ShipGroup, VehicleGroup]) -> None:
if self.game.settings.perf_red_alert_state:
group.points[0].tasks.append(OptAlarmState(2))
else:
group.points[0].tasks.append(OptAlarmState(1))
def _register_unit_group(self, persistence_group: Group, miz_group: Group) -> None:
def _register_unit_group(
self,
persistence_group: Union[ShipGroup, VehicleGroup],
miz_group: Union[ShipGroup, VehicleGroup],
) -> None:
self.unit_map.add_ground_object_units(
self.ground_object, persistence_group, miz_group
)
class MissileSiteGenerator(GenericGroundObjectGenerator):
class MissileSiteGenerator(GenericGroundObjectGenerator[MissileSiteGroundObject]):
@property
def culled(self) -> bool:
# Don't cull missile sites - their range is long enough to make them easily
@@ -148,7 +162,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
for group in self.ground_object.groups:
vg = self.m.find_group(group.name)
if vg is not None:
targets = self.possible_missile_targets(vg)
targets = self.possible_missile_targets()
if targets:
target = random.choice(targets)
real_target = target.point_from_heading(
@@ -165,7 +179,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
"Couldn't setup missile site to fire, group was not generated."
)
def possible_missile_targets(self, vg: Group) -> List[Point]:
def possible_missile_targets(self) -> List[Point]:
"""
Find enemy control points in range
:param vg: Vehicle group we are searching a target for (There is always only oe group right now)
@@ -174,7 +188,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
targets: List[Point] = []
for cp in self.game.theater.controlpoints:
if cp.captured != self.ground_object.control_point.captured:
distance = cp.position.distance_to_point(vg.position)
distance = cp.position.distance_to_point(self.ground_object.position)
if distance < self.missile_site_range:
targets.append(cp.position)
return targets
@@ -196,7 +210,7 @@ class MissileSiteGenerator(GenericGroundObjectGenerator):
return site_range
class BuildingSiteGenerator(GenericGroundObjectGenerator):
class BuildingSiteGenerator(GenericGroundObjectGenerator[BuildingGroundObject]):
"""Generator for building sites.
Building sites are the primary type of non-airbase objective locations that
@@ -225,7 +239,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
f"{self.ground_object.dcs_identifier} not found in static maps"
)
def generate_vehicle_group(self, unit_type: Type[UnitType]) -> None:
def generate_vehicle_group(self, unit_type: Type[VehicleType]) -> None:
if not self.ground_object.is_dead:
group = self.m.vehicle_group(
country=self.country,
@@ -324,7 +338,7 @@ class SceneryGenerator(BuildingSiteGenerator):
self.unit_map.add_scenery(scenery)
class GenericCarrierGenerator(GenericGroundObjectGenerator):
class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundObject]):
"""Base type for carrier group generation.
Used by both CV(N) groups and LHA groups.
@@ -376,13 +390,12 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
self._register_unit_group(group, ship_group)
def get_carrier_type(self, group: Group) -> Type[UnitType]:
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized carrier name: {group.units[0].type}")
return unit_type
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
return ship_type_from_name(group.units[0].type)
def configure_carrier(self, group: Group, atc_channel: RadioFrequency) -> ShipGroup:
def configure_carrier(
self, group: ShipGroup, atc_channel: RadioFrequency
) -> ShipGroup:
unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(
@@ -474,7 +487,7 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups."""
def get_carrier_type(self, group: Group) -> UnitType:
def get_carrier_type(self, group: ShipGroup) -> Type[ShipType]:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type, self.control_point.name)
@@ -518,7 +531,7 @@ class LhaGenerator(GenericCarrierGenerator):
)
class ShipObjectGenerator(GenericGroundObjectGenerator):
class ShipObjectGenerator(GenericGroundObjectGenerator[ShipGroundObject]):
"""Generator for non-carrier naval groups."""
def generate(self) -> None:
@@ -529,14 +542,11 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
self.generate_group(group, ship_type_from_name(group.units[0].type))
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, first_unit_type: Type[UnitType]) -> None:
def generate_group(
self, group_def: ShipGroup, first_unit_type: Type[ShipType]
) -> None:
group = self.m.ship_group(
self.country,
group_def.name,
@@ -624,7 +634,7 @@ class GroundObjectsGenerator:
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
def generate(self):
def generate(self) -> None:
for cp in self.game.theater.controlpoints:
if cp.captured:
country_name = self.game.player_country
@@ -637,6 +647,7 @@ class GroundObjectsGenerator:
).generate()
for ground_object in cp.ground_objects:
generator: GenericGroundObjectGenerator[Any]
if isinstance(ground_object, FactoryGroundObject):
generator = FactoryGenerator(
ground_object, country, self.game, self.m, self.unit_map

View File

@@ -37,6 +37,7 @@ from tabulate import tabulate
from game.data.alic import AlicCodes
from game.db import unit_type_from_name
from game.dcs.aircrafttype import AircraftType
from game.savecompat import has_save_compat_for
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye
from game.utils import meters
@@ -63,7 +64,8 @@ class KneeboardPageWriter:
else:
self.foreground_fill = (15, 15, 15)
self.background_fill = (255, 252, 252)
self.image = Image.new("RGB", (768, 1024), self.background_fill)
self.image_size = (768, 1024)
self.image = Image.new("RGB", self.image_size, self.background_fill)
# These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should
@@ -82,6 +84,7 @@ class KneeboardPageWriter:
"resources/fonts/Inconsolata.otf", 20, layout_engine=ImageFont.LAYOUT_BASIC
)
self.draw = ImageDraw.Draw(self.image)
self.page_margin = page_margin
self.x = page_margin
self.y = page_margin
self.line_spacing = line_spacing
@@ -91,10 +94,24 @@ class KneeboardPageWriter:
return self.x, self.y
def text(
self, text: str, font=None, fill: Tuple[int, int, int] = (0, 0, 0)
self,
text: str,
font: Optional[ImageFont.FreeTypeFont] = None,
fill: Optional[Tuple[int, int, int]] = None,
wrap: bool = False,
) -> None:
if font is None:
font = self.content_font
if fill is None:
fill = self.foreground_fill
if wrap:
text = "\n".join(
self.wrap_line_with_font(
line, self.image_size[0] - self.page_margin - self.x, font
)
for line in text.splitlines()
)
self.draw.text(self.position, text, font=font, fill=fill)
width, height = self.draw.textsize(text, font=font)
@@ -134,6 +151,24 @@ class KneeboardPageWriter:
output = combo
return "".join(segments + [output]).strip()
@staticmethod
def wrap_line_with_font(
inputstr: str, max_width: int, font: ImageFont.FreeTypeFont
) -> str:
if font.getsize(inputstr)[0] <= max_width:
return inputstr
tokens = inputstr.split(" ")
output = ""
segments = []
for token in tokens:
combo = output + " " + token
if font.getsize(combo)[0] > max_width:
segments.append(output + "\n")
output = token
else:
output = combo
return "".join(segments + [output]).strip()
class KneeboardPage:
"""Base class for all kneeboard pages."""
@@ -554,6 +589,24 @@ class StrikeTaskPage(KneeboardPage):
]
class NotesPage(KneeboardPage):
"""A kneeboard page containing the campaign owner's notes."""
def __init__(
self,
notes: str,
dark_kneeboard: bool,
) -> None:
self.notes = notes
self.dark_kneeboard = dark_kneeboard
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
writer.title(f"Notes")
writer.text(self.notes, wrap=True)
writer.write(path)
class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission."""
@@ -602,6 +655,7 @@ class KneeboardGenerator(MissionInfoGenerator):
return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater)
return None
@has_save_compat_for(4)
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
"""Returns a list of kneeboard pages for the given flight."""
pages: List[KneeboardPage] = [
@@ -623,6 +677,10 @@ class KneeboardGenerator(MissionInfoGenerator):
),
]
# Only create the notes page if there are notes to show.
if notes := getattr(self.game, "notes", ""):
pages.append(NotesPage(notes, self.dark_kneeboard))
if (target_page := self.generate_task_page(flight)) is not None:
pages.append(target_page)

View File

@@ -1,22 +0,0 @@
from dataclasses import dataclass, field
from typing import List
from gen.locations.preset_locations import PresetLocation
@dataclass
class PresetControlPointLocations:
"""A repository of preset locations for a given control point"""
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious)
ashore_locations: List[PresetLocation] = field(default_factory=list)
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
offshore_locations: List[PresetLocation] = field(default_factory=list)
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
antiship_locations: List[PresetLocation] = field(default_factory=list)
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
powerplant_locations: List[PresetLocation] = field(default_factory=list)

View File

@@ -1,21 +0,0 @@
from dataclasses import dataclass
from dcs import Point
@dataclass
class PresetLocation:
"""A preset location"""
position: Point
heading: int
id: str
def __str__(self):
return (
"-" * 10
+ "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(
self.position.x, self.position.y, self.heading, self.id
)
+ "-" * 10
)

View File

@@ -1,13 +1,20 @@
import logging
import random
from game import db
from typing import Optional
from dcs.unitgroup import VehicleGroup
from game import db, Game
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGenerator}
def generate_missile_group(game, ground_object, faction_name: str):
def generate_missile_group(
game: Game, ground_object: MissileSiteGroundObject, faction_name: str
) -> Optional[VehicleGroup]:
"""
This generate a missiles group
:return: Nothing, but put the group reference inside the ground object

View File

@@ -2,15 +2,20 @@ import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class ScudGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
class ScudGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
def __init__(
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
) -> None:
super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
def generate(self) -> None:
# Scuds
self.add_unit(

View File

@@ -2,15 +2,20 @@ import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class V1GroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
class V1GroupGenerator(VehicleGroupGenerator[MissileSiteGroundObject]):
def __init__(
self, game: Game, ground_object: MissileSiteGroundObject, faction: Faction
) -> None:
super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
def generate(self) -> None:
# Ramps
self.add_unit(

View File

@@ -1,6 +1,6 @@
import random
import time
from typing import List
from typing import List, Any
from dcs.country import Country
@@ -256,7 +256,7 @@ class NameGenerator:
existing_alphas: List[str] = []
@classmethod
def reset(cls):
def reset(cls) -> None:
cls.number = 0
cls.infantry_number = 0
cls.convoy_number = 0
@@ -265,7 +265,7 @@ class NameGenerator:
cls.existing_alphas = []
@classmethod
def reset_numbers(cls):
def reset_numbers(cls) -> None:
cls.number = 0
cls.infantry_number = 0
cls.aircraft_number = 0
@@ -273,7 +273,9 @@ class NameGenerator:
cls.cargo_ship_number = 0
@classmethod
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
def next_aircraft_name(
cls, country: Country, parent_base_id: int, flight: Flight
) -> str:
cls.aircraft_number += 1
try:
if flight.custom_name:
@@ -293,7 +295,9 @@ class NameGenerator:
)
@classmethod
def next_unit_name(cls, country: Country, parent_base_id: int, unit_type: UnitType):
def next_unit_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
) -> str:
cls.number += 1
return "unit|{}|{}|{}|{}|".format(
country.id, cls.number, parent_base_id, unit_type.name
@@ -301,8 +305,8 @@ class NameGenerator:
@classmethod
def next_infantry_name(
cls, country: Country, parent_base_id: int, unit_type: UnitType
):
cls, country: Country, parent_base_id: int, unit_type: UnitType[Any]
) -> str:
cls.infantry_number += 1
return "infantry|{}|{}|{}|{}|".format(
country.id,
@@ -312,17 +316,17 @@ class NameGenerator:
)
@classmethod
def next_awacs_name(cls, country: Country):
def next_awacs_name(cls, country: Country) -> str:
cls.number += 1
return "awacs|{}|{}|0|".format(country.id, cls.number)
@classmethod
def next_tanker_name(cls, country: Country, unit_type: AircraftType):
def next_tanker_name(cls, country: Country, unit_type: AircraftType) -> str:
cls.number += 1
return "tanker|{}|{}|0|{}".format(country.id, cls.number, unit_type.name)
@classmethod
def next_carrier_name(cls, country: Country):
def next_carrier_name(cls, country: Country) -> str:
cls.number += 1
return "carrier|{}|{}|0|".format(country.id, cls.number)
@@ -337,7 +341,7 @@ class NameGenerator:
return f"Cargo Ship {cls.cargo_ship_number:03}"
@classmethod
def random_objective_name(cls):
def random_objective_name(cls) -> str:
if cls.animals:
animal = random.choice(cls.animals)
cls.animals.remove(animal)

View File

@@ -15,7 +15,7 @@ class RadioFrequency:
#: The frequency in kilohertz.
hertz: int
def __str__(self):
def __str__(self) -> str:
if self.hertz >= 1000000:
return self.format("MHz", 1000000)
return self.format("kHz", 1000)

View File

@@ -14,25 +14,21 @@ class BoforsGenerator(AirDefenseGroupGenerator):
"""
name = "Bofors AAA"
price = 75
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
def generate(self) -> None:
index = 0
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
AirDefence.Bofors40,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
index = index + 1
self.add_unit(
AirDefence.Bofors40,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -23,31 +23,26 @@ class FlakGenerator(AirDefenseGroupGenerator):
"""
name = "Flak Site"
price = 135
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(20, 35)
def generate(self) -> None:
index = 0
mixed = random.choice([True, False])
unit_type = random.choice(GFLAK)
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
unit_type,
"AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5),
self.heading,
)
for i in range(4):
index = index + 1
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
self.add_unit(
unit_type,
"AAA#" + str(index),
self.position.x + spacing_x * i + random.randint(1, 5),
self.position.y + spacing_y * i + random.randint(1, 5),
self.heading,
)
if mixed:
unit_type = random.choice(GFLAK)
if mixed:
unit_type = random.choice(GFLAK)
# Search lights
search_pos = self.get_circular_position(random.randint(2, 3), 80)
@@ -86,8 +81,10 @@ class FlakGenerator(AirDefenseGroupGenerator):
)
# Some Opel Blitz trucks
for i in range(int(max(1, grid_x / 2))):
for j in range(int(max(1, grid_x / 2))):
index = 0
for i in range(int(max(1, 2))):
for j in range(int(max(1, 2))):
index += 1
self.add_unit(
Unarmed.Blitz_36_6700A,
"BLITZ#" + str(index),

View File

@@ -14,9 +14,8 @@ class Flak18Generator(AirDefenseGroupGenerator):
"""
name = "WW2 Flak Site"
price = 40
def generate(self):
def generate(self) -> None:
spacing = random.randint(30, 60)
index = 0

View File

@@ -13,12 +13,8 @@ class KS19Generator(AirDefenseGroupGenerator):
"""
name = "KS-19 AAA Site"
price = 98
def generate(self):
spacing = random.randint(10, 40)
def generate(self) -> None:
self.add_unit(
highdigitsams.AAA_SON_9_Fire_Can,
"TR",
@@ -28,16 +24,17 @@ class KS19Generator(AirDefenseGroupGenerator):
)
index = 0
for i in range(3):
for j in range(3):
index = index + 1
self.add_unit(
highdigitsams.AAA_100mm_KS_19,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
index = index + 1
self.add_unit(
highdigitsams.AAA_100mm_KS_19,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -14,9 +14,8 @@ class AllyWW2FlakGenerator(AirDefenseGroupGenerator):
"""
name = "WW2 Ally Flak Site"
price = 140
def generate(self):
def generate(self) -> None:
positions = self.get_circular_position(4, launcher_distance=30, coverage=360)
for i, position in enumerate(positions):

View File

@@ -12,10 +12,9 @@ class ZSU57Generator(AirDefenseGroupGenerator):
"""
name = "ZSU-57-2 Group"
price = 60
def generate(self):
num_launchers = 5
def generate(self) -> None:
num_launchers = 4
positions = self.get_circular_position(
num_launchers, launcher_distance=110, coverage=360
)

View File

@@ -14,25 +14,20 @@ class ZU23InsurgentGenerator(AirDefenseGroupGenerator):
"""
name = "Zu-23 Site"
price = 56
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10, 40)
def generate(self) -> None:
index = 0
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(
AirDefence.ZU_23_Closed_Insurgent,
"AAA#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
self.heading,
)
for i in range(4):
index = index + 1
spacing_x = random.randint(10, 40)
spacing_y = random.randint(10, 40)
self.add_unit(
AirDefence.ZU_23_Closed_Insurgent,
"AAA#" + str(index),
self.position.x + spacing_x * i,
self.position.y + spacing_y * i,
self.heading,
)
@classmethod
def range(cls) -> AirDefenseRange:

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from enum import Enum
from typing import Iterator, List
@@ -6,36 +8,68 @@ from dcs.unitgroup import VehicleGroup
from game import Game
from game.theater.theatergroundobject import SamGroundObject
from gen.sam.group_generator import GroupGenerator
from gen.sam.group_generator import VehicleGroupGenerator
class SkynetRole(Enum):
#: A radar SAM that should be controlled by Skynet.
Sam = "Sam"
#: A radar SAM that should be controlled and used as an EWR by Skynet.
SamAsEwr = "SamAsEwr"
#: An air defense unit that should be used as point defense by Skynet.
PointDefense = "PD"
#: All other types of groups that might be present in a SAM TGO. This includes
#: SHORADS, AAA, supply trucks, etc. Anything that shouldn't be controlled by Skynet
#: should use this role.
NoSkynetBehavior = "NoSkynetBehavior"
class AirDefenseRange(Enum):
AAA = "AAA"
Short = "short"
Medium = "medium"
Long = "long"
AAA = ("AAA", SkynetRole.NoSkynetBehavior)
Short = ("short", SkynetRole.NoSkynetBehavior)
Medium = ("medium", SkynetRole.Sam)
Long = ("long", SkynetRole.SamAsEwr)
def __init__(self, description: str, default_role: SkynetRole) -> None:
self.range_name = description
self.default_role = default_role
class AirDefenseGroupGenerator(GroupGenerator, ABC):
class AirDefenseGroupGenerator(VehicleGroupGenerator[SamGroundObject], ABC):
"""
This is the base for all SAM group generators
"""
price: int
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
ground_object.skynet_capable = True
super().__init__(game, ground_object)
self.vg.name = self.group_name_for_role(self.vg.id, self.primary_group_role())
self.auxiliary_groups: List[VehicleGroup] = []
def add_auxiliary_group(self, name_suffix: str) -> VehicleGroup:
group = VehicleGroup(
self.game.next_group_id(), "|".join([self.go.group_name, name_suffix])
)
def add_auxiliary_group(self, role: SkynetRole) -> VehicleGroup:
gid = self.game.next_group_id()
group = VehicleGroup(gid, self.group_name_for_role(gid, role))
self.auxiliary_groups.append(group)
return group
def group_name_for_role(self, gid: int, role: SkynetRole) -> str:
if role is SkynetRole.NoSkynetBehavior:
# No special naming needed for air defense groups that don't participate in
# Skynet.
return f"{self.go.group_name}|{gid}"
# For those that do, we need a prefix of `$COLOR|SAM| so our Skynet config picks
# the group up at all. To support PDs we need to append the ID of the TGO so
# that the PD will know which group it's protecting. We then append the role so
# our config knows what to do with the group, and finally the GID of *this*
# group to ensure no conflicts.
return "|".join(
[self.go.faction_color, "SAM", str(self.go.group_id), role.value, str(gid)]
)
def get_generated_group(self) -> VehicleGroup:
raise RuntimeError(
"Deprecated call to AirDefenseGroupGenerator.get_generated_group "
@@ -52,3 +86,7 @@ class AirDefenseGroupGenerator(GroupGenerator, ABC):
@abstractmethod
def range(cls) -> AirDefenseRange:
...
@classmethod
def primary_group_role(cls) -> SkynetRole:
return cls.range().default_role

View File

@@ -17,9 +17,8 @@ class EarlyColdWarFlakGenerator(AirDefenseGroupGenerator):
"""
name = "Early Cold War Flak Site"
price = 74
def generate(self):
def generate(self) -> None:
spacing = random.randint(30, 60)
index = 0
@@ -90,9 +89,8 @@ class ColdWarFlakGenerator(AirDefenseGroupGenerator):
"""
name = "Cold War Flak Site"
price = 72
def generate(self):
def generate(self) -> None:
spacing = random.randint(30, 60)
index = 0

View File

@@ -18,6 +18,7 @@ from gen.sam.ewrs import (
StraightFlushGenerator,
TallRackGenerator,
EwrGenerator,
TinShieldGenerator,
)
EWR_MAP = {
@@ -31,6 +32,7 @@ EWR_MAP = {
"SnowDriftGenerator": SnowDriftGenerator,
"StraightFlushGenerator": StraightFlushGenerator,
"HawkEwrGenerator": HawkEwrGenerator,
"TinShieldGenerator": TinShieldGenerator,
}

View File

@@ -1,23 +1,19 @@
from typing import Type
from dcs.vehicles import AirDefence
from dcs.unittype import VehicleType
from dcs.vehicles import AirDefence
from gen.sam.group_generator import GroupGenerator
from game.theater.theatergroundobject import EwrGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class EwrGenerator(GroupGenerator):
class EwrGenerator(VehicleGroupGenerator[EwrGroundObject]):
unit_type: Type[VehicleType]
@classmethod
def name(cls) -> str:
return cls.unit_type.name
@staticmethod
def price() -> int:
# TODO: Differentiate sites.
return 20
def generate(self) -> None:
self.add_unit(
self.unit_type, "EWR", self.position.x, self.position.y, self.heading
@@ -106,3 +102,9 @@ class HawkEwrGenerator(EwrGenerator):
"""
unit_type = AirDefence.Hawk_sr
class TinShieldGenerator(EwrGenerator):
"""19ZH6 "Tin Shield" EWR."""
unit_type = AirDefence.RLS_19J6

View File

@@ -12,9 +12,8 @@ class FreyaGenerator(AirDefenseGroupGenerator):
"""
name = "Freya EWR Site"
price = 60
def generate(self):
def generate(self) -> None:
# TODO : would be better with the Concrete structure that is supposed to protect it
self.add_unit(

View File

@@ -1,58 +1,93 @@
from __future__ import annotations
import logging
import math
import random
from typing import TYPE_CHECKING, Type
from collections import Iterable
from typing import TYPE_CHECKING, Type, TypeVar, Generic, Any
from dcs import unitgroup
from dcs.mapping import Point
from dcs.point import PointAction
from dcs.unit import Ship, Vehicle
from dcs.unittype import VehicleType
from dcs.unit import Ship, Vehicle, Unit
from dcs.unitgroup import ShipGroup, VehicleGroup
from dcs.unittype import VehicleType, UnitType, ShipType
from game.dcs.groundunittype import GroundUnitType
from game.factions.faction import Faction
from game.theater.theatergroundobject import TheaterGroundObject
from game.theater.theatergroundobject import TheaterGroundObject, NavalGroundObject
if TYPE_CHECKING:
from game.game import Game
GroupT = TypeVar("GroupT", VehicleGroup, ShipGroup)
UnitT = TypeVar("UnitT", bound=Unit)
UnitTypeT = TypeVar("UnitTypeT", bound=Type[UnitType])
TgoT = TypeVar("TgoT", bound=TheaterGroundObject[Any])
# TODO: Generate a group description rather than a pydcs group.
# It appears that all of this work gets redone at miz generation time (see
# groundobjectsgen for an example). We can do less work and include the data we
# care about in the format we want if we just generate our own group description
# types rather than pydcs groups.
class GroupGenerator:
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
class GroupGenerator(Generic[GroupT, UnitT, UnitTypeT, TgoT]):
def __init__(self, game: Game, ground_object: TgoT, group: GroupT) -> None:
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.go.group_name)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
self.price = 0
self.vg: GroupT = group
def generate(self):
def generate(self) -> None:
raise NotImplementedError
def get_generated_group(self) -> unitgroup.VehicleGroup:
def get_generated_group(self) -> GroupT:
return self.vg
def add_unit(
self,
unit_type: Type[VehicleType],
unit_type: UnitTypeT,
name: str,
pos_x: float,
pos_y: float,
heading: int,
) -> Vehicle:
) -> UnitT:
return self.add_unit_to_group(
self.vg, unit_type, name, Point(pos_x, pos_y), heading
)
def add_unit_to_group(
self,
group: unitgroup.VehicleGroup,
group: GroupT,
unit_type: UnitTypeT,
name: str,
position: Point,
heading: int,
) -> UnitT:
raise NotImplementedError
class VehicleGroupGenerator(
Generic[TgoT], GroupGenerator[VehicleGroup, Vehicle, Type[VehicleType], TgoT]
):
def __init__(self, game: Game, ground_object: TgoT) -> None:
super().__init__(
game,
ground_object,
unitgroup.VehicleGroup(game.next_group_id(), ground_object.group_name),
)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
def generate(self) -> None:
raise NotImplementedError
def add_unit_to_group(
self,
group: VehicleGroup,
unit_type: Type[VehicleType],
name: str,
position: Point,
@@ -62,9 +97,19 @@ class GroupGenerator:
unit.position = position
unit.heading = heading
group.add_unit(unit)
# get price of unit to calculate the real price of the whole group
try:
ground_unit_type = next(GroundUnitType.for_dcs_type(unit_type))
self.price += ground_unit_type.price
except StopIteration:
logging.error(f"Cannot get price for unit {unit_type.name}")
return unit
def get_circular_position(self, num_units, launcher_distance, coverage=90):
def get_circular_position(
self, num_units: int, launcher_distance: int, coverage: int = 90
) -> Iterable[tuple[float, float, int]]:
"""
Given a position on the map, array a group of units in a circle a uniform distance from the unit
:param num_units:
@@ -90,39 +135,47 @@ class GroupGenerator:
else:
current_offset = self.heading
current_offset -= outer_offset * (math.ceil(num_units / 2) - 1)
for x in range(1, num_units + 1):
positions.append(
(
self.position.x
+ launcher_distance * math.cos(math.radians(current_offset)),
self.position.y
+ launcher_distance * math.sin(math.radians(current_offset)),
current_offset,
)
for _ in range(1, num_units + 1):
x: float = self.position.x + launcher_distance * math.cos(
math.radians(current_offset)
)
y: float = self.position.y + launcher_distance * math.sin(
math.radians(current_offset)
)
heading = current_offset
positions.append((x, y, int(heading)))
current_offset += outer_offset
return positions
class ShipGroupGenerator(GroupGenerator):
class ShipGroupGenerator(
GroupGenerator[ShipGroup, Ship, Type[ShipType], NavalGroundObject]
):
"""Abstract class for other ship generator classes"""
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
def __init__(self, game: Game, ground_object: NavalGroundObject, faction: Faction):
super().__init__(
game,
ground_object,
unitgroup.ShipGroup(game.next_group_id(), ground_object.group_name),
)
self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(), self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
def generate(self) -> None:
raise NotImplementedError
def add_unit_to_group(
self,
group: ShipGroup,
unit_type: Type[ShipType],
name: str,
position: Point,
heading: int,
) -> Ship:
unit = Ship(self.game.next_unit_id(), f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x
unit.position.y = pos_y
unit.position = position
unit.heading = heading
self.vg.add_unit(unit)
group.add_unit(unit)
return unit

View File

@@ -14,10 +14,9 @@ class AvengerGenerator(AirDefenseGroupGenerator):
"""
name = "Avenger Group"
price = 62
def generate(self):
num_launchers = random.randint(2, 3)
def generate(self) -> None:
num_launchers = 2
self.add_unit(
Unarmed.M_818,

View File

@@ -14,10 +14,9 @@ class ChaparralGenerator(AirDefenseGroupGenerator):
"""
name = "Chaparral Group"
price = 66
def generate(self):
num_launchers = random.randint(2, 4)
def generate(self) -> None:
num_launchers = 2
self.add_unit(
Unarmed.M_818,

View File

@@ -14,23 +14,20 @@ class GepardGenerator(AirDefenseGroupGenerator):
"""
name = "Gepard Group"
price = 50
def generate(self):
self.add_unit(
AirDefence.Gepard,
"SPAAA",
self.position.x,
self.position.y,
self.heading,
def generate(self) -> None:
num_launchers = 2
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)
if random.randint(0, 1) == 1:
for i, position in enumerate(positions):
self.add_unit(
AirDefence.Gepard,
"SPAAA2",
self.position.x,
self.position.y,
self.heading,
"SPAA#" + str(i),
position[0],
position[1],
position[2],
)
self.add_unit(
Unarmed.M_818,

View File

@@ -28,6 +28,7 @@ from gen.sam.sam_gepard import GepardGenerator
from gen.sam.sam_hawk import HawkGenerator
from gen.sam.sam_hq7 import HQ7Generator
from gen.sam.sam_linebacker import LinebackerGenerator
from gen.sam.sam_nasams import NasamBGenerator, NasamCGenerator
from gen.sam.sam_patriot import PatriotGenerator
from gen.sam.sam_rapier import RapierGenerator
from gen.sam.sam_roland import RolandGenerator
@@ -100,6 +101,8 @@ SAM_MAP: Dict[str, Type[AirDefenseGroupGenerator]] = {
"SA20Generator": SA20Generator,
"SA20BGenerator": SA20BGenerator,
"SA23Generator": SA23Generator,
"NasamBGenerator": NasamBGenerator,
"NasamCGenerator": NasamCGenerator,
}

View File

@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -15,9 +16,8 @@ class HawkGenerator(AirDefenseGroupGenerator):
"""
name = "Hawk Site"
price = 115
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.Hawk_sr,
"SR",
@@ -41,7 +41,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
)
# Triple A for close range defense
aa_group = self.add_auxiliary_group("AA")
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
self.add_unit_to_group(
aa_group,
AirDefence.Vulcan,
@@ -50,7 +50,7 @@ class HawkGenerator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(3, 6)
num_launchers = 6
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=180
)

View File

@@ -6,6 +6,7 @@ from dcs.vehicles import AirDefence
from gen.sam.airdefensegroupgenerator import (
AirDefenseRange,
AirDefenseGroupGenerator,
SkynetRole,
)
@@ -15,9 +16,8 @@ class HQ7Generator(AirDefenseGroupGenerator):
"""
name = "HQ-7 Site"
price = 120
def generate(self):
def generate(self) -> None:
self.add_unit(
AirDefence.HQ_7_STR_SP,
"STR",
@@ -25,16 +25,9 @@ class HQ7Generator(AirDefenseGroupGenerator):
self.position.y,
self.heading,
)
self.add_unit(
AirDefence.HQ_7_LN_SP,
"LN",
self.position.x + 20,
self.position.y,
self.heading,
)
# Triple A for close range defense
aa_group = self.add_auxiliary_group("AA")
aa_group = self.add_auxiliary_group(SkynetRole.NoSkynetBehavior)
self.add_unit_to_group(
aa_group,
AirDefence.Ural_375_ZU_23,
@@ -50,7 +43,7 @@ class HQ7Generator(AirDefenseGroupGenerator):
self.heading,
)
num_launchers = random.randint(0, 3)
num_launchers = 2
if num_launchers > 0:
positions = self.get_circular_position(
num_launchers, launcher_distance=120, coverage=360

View File

@@ -14,10 +14,9 @@ class LinebackerGenerator(AirDefenseGroupGenerator):
"""
name = "Linebacker Group"
price = 75
def generate(self):
num_launchers = random.randint(2, 4)
def generate(self) -> None:
num_launchers = 2
self.add_unit(
Unarmed.M_818,

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