Compare commits

..

744 Commits

Author SHA1 Message Date
Magnus Wolffelt
a6c544a6e6 Stop using Su-34 for CAP missions, update changelog 2021-08-10 13:39:40 +02:00
Magnus Wolffelt
46755240d2 Update changelog with sam site heading fix 2021-08-10 13:20:08 +02:00
RndName
2141ee8a0a calculate heading to center of conflict for sams 2021-08-10 13:05:06 +02:00
Magnus Wolffelt
a71a053d05 Update changelog with Marianas fix for 4.1.1 2021-08-10 11:59:59 +02:00
Khopa
6ddea481e4 Corrected some bugs preventing marianas campaigns from running 2021-08-10 11:59:59 +02:00
Dan Albert
ab9fd69493 Move 4.x dev to 4.1.1. 2021-08-08 12:53:49 -07:00
bgreman
a54f0e792d Indiciated gripen support version 2021-08-08 15:30:56 -04:00
RndName
f0476fcbb3 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 13:20:26 +02:00
Dan Albert
263c1cc012 Remove abandoned campaigns.
(cherry picked from commit 07f8a203ea)
2021-08-05 19:33:12 -07:00
RndName
c06550ca64 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-05 22:27:11 +02:00
Magnus Wolffelt
03471c6c13 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-05 12:19:59 +02:00
Khopa
d6c1456108 Campaign Update 8.0 : operation_dynamo.miz, updated mission targets ids
(cherry picked from commit f2608cecd5)
2021-08-04 23:35:41 -07:00
Khopa
11389f50c8 Campaign Update 8.0 : golan_heights_lite.miz, updated mission targets ids
(cherry picked from commit c95d5464d8)
2021-08-04 23:35:36 -07:00
Khopa
70ed11bf69 Campaign Update 8.0 : caen_to_evreux.miz, updated mission targets ids
(cherry picked from commit 0ac7466a81)
2021-08-04 23:35:35 -07:00
Dan Albert
26cfa4e30a 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-04 23:34:37 -07:00
Kangwook Lee
8282a95569 Wrap lines for NotesPage
(cherry picked from commit 8608b73009)
2021-08-04 23:28:07 -07:00
Dan Albert
283fc26385 Stop cluttering the kneeboard with empty notes.
(cherry picked from commit d11174da21)
2021-08-04 23:28:07 -07:00
Kangwook Lee
5729cc5f42 Fix text foreground color for dark kneeboard
(cherry picked from commit 77e62d5a54)
2021-08-04 23:13:18 -07:00
Magnus Wolffelt
3fadaa9a5a 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-03 18:46:30 +02:00
Magnus Wolffelt
3de4feec7e Changelog updates for 4.x.
Regarding patrol speeds and helo fix.
2021-08-03 13:12:02 +02:00
Magnus Wolffelt
570108e2a7 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-03 12:48:37 +02:00
Dan Albert
adc7a41941 Changelog updates for 4.x.
(cherry picked from commit bef015eb57)
2021-08-01 17:27:15 -07:00
Magnus Wolffelt
fcd8d6c76b Tweak max-speed-based patrol altitudes
(cherry picked from commit 6621421a6f)
2021-08-01 15:32:09 -07:00
Magnus Wolffelt
3dc71a558d Estimate preferred patrol altitude based on max speed
(cherry picked from commit a3e3e9046f)
2021-08-01 15:32:03 -07:00
RndName
ab1c682f9b fix for wrong patrol speed
(cherry picked from commit 04cdb6fbfc)
2021-08-01 15:31:57 -07:00
bgreman
4e489fe75d Update skynet plugin (#1478)
(cherry picked from commit 8c7e56a2bd)
2021-08-01 11:37:21 -04:00
RndName
5d8e0b3b1e 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-07-31 15:01:20 -07:00
Mustang-25
28fbd448b0 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-07-31 14:57:56 -07:00
Dan Albert
93d85830a9 Update USN 2005 faction.
https://github.com/dcs-liberation/dcs_liberation/issues/1427
(cherry picked from commit bef85963a6)
2021-07-31 14:47:41 -07:00
Mustang-25
7de399090b 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-07-31 14:42:44 -07:00
Dan Albert
8a2a21bf11 Campaign updates from Starfire.
(cherry picked from commit 3be57efa97)
2021-07-31 14:40:56 -07:00
Dan Albert
62716f7b86 Ack new campaign version for unaffected maps.
(cherry picked from commit 981d8510c2)
2021-07-31 14:38:25 -07:00
Khopa
5febba9640 Added Tin Shield EWR support
(cherry picked from commit 32f05dccd9)
2021-07-31 14:27:22 -07:00
Khopa
7dea51321f Added NASAMS support
(cherry picked from commit 4aac2d2b7b)
2021-07-31 14:27:17 -07:00
Dan Albert
836c184241 Add ALIC codes for the tin shield and NASAMS.
https://github.com/dcs-liberation/dcs_liberation/issues/1448
(cherry picked from commit 971d7e730a)
2021-07-31 14:26:48 -07:00
Dan Albert
50ec5b2832 Update the F-16 DEAD loadout to use JSOWs.
https://github.com/dcs-liberation/dcs_liberation/issues/1448
(cherry picked from commit 06f8b9b817)
2021-07-31 14:26:35 -07:00
Dan Albert
08919d4b8e 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-07-31 14:24:47 -07:00
Dan Albert
c1bdf55ff3 Update pydcs to latest master.
https://github.com/dcs-liberation/dcs_liberation/issues/1448
(cherry picked from commit 0cb41469ab)
2021-07-31 13:58:15 -07:00
Dan Albert
eee62fe84a Update pydcs to latest master.
(cherry picked from commit 9f23cb35a9)
2021-07-31 13:58:03 -07:00
bgreman
8a23792ae1 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-07-31 15:44:42 -04:00
bgreman
0332c32bb3 Updates gripen support fixes legacy DEAD loadouts 2021-07-31 12:15:54 -04:00
bgreman
9ed165b8bd 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-07-31 12:06:34 -04:00
RndName
ad8e70c250 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-07-30 16:47:44 -07:00
Mustang-25
e7e1e1cad4 Increment to Campaign v8.0
(cherry picked from commit 0117ab8aa4)
2021-07-30 16:37:28 -07:00
Mustang-25
c386ce8ea0 Increment to Campaign v8.0
(cherry picked from commit a5ade0c41a)
2021-07-30 16:37:13 -07:00
Mustang-25
a0574183d9 Increment to Campaign v8.0
(cherry picked from commit 4df12ae675)
2021-07-30 16:32:43 -07:00
Mustang-25
2749c050e7 Increment to Campaign v8.0
(cherry picked from commit 274a41f052)
2021-07-30 16:32:42 -07:00
Mustang-25
9267be5798 Increment to Campaign v8.0
(cherry picked from commit 3670c8f879)
2021-07-30 16:32:29 -07:00
Mustang-25
82a4d9194d Increment to Campaign v8.0
(cherry picked from commit e88bb442f3)
2021-07-30 16:32:15 -07:00
Dan Albert
339fa3d835 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-07-24 15:11:10 -07:00
Dan Albert
c0a9eb3473 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-07-21 17:11:58 -07:00
Dan Albert
04b53fa23d 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-07-12 13:48:33 -07:00
Dan Albert
a22f1d8e63 Correct int/float confusion in Point APIs.
The heading and distance calculations always return floats.

(cherry picked from commit 6ce02282e7)
2021-07-11 14:34:43 -07:00
Dan Albert
5860518f92 Use Pillow types from typeshed.
(cherry picked from commit a19a0b6789)
2021-07-11 13:54:19 -07:00
Dan Albert
68473ae63a 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-07-11 13:38:54 -07:00
Dan Albert
1ea13954ec 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-07-11 13:38:50 -07:00
Brock Greman
53a1c938c3 Fixing broken group generation.
(cherry picked from commit 469dd49def)
2021-07-11 13:38:41 -07:00
Dan Albert
229a2cd7a4 Fix some typing in preparation for pydcs types.
Not complete, but progress.

(cherry picked from commit 53f6a0b32b)
2021-07-11 13:38:40 -07:00
Dan Albert
19980e5d6b Flesh out typing information, enforce.
(cherry picked from commit fb9a0fe833)
2021-07-07 17:42:21 -07:00
Dan Albert
85be9df481 Disallow partially specified generics.
(cherry picked from commit 69c3d41a8a)
2021-07-07 16:07:58 -07:00
Dan Albert
9bc8b51794 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-07-07 16:07:57 -07:00
Dan Albert
e9bc3f3e69 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-07-07 16:07:56 -07:00
Dan Albert
3f5fdc580a 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-07-07 16:07:54 -07:00
Dan Albert
f4e02954b7 Add documentation for turn processing.
(cherry picked from commit 7983cd8d62)
2021-07-07 14:45:05 -07:00
RndName
e88dfc53c2 correct display of turn statistics 2021-07-07 14:12:42 -07:00
RndName
2926431dc7 replan opfor mission on sell or buy of tgos
(cherry picked from commit 7229b886e0)
2021-07-07 14:12:34 -07:00
Dan Albert
70a0341675 Note fix for empty convoy groups.
(cherry picked from commit 8b70d2674f)
2021-07-05 15:54:47 -07:00
RndName
251c84019f remove completely destroyed units from the convoy
(cherry picked from commit 8ba27cdaea)
2021-07-05 15:54:46 -07:00
bgreman
8fae7decca 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-07-03 14:52:21 -04:00
Dan Albert
3415525e2c 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-07-02 17:33:40 -07:00
Dan Albert
355ea9f9be Fix the legacy tanker.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1379

(cherry picked from commit 4add853473)
2021-07-02 17:18:31 -07:00
RndName
eff674c441 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-07-02 16:46:29 -07:00
RndName
9858e3e257 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-07-02 16:46:28 -07:00
Dan Albert
b206bcae56 Note the silkworm fix in the changelog.
(cherry picked from commit 3f42f1281d)
2021-07-02 16:28:35 -07:00
Mustang-25
3dbfa8ca60 Corrected Silkworm launcher name
(cherry picked from commit bab8384803)
2021-07-02 16:28:34 -07:00
Florian
779dc8ad70 Remove the randomness from SAM group size.
(cherry picked from commit 3f65928e9d)
2021-07-02 01:38:37 -07:00
Dan Albert
467de580c5 Bump version to 4.1.0. 2021-07-02 01:29:25 -07:00
Dan Albert
adbba788c6 4.0.1 -> 4.1.0
This includes new features now.

(cherry picked from commit 4e6659e7e8)
2021-07-02 01:29:09 -07:00
Chris Seagraves
99f359b46b Note TGO tooltip improvement in the changelog.
(cherry picked from commit 9e22d4b5df)
2021-07-02 01:27:07 -07:00
RndName
277df247b9 fixed lua data generation
(cherry picked from commit 357361de3d)
2021-07-02 01:25:18 -07:00
RndName
e0a4ceef67 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-07-02 01:25:17 -07:00
Dan Albert
234a998abe Minor formatting fix for the changelog.
(cherry picked from commit 20839853b7)
2021-07-01 20:07:40 -07:00
Chris Seagraves
2233141033 Fix for crash when clear weather.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1394

(cherry picked from commit bc2539b566)
2021-07-01 20:07:09 -07:00
Dan Albert
c47750b2d9 Remove debug cruft.
We don't need to print the description of every unit on startup.

(cherry picked from commit 2ef2eafdd3)
2021-07-01 19:37:46 -07:00
bgreman
3f8dfce9e0 Addresses #478 to clean up the angle summing functionality. (#1386)
(cherry picked from commit 9bd6f9ef47)
2021-06-30 23:59:03 -04:00
bgreman
20e7690a85 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-06-30 23:56:06 -04:00
bgreman
7f1e21b587 Fixes #240 by making statistics windows axis labels integers (#1370)
(cherry picked from commit 7ba4077f9f)
2021-06-30 23:54:31 -04:00
Mustang-25
bc3a75836d Update TGP Restriction Dates
TGP dates to more accurately reflect IRL IOC dates.

(cherry picked from commit 151f8bf329)
2021-06-30 19:17:45 -07:00
Chris Seagraves
f588c445ae Notes to kneeboard (#1375)
Adds global-level kneeboard notes.  Explicit save compatability with 4.0.0

(cherry picked from commit e94d48c265)
2021-06-30 18:08:44 -04:00
Fryderyk Wysocki
7807e2fc31 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-06-30 15:31:02 -04:00
Chris Seagraves
91bde9dccf Add Cloud Base Altitude to Weather Display (#1371)
Adds tooltip with cloud base altitude to weather panel

(cherry picked from commit f80696b724)
2021-06-30 15:23:13 -04:00
Chris Seagraves
de9d388b96 asset reference links 😎 (#1363)
Adds urls to unit info pages that don't have data.

(cherry picked from commit 5f5b5f69e3)
2021-06-30 15:05:48 -04:00
Chris Seagraves
c7d3f1a340 Update main.py (#1382)
(cherry picked from commit d99f8fef09)
2021-06-29 18:21:52 -04:00
Simon Clark
d3b44e5ba1 Merge branch 'develop-4.x' of https://github.com/dcs-liberation/dcs_liberation into develop-4.x 2021-06-26 22:39:45 +01:00
Dan Albert
ac0e29a54d Bump branch to 4.0.1-preview. 2021-06-26 14:40:53 -07:00
Dan Albert
91d08e2160 Add changelog section for 4.0.1.
(cherry picked from commit 0b90b53e09)
2021-06-26 14:40:40 -07:00
Simon Clark
d18d6b2422 Fix begin campaign button on reload. 2021-06-26 22:39:32 +01: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
d2cc3f673e Switch pydcs to a pip requirement. 2021-06-20 15:16:22 -07:00
Dan Albert
dc85644d71 Exclude weapon names and weights from comparisons.
Only the class ID matters, and the names sometimes change with new pydcs
updates.
2021-06-20 15:11:35 -07:00
SnappyComebacks
0b5bdf8151 Move max_group_size from S-3B to S-3B Tanker. 2021-06-20 14:13:23 -07:00
jsjlewis96
b27238a69a Moved Hind-F to separate faction, not operational at time, but only gunship pilotable 2021-06-20 14:13:03 -07:00
Mike
bb2bf78e8a Fix current_airlift_capacity always returning 0.
Squadron.aircraft is of type AircraftType, while TRANSPORT_CAPABLE is
a list of pydcs DcsUnitTypes. As a result, the intersection was always
empty causing the function to always return 0.
2021-06-20 13:56:00 -07:00
Chris Seagraves
7e17533cc6 Air/ground intel prettification. (#1285)
* Sort rows.
* Add totals to group headers.
* Indent content.
* Add space between sections.
2021-06-20 13:43:54 -07:00
Chris Seagraves
7808da118a Include the save name in the window title. 2021-06-20 13:33:27 -07:00
Mustang-25
4259cf8764 Add IAF Eagle Sqn 2021-06-20 13:33:01 -07:00
Mustang-25
994c55945e Add IAF Strike Eagle Sqn 2021-06-20 13:33:01 -07:00
Mustang-25
f20c145ece Add IAF Viper Sqns 2021-06-20 13:33:01 -07:00
SnappyComebacks
5b31026e1c Fix UI to obey max group sizes.
This also adds max group sizes for aircraft that need it but don't
according to DCS. Only the first tanker or AEW&C unit in a group can be
contacted by radio.
2021-06-20 13:32:00 -07:00
Simon Clark
39fe5951f7 Add factories to Russian Intervention Campaign. 2021-06-20 19:23:07 +01:00
Simon Clark
9d767c3dd8 Fix destination opacity bug. 2021-06-20 19:06:24 +01:00
Simon Clark
2a3f9bf81c More Russian Intervention campaign work. 2021-06-20 18:48:46 +01:00
Simon Clark
3fd4359cb1 Adds Russian Intervention 2015 campaign. 2021-06-20 17:08:44 +01:00
Khopa
ca1be580df Squadrons : Added french Sa-342 and Mirage 2000-5 squadrons 2021-06-20 17:43:35 +02:00
Khopa
28820f2e64 Squadrons : Allow unicode characters in squadrons names 2021-06-20 17:43:07 +02:00
Simon Clark
6c3987ec86 Updates the intel box text for turn 0.
It was a bit misleading beforehand, as there were no forces on either side.
2021-06-20 15:56:53 +01:00
Simon Clark
089eb9e86b Changelog. 2021-06-20 15:36:40 +01:00
Simon Clark
0793e9afc5 Addresses #1184.
CV and LHA icons are now the same colour as airfields.

Destination markers now have transparency.
2021-06-20 15:33:01 +01:00
Dan Albert
1e2522375b Increase squadron size and replenishment rate.
Given the current lack of control over the number of squadrons this
needs be be raised to make it have less of an impact.
2021-06-19 23:24:23 -07:00
Dan Albert
e09f53da8f Fix exceptions when no aircraft are selected.
This commonly happens during reset of the UI, but also happens when the
player is out of aircraft.
2021-06-19 20:26:33 -07:00
Dan Albert
29b4b62a44 Revert "Add Around the Mountain campaign."
Doesn't include basic requirements like factories.

This reverts commit 30cab8e3a7.
2021-06-19 15:11:41 -07:00
Dan Albert
b1a63db1fc Correct some ILS/VOR frequencies in Syria.
`MHz(110, 30)` is 110.03 MHz, not 110.30 MHz.
2021-06-19 15:07:45 -07:00
Dan Albert
9940dc8451 Fix type annotations for some UI code. 2021-06-19 15:03:39 -07:00
Dan Albert
703c68eb66 Add Akrotiri to Inherent Resolve. 2021-06-19 12:17:47 -07:00
Dan Albert
3338df9836 Don't propose missions the air wing can't plan.
We were doignt his for escorts, but now that we quit planning as soon as
we find an unplannable mission (to save money for higher priority
missions), if we hit an early unplannable mission like BARCAP no other
missions wil be planned.

Maybe fixes https://github.com/dcs-liberation/dcs_liberation/issues/1228
2021-06-19 11:47:43 -07:00
SnappyComebacks
dc4794b246 Add kneeboard data for new Syria Cyprus airfields. (#1277)
* Add kneeboard data for new Syria Cyprus airfields.
2021-06-19 12:29:21 -06:00
Dan Albert
b130c9882a Remove max distance for AEW&C auto planning.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1259
2021-06-19 11:27:52 -07:00
Dan Albert
5f8b838652 Add new campaign minor version for Cyprus. 2021-06-19 11:18:31 -07:00
Dan Albert
4efd1b5d3e Note EWR waypoint selector fix. 2021-06-19 11:16:20 -07:00
RndName
ad6ed21b6b Add EWR objects to predefined waypoints 2021-06-19 11:01:35 -07:00
Dan Albert
2ffaa71bb5 Note some new features in the changelog. 2021-06-19 10:47:59 -07:00
RndName
1763f59320 Allow deletion of multiple waypoints by selection
#1221
2021-06-19 10:40:40 -07:00
RndName
08d32ffc77 Allow shift/ctrl click to buy/sell multiple units.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1221
2021-06-19 10:36:33 -07:00
RndName
7e3cebb96d Fix purchase groups.
The new class PurchaseGroup coming in with commit 9bb986c was not
initiallized correctly.

This causes the bug that the update function is not working when you
for example open the AircraftRecruitmentMenu press "+" or "-", close
the dialog and then open ArmorRecruitmentMenu. If you then want to buy
or sell the update function will raise an error "Internal C++ Object
Already Deleted".
2021-06-19 10:33:35 -07:00
Jake Lewis
930fb404af Updated Hind-F price for rebalance 2021-06-19 03:43:55 -07:00
jsjlewis96
6cd711a1e2 Added option to disable AI pilot levelling 2021-06-19 03:03:50 -07:00
docofmur
1bcc332885 Syrian Terrain update 2021-06-19 02:28:11 -07:00
Dan Albert
9bb986cff9 Update *all* buy/sell buttons, not just the row.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1207
2021-06-18 20:14:44 -07:00
Dan Albert
1247942bf1 Note fix for convoy naming bug. 2021-06-18 19:41:03 -07:00
Dan Albert
95d3ff4cbe Don't show ground unit menu at carriers.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1220
2021-06-18 19:39:42 -07:00
Marcel
0a874a28ef Fix group name for EWRs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1246
2021-06-18 19:26:42 -07:00
Dan Albert
2dee702060 Persist name generator state to the save game.
This is a bit of an ugly hack but it's effectively what we would need
anyway. We could clean up the global replacement by making the name
generator _only_ a property of Game and plumbing it through to a large
number of places. Could maybe also use `__getstate__` and `__setstate__`
tricks to save `naming.namegen` to the file even without making it truly
a part of Game.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1247
2021-06-18 19:24:23 -07:00
Dan Albert
4ea66477fe Add options for changing pilot limits and rates. 2021-06-18 18:33:15 -07:00
Dan Albert
d3be732566 Fix tooltips for scuds/silkworms.
Same problem as the ground object menu, same cleanup required at some
point.
2021-06-18 18:14:20 -07:00
Dan Albert
933517055e Fix ground object menus for scuds and silkworms.
These really need to be added to the unit data, but this will do as a
stop gap since the group generators need an overhaul anyway.
2021-06-18 18:06:05 -07:00
Dan Albert
040a3d9b36 Add coastal defenses and missiles to Abu Dhabi. 2021-06-18 18:02:33 -07:00
Florian
2c859bf280 Rebalance aircraft prices.
Main goal here is to make sure that warbirds don't cost more than early
jets, but this includes rebalancing of all aircraft.
2021-06-18 17:53:19 -07:00
jsjlewis96
fe227e02b8 Shows total at top for economic intel 2021-06-18 17:35:18 -07:00
Dan Albert
c68e583c20 Note the Skynet EWR fix. 2021-06-18 17:02:22 -07:00
Khopa
6620d56859 Factions : Added hind-P to Russia 75. 2021-06-19 01:58:11 +02:00
Dan Albert
1ec72d3e94 Fix mission generation when infantry is disabled.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1256
2021-06-18 16:52:03 -07:00
Dan Albert
5c3bb75786 Fix EPLRS for aircraft.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1237
2021-06-18 16:48:59 -07:00
Dan Albert
a90cb0dad9 Fix carrier names in factions.
The rest of the ships were covered but it looks like carriers were
missed.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1258
2021-06-18 16:42:34 -07:00
Khopa
8854a491ab Custom banner for Mi-24P (different from Mi-24V) 2021-06-19 01:32:43 +02:00
UKayeF
74e8073328 add Mi-24V Hind icons as placeholders for Mi-24P icons yet to come 2021-06-19 01:32:43 +02:00
Dan Albert
a6d62a7596 Add some missing changes to the changelog. 2021-06-18 16:23:44 -07:00
Dan Albert
980a224d02 Move fixes to the correct changelog section.
What was to be 3.1.0 is now 4.0.0 to accomodate that DCS update.
2021-06-18 16:11:02 -07:00
Khopa
0c6b83fc35 Added default Mi-24P payloads 2021-06-19 01:05:14 +02:00
UKayeF
6d2310f59d add Mi-24P as CAS capable aircraft - unsure which other tasks it could be useful for? 2021-06-19 00:54:20 +02:00
jsjlewis96
05107fab1c Added Mi-24P to factions post '80 that use Hind-E 2021-06-19 00:51:49 +02:00
jsjlewis96
285bed65c6 Added Mi-24P to Russia 1990 & 2010 factions 2021-06-19 00:51:47 +02:00
Khopa
b523c23e7a Added french Mirage-2000C squadrons 2021-06-18 18:38:05 +02:00
Khopa
4c9a028a4e Fixed pydcs error in test_factions.py 2021-06-18 18:19:48 +02:00
C. Perreau
cea970f065 Merge pull request #1248 from jsjlewis96/dot-neutral
#987 Neutral dot labels in options
2021-06-18 18:13:09 +02:00
Khopa
d8511fab1d Fixed pydcs error with missiles units 2021-06-18 18:08:03 +02:00
Khopa
b2d10e92e9 Fixed requirements.txt changes not required 2021-06-18 18:01:44 +02:00
Khopa
0582d5e2b6 Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2021-06-18 18:00:58 +02:00
C. Perreau
da2b56b5b1 Merge pull request #1252 from docofmur/terrain_update_cyprus
Syria Terrian map update
2021-06-18 18:00:09 +02:00
docofmur
46c15f37c5 exclusion zone update 1 test change on caucases, massive update on syria 2021-06-18 17:58:08 +02:00
Dan Albert
4ddc02d7fe Update pydcs. 2021-06-17 23:01:20 -07:00
Dan Albert
4c3ac0af91 Adapt to DCS update. 2021-06-17 22:58:46 -07:00
Dan Albert
edd0b90576 Update pydcs. 2021-06-17 22:22:47 -07:00
Dan Albert
11dca41945 Remove unused file. 2021-06-17 22:11:00 -07:00
Dan Albert
75c4724200 Delete the obsolete converter. 2021-06-17 22:10:15 -07:00
Dan Albert
09704b6f37 Add a wrapper type for ground unit info. 2021-06-17 22:09:17 -07:00
Dan Albert
8a0824880e Add unit classes and weights to infantry. 2021-06-17 22:09:16 -07:00
Dan Albert
d84abf021e Fix some bad unit data. 2021-06-17 22:09:16 -07:00
Dan Albert
e7223da19f Convert remaining unit data. 2021-06-17 22:09:16 -07:00
Dan Albert
499d143199 Add a converter for groun unit info. 2021-06-17 22:09:06 -07:00
Dan Albert
fefeb3c006 Fix broken factions. 2021-06-17 18:49:55 -07:00
Dan Albert
6aeee933d2 Fix broken ground unit data. 2021-06-17 18:39:45 -07:00
Dan Albert
34c0698c48 Remove unused aircraft from the old unit data. 2021-06-17 18:39:45 -07:00
jsjlewis96
1cc1a00820 Updated changelog 2021-06-17 23:12:57 +01:00
jsjlewis96
62f6b57948 Added neutral dot option 2021-06-17 23:12:30 +01:00
docofmur
5387acf533 exclusion zone update 1 test change on caucases, massive update on syria 2021-06-17 14:51:48 -07:00
C. Perreau
077b3ef04d Merge pull request #1243 from DanAlbert/f-16-strike-gbu-31
Update F-16 strike loadout to use GBU-31.
2021-06-17 22:39:58 +02:00
Dan Albert
9c654254d3 Update F-16 strike loadout to use GBU-31. 2021-06-16 21:43:09 -07:00
Mustang-25
4bb8bbbad8 Add remaining HAF Viper squadrons. 2021-06-16 21:05:50 -07:00
Dan Albert
39adafb1be Fix Greek F-16 livery ID. 2021-06-16 20:52:15 -07:00
Dan Albert
e19bfcdd04 Add a Greek F-16 squadron. 2021-06-16 20:37:07 -07:00
Dan Albert
6fde92f5ac Add the 191. Filo Turkish F-16 squadron. 2021-06-16 20:13:07 -07:00
Dan Albert
7170a7b302 Fix spawning unused aircraft.
These are assigned a squadron even though they're unused as a hack. We
need to tolerate these aircraft having no pilot assigned since that's
the desired case for unused aircraft (though only happens when the
squadron runs out of pilots, which should be fixed).
2021-06-16 20:11:36 -07:00
Dan Albert
24884e4a77 Import latest beacon data from DCS. 2021-06-16 19:46:19 -07:00
Dan Albert
384be8ceae Update Turkish faction.
Add a bunch of missing helicopters, some ground units, and remove the
KC-130 which they don't seem to use.
2021-06-16 19:10:23 -07:00
Dan Albert
ee9a5e8482 Update the Greek faction.
Greece has C-130s and Patriots. They don't have the E-3 but they do have
AEW&C via the ER-99, which isn't in DCS so just use an E-3 to pretend.
Also remove the Hawk radar as an EWR since we have the P-19 which is
better.
2021-06-16 18:54:12 -07:00
Dan Albert
34453fa3be Fix incorrect conditional. 2021-06-16 17:23:57 -07:00
Dan Albert
f727712bfa Make non-interactive map elements unobstructive.
This makes most of the lines and polygons on the map non-interactive so
they don't capture mouse events, and also makes the culling exclusion
zones unfilled so they don't obscure real map objects in dense areas.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1217
2021-06-16 17:22:07 -07:00
Dan Albert
3bb974b9e0 Note procurement fix in the changelog. 2021-06-16 17:10:21 -07:00
Dan Albert
021445216e Escape the JTAC zone name in the plugin data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1218
2021-06-16 17:06:45 -07:00
Schneefl0cke
c13bf3ccd1 Fix procurement for factions that lack some units.
Fixes procurement for factions with no aircraft, no ground units, or no
tanks.
2021-06-16 09:34:58 -07:00
Mustang-25
8d53f42421 Update KC135MPRS.yaml
Fixed minor spelling error
2021-06-13 22:56:35 -07:00
Dan Albert
ace42019fb Cap squadron size, limit replenishment rate.
This caps squadrons to 12 pilots and limits their replenishment rate to
1 pilot per turn. Should probably make those values configurable, but
they aren't currently.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1136
2021-06-13 14:40:15 -07:00
Dan Albert
54aa161da0 Fix new game wizard faction template.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1227
2021-06-12 21:48:22 -07:00
Dan Albert
25c289deaa Make squadron nicknames optional. 2021-06-12 21:43:26 -07:00
Dan Albert
3c802e7d55 Fix unit info window. 2021-06-12 21:35:01 -07:00
Dan Albert
ba3cf4d2bd Put back all the radio data. 2021-06-12 20:56:24 -07:00
Dan Albert
4aa905716b Remove old file.
This was from testing and shouldn't have been kept.
2021-06-12 20:28:14 -07:00
Dan Albert
0fc1e8ec10 Remove unused file, begin fixing radios.
My radio data converter broke at some point while testing, so adding
this all back manually.
2021-06-12 20:27:18 -07:00
Dan Albert
0875d35129 Fix some squadrons.
I accidentally reverted my changes here while testing something earlier.
2021-06-12 20:24:04 -07:00
Dan Albert
8c62a081fe Remove unused code from AircraftType conversion. 2021-06-12 20:13:45 -07:00
Dan Albert
f811ae6c61 Convert factions and unit data. 2021-06-12 20:13:45 -07:00
Dan Albert
4a3ef42e67 Wrap the pydcs FlyingType in our own AircraftType.
This is an attempt to remove a lot of our supposedly unnecessary error
handling. Every aircraft should have a price, a description, a name,
etc; and none of those should require carrying around the faction's
country as context.

This moves all the data for aircraft into yaml files (only one converted
here as an example). Most of the "extended unit info" isn't actually
being read yet.

To replace the renaming of units based on the county, we instead
generate multiple types of each unit when necessary. The CF-18 is just
as much a first-class type as the F/A-18 is.

This doesn't work in its current state because it does break all the
existing names for aircraft that are used in the faction and squadron
files, and we no longer let those errors go as a warning. It will be an
annoying one time switch, but it allows us to define the names that get
used in these files instead of being sensitive to changes as they happen
in pydcs, and allows faction designers to specifically choose, for
example, the Su-22 instead of the Su-17.

One thing not handled by this is aircraft task capability. This is
because the lists in ai_flight_planner_db.py are a priority list, and to
move it out to a yaml file we'd need to assign a weight to it that would
be used to stack rank each aircraft. That's doable, but it makes it much
more difficult to see the ordering of aircraft at a glance, and much
more annoying to move aircraft around in the priority list. I don't
think this is worth doing, and the priority lists will remain in their
own separate lists.

This includes the converted I used to convert all the old unit info and
factions to the new format. This doesn't need to live long, but we may
want to reuse it in the future so we want it in the version history.
2021-06-12 20:13:45 -07:00
Dan Albert
88abaef7f9 Fix inconsistencies in prices and unit data. 2021-06-12 20:13:45 -07:00
Schneefl0cke
21fe746f2f Use ID for unit info lookups instead of name. 2021-06-12 02:24:21 -07:00
Dan Albert
c3c6915fa0 Include the micro version in the version string. 2021-06-11 16:52:23 -07:00
Dan Albert
b2705c1a13 Update Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1206
2021-06-11 16:42:12 -07:00
Mustang-25
75e3b4cc84 Add the First Lebanon War Historical Campaign. 2021-06-10 17:38:40 -07:00
Khopa
78f5235eca Removed helipad from golan heights campaign to avoid capture trigger error 2021-06-10 23:27:45 +02:00
Florian
78cd17e279 added missing units 2021-06-10 11:18:52 -07:00
Dan Albert
c51c8aae5c Clarify the docs/name of the task type list. 2021-06-09 21:53:45 -07:00
Dan Albert
40aa7734e1 Fix CAS commit range display.
CAS commits around the target, not its flight plan.
2021-06-09 21:51:26 -07:00
Dan Albert
0594e1148e Update Operation Mole Cricket.
https://github.com/dcs-liberation/dcs_liberation/issues/1203
2021-06-09 21:26:47 -07:00
Dan Albert
9eacd1563f Add Northern Russia campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1202
2021-06-09 21:25:59 -07:00
SnappyComebacks
a53a648a63 Add plannable tankers.
This Pull Request lets users plan Tanker flights.

Features:

- Introduction of `Refueling` flight type.
- Tankers can be purchased at airbases and carriers.
- Tankers get planned by AI.
- Tankers are planned from airbases and at aircraft carriers.
- Tankers aim to be at high, fast, and 70 miles from the nearest threat.
  (A10s won't be able to tank)
- Tankers racetrack orbit for one hour.
- Optional Tickbox to enable legacy tankers.
- S-3B Tanker added to factions.
- KC-130 MPRS added to factions.
- Kneeboard shows planned tankers, their tacans, and radios.

Limitations:

- AI doesn't know whether to plan probe and drogue or boom refueling
  tankers.
- User can't choose tanker speed.  Heavily loaded aircraft may have
  trouble.
- User can't choose tanker altitude.  A-10s will not make it to high
  altitude.

Problems:

- Tanker callsigns do not increment, see attached image.  (Investigated:
  Need to use `FlyingType.callsign_dict`, instead of just
  `FlyingType.callsign`.  This seems like it might be significant work
  to do.).
- Having a flight of two or more tankers only spawns one tanker.
- Let me know if you have a solution, or feel free to commit one.

https://user-images.githubusercontent.com/74509817/120909602-d7bc3680-c633-11eb-80d7-eccd4e095770.png
2021-06-09 21:14:10 -07:00
Dan Albert
a9dacf4a29 Fix engagement distance display. 2021-06-09 21:01:14 -07:00
Dan Albert
66f82b6ff9 Update mission start guidance. 2021-06-09 19:20:56 -07:00
Dan Albert
0e68884493 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
2021-06-09 19:08:36 -07:00
Dan Albert
f8d885fc9a Fix broken import. 2021-06-09 19:06:02 -07:00
Florian
366190ee99 added missing units to price table 2021-06-09 19:03:25 -07:00
Dan Albert
42d56a324f Make campaign names consistent. 2021-06-09 18:58:35 -07:00
docofmur
7d1f1ea2f7 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
2021-06-09 18:55:22 -07:00
Dan Albert
30cab8e3a7 Add Around the Mountain campaign.
https://github.com/dcs-liberation/dcs_liberation/issues/1199
2021-06-09 18:51:34 -07:00
Dan Albert
e0e2162c6d Add Operation Allied Sword campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1196
2021-06-09 18:44:09 -07:00
Dan Albert
f1582fcc10 Add Humble Helper campaign and factions.
https://github.com/dcs-liberation/dcs_liberation/issues/1197
2021-06-09 18:39:26 -07:00
Florian
eb6206ea57 added texts for all units 2021-06-09 12:42:56 -07:00
Brock Greman
3ad51cafa8 Fixing display of "sunny" during clear conditions at night. 2021-06-09 02:16:05 -07:00
Dan Albert
b8c14d69c3 Make enable_and_reset not half lie.
https://github.com/dcs-liberation/dcs_liberation/issues/1185
2021-06-08 21:19:48 -07:00
Dan Albert
725b5083c7 Fix typo in Incirlik runway data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1188
2021-06-08 21:14:36 -07:00
Dan Albert
87dd6b19bf Fix repeated JTACs after multiple generations.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1191
2021-06-08 20:56:00 -07:00
bgreman
3188994261 Gripen mod support.
(cherry picked from commit 0220fa4ff6)
2021-06-08 20:49:24 -07:00
Schneefl0cke
e4c9d8799e Add Recon combat role. 2021-06-08 19:48:56 -07:00
dependabot[bot]
bc938db7f9 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>
2021-06-08 16:15:25 -07:00
Dan Albert
0a9dc49e7f Remove UNIT_BY_TASK. 2021-06-07 19:13:49 -07:00
Dan Albert
07cdfc16d0 Move support tanker off find_unittype. 2021-06-07 19:12:30 -07:00
Dan Albert
622a171ac4 Move armor TGO purchase off find_unittype. 2021-06-07 19:01:16 -07:00
Dan Albert
fd85efbf55 Remove more dead code in game.db. 2021-06-07 18:59:02 -07:00
Dan Albert
ae2a818d8c Move the base intel menu off find_unittype. 2021-06-07 18:54:51 -07:00
Dan Albert
6966c16dd2 Remove dead code. 2021-06-07 18:54:39 -07:00
Dan Albert
27b5f24a0f Move unit purchase off find_unittype. 2021-06-07 18:54:21 -07:00
Dan Albert
ea15421308 Migrate support AEW&C away from find_unittype. 2021-06-07 18:52:54 -07:00
Dan Albert
ef35ad90b8 Remove one user of UNIT_BY_TASK. 2021-06-07 18:01:31 -07:00
Dan Albert
914691eaa7 Remove more unused code from Base. 2021-06-07 17:51:25 -07:00
Dan Albert
37bb83dfa6 Delete a bunch of unused code from Base. 2021-06-07 17:48:03 -07:00
Dan Albert
d8881e2734 Fix hangar status display. 2021-06-06 17:12:44 -07:00
Dan Albert
45869c428e Label the player checkbox in the roster editor. 2021-06-06 13:43:34 -07:00
Dan Albert
40832bd3a1 Update screenshot on the front page. 2021-06-06 13:37:01 -07:00
Khopa
126a8e8efb Added a small WW2 campaign on Normandy map (Replacing the former Normandy Small campaign). 2021-06-06 18:50:16 +02:00
Dan Albert
1796c21f48 Update the Syria full campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1182
2021-06-05 21:39:45 -07:00
Dan Albert
363d4af639 Add Jordan 2010 faction. 2021-06-05 19:08:52 -07:00
Dan Albert
f1c881378c Add/updates campaigns from Starfire.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1181
2021-06-05 19:07:13 -07:00
Dan Albert
d316e13fa6 Suppress events fired while rebuilding model. 2021-06-05 15:21:23 -07:00
Dan Albert
1ea98a6ed1 Hide incompatible campaigns by default.
https://github.com/dcs-liberation/dcs_liberation/issues/1178
2021-06-05 15:15:30 -07:00
Dan Albert
3d4415d5d2 Move develop to 4.0. 2021-06-05 14:35:07 -07:00
Dan Albert
3e43414d9c Make the new package dialog modal.
In the *new* package dialog, a package has been created and may have
aircraft assigned to it, but it is not a part of the ATO until the user
saves it.

Other actions (modifying settings, closing some other dialogs like the
base menu) can cause a Game update which will forcibly close this window
without either accepting or rejecting it, so we neither save the package
nor release any allocated units.

While it would be preferable to be able to update this dialog as needed
in the event of game updates, the quick fix is to just not allow
interaction with other UI elements until the new package has either been
finalized or canceled.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1027
2021-06-05 14:21:31 -07:00
Dan Albert
6d682d509f Restore former turn 0 budget split. 2021-06-05 14:09:10 -07:00
Florian
3a592aee8b Split purchase budget based on investment ratio.
The AI purchaser will aim to have a 50/50 ground/air investment mix.
This allows it to overspend on one category if significant losses were
taken the previous turn.

The total purchase amount is still limited, so if the bases are full
when only 10% of the investment is in ground units, the full budget for
the turn will still go to air.
2021-06-05 14:07:18 -07:00
Dan Albert
b74f60fe0e Stagger packages in units of seconds, not minutes.
Missions with very large numbers of packages and short mission windows
would raise an exception here because we couldn't schedule more
frequently than once a minute. Switch to using seconds instead of
minutes to avoid that problem. If there are more packages than there are
seconds in the mission the game is broken for other reasons.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1154
2021-06-05 13:50:28 -07:00
Dan Albert
34f3a50234 Fix UI quirks when reopening custom loadout.
* Disable the selector.
* Select the default loadout rather than the first one so unchecking the
  custom box goes back to the default loadout.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1152
2021-06-05 13:40:11 -07:00
Dan Albert
6094179a40 Show pending ground unit count in the base menu.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1161
2021-06-05 13:24:12 -07:00
Dan Albert
e3bc2688ba Revert accidental change to IR campaign.
Used this for debugging airlifts and accidentally included it.
2021-06-05 12:46:19 -07:00
Dan Albert
96cdea2a94 Load two units per cargo plane.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1029
2021-06-05 12:21:34 -07:00
Dan Albert
cb159e3341 Fix canceling transfers.
singledispatchmethod only overloads on the first argument.
2021-06-05 12:00:43 -07:00
Dan Albert
136e776b03 Add map markers for each building in the group.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1126
2021-06-04 18:07:12 -07:00
Dan Albert
a0833e8943 Allow selection of auto-assigned mission types.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1176
2021-06-04 17:50:21 -07:00
Dan Albert
8bb1b1da7c Fix squadron roster size.
This was excluding a number of pilots from the end of the roster equal
to the number of losses the squadron had suffered.
2021-06-04 17:01:29 -07:00
Dan Albert
558502d8ea Convert flight creator to pilot roster.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1143
2021-06-04 17:01:29 -07:00
Khopa
8edb952800 Updated campaign "Golan Heights" to version 6.0 2021-06-05 01:24:08 +02:00
Mustang-25
f3d79e58db Add the Battle for the UAE Campaign.
The Battle for the UAE is a follow-on campaign to the Battle for Abu Dhabi.
2021-06-03 23:30:02 -07:00
Dan Albert
f26ff085e1 Add a new bug template for campaign update "PRs". 2021-06-03 19:08:00 -07:00
Brock Greman
7ea550738e Sorts flights in the base menu by mission start time. Also makes base menu dialog bigger. 2021-06-03 15:32:49 -07:00
Brock Greman
6b1048590f Fixes issue where only first a/c in a flight would show in air inventory 2021-06-03 14:46:12 -07:00
Schneefl0cke
203f0d3851 Introduce recon unit type, adjust ratios. 2021-06-02 23:00:35 -07:00
Dan Albert
d9c38a716c Move settings and stats to the toolbar.
https://github.com/dcs-liberation/dcs_liberation/issues/1146
2021-06-01 22:59:15 -07:00
Mustang-25
24709d01bd Add the Operation Mole Cricket 2010 Campaign. 2021-06-01 22:13:23 -07:00
Dan Albert
2dc2681f84 Attach bug URL to a TODO. 2021-06-01 20:16:33 -07:00
Dan Albert
d53a39860e Update Black Sea to latest campaign format. 2021-06-01 18:49:33 -07:00
SnappyComebacks
ad2f084112 Added ammunition depot changes to 3.0 changelog. 2021-05-31 22:20:38 -07:00
Dan Albert
d59c42ed3f Fix generation crash for large campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1162
2021-05-31 21:02:46 -07:00
Dan Albert
e022ffee62 Auto-ASAP player missions. 2021-05-31 20:05:04 -07:00
Dan Albert
77ddd5ed78 Don't include off-map spawns for AEW&C planning. 2021-05-31 20:00:10 -07:00
Dan Albert
8604faffe6 Make EWR sites purchasable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/968
2021-05-31 16:33:55 -07:00
Dan Albert
45919200c4 Depopulate captured objectives.
Buildings are left to be captured, but the retreating coalition now
destroys their abandoned equipment.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1158
2021-05-31 15:43:20 -07:00
Dan Albert
d498bb9cff Give AEW&C a significant buffer from threats. 2021-05-31 15:33:21 -07:00
Dan Albert
389f60786a Fix moving carriers. 2021-05-31 15:13:56 -07:00
Dan Albert
2d0929cd69 Plan AEW&C in safer locations.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1119
2021-05-31 14:57:25 -07:00
Dan Albert
e94ebd6ed2 Invert AEW&C default behavior.
This is working well. Make it the default, but don't remove it since we
don't have HAVCAP yet.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1118
2021-05-31 14:42:22 -07:00
Dan Albert
77373606fe Fix crash in startup. 2021-05-31 14:36:25 -07:00
Dan Albert
284f2bc323 Show runway status on the new map.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1105
2021-05-31 14:18:27 -07:00
Dan Albert
355e6e1d15 Fix several cases of wrongly using broken runways.
The usual symptom here was the game breaking when a carrier is
destroyed. The carrier would no longer be operational but missions would
be assigned there that could not generate flight plans.
2021-05-31 14:13:33 -07:00
Dan Albert
f6909d2f98 Use airfield icons for off-map spawns.
Nothing else is really appropriate, and these are meant to represent
off-map airfields.
2021-05-31 13:23:47 -07:00
Dan Albert
c42974f7b3 Update FOB icon.
Also removes the possibility of generating FOBs that are not FOBs.
2021-05-31 13:14:45 -07:00
Khopa
230d80a2a5 Updated campaign "Russia Small" campaign to version 6.0 2021-05-31 19:52:06 +02:00
Dan Albert
551038b295 Fix TGO purchase UI.
Enable buying and selling of armor groups. Don't allow armor groups to
be replaced with air defenses or vice versa. These are a different TGO
type and this has always been a thing that will break the flight
planner.
2021-05-30 21:17:55 -07:00
Dan Albert
4055b06e71 Clean up and rename ControlPoint.for_airbase. 2021-05-30 21:17:12 -07:00
Dan Albert
6616359baf Remove "base defenses" UI features.
There's no such thing any more. There are just objectives that are
closer to the CP than the others.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1130
2021-05-30 21:07:48 -07:00
Dan Albert
a5336bbe56 Note dropping random objectives in the changelog. 2021-05-30 20:51:18 -07:00
Dan Albert
871e7f7a50 Remove random objective generation.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
2021-05-30 20:47:20 -07:00
Dan Albert
d1c7146a47 Add cheat options back to front lines.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1108.
2021-05-30 20:16:54 -07:00
Dan Albert
30f6220c3e Remove the old map. 2021-05-30 19:57:14 -07:00
Dan Albert
acd3e87996 Remove accidentally preserved debug log in UI. 2021-05-30 19:46:38 -07:00
Dan Albert
8c8814d07e Add culling display option to the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-30 19:32:08 -07:00
Dan Albert
417fc3af5b Allow objects near missile launchers to be culled.
We want the scud to not be culled, but we should still cull things
nearby. Rather than making the scud the center of a 2.5km unculled zone,
just exclude missile objectives from culling.
2021-05-30 19:29:59 -07:00
Dan Albert
2218733da4 Add exclusion zone display to the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-30 18:50:53 -07:00
Brock Greman
9d1060975e Fixes the ugly border on the Air Inventory View 2021-05-30 16:47:18 -07:00
Dan Albert
82281e2477 Fix inventory handling when adjusting flight size.
We need to resize the flight before attempting to claim the aircraft, or
we'll reclaim the original number of aircraft rather than the new size.
2021-05-30 15:46:54 -07:00
Dan Albert
d0976c45e9 Add pilot assignements to the inventory table. 2021-05-30 14:28:58 -07:00
bgreman
a888397bef Add a global air inventory view to air wing dialog.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/428.
2021-05-30 13:40:51 -07:00
Brock Greman
7b2bb4a128 Show ground unit supply info in the base menu.
Update the base UI to have a hint about ground unit deployment limits
and a matching tooltip for how it is calculated.
2021-05-30 13:15:06 -07:00
SnappyComebacks
d440dc00f1 Purchase reserves at front lines.
This changes the ground unit purchase behavior such that the supply
limit is exceeded by 30%, with the extra units kept in reserve.

The old hard cap of 50 units is no longer needed, since the ammo supply
now does the same task, so that's been removed.
2021-05-30 13:04:18 -07:00
Florian
d61382f4e2 Maintain composition when buying ground units.
Unit composition is defined by the doctrine. The most understaffed CP
will now get the most underrepresented unit type. Previously a random
understaffed CP would get a random unit type.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1057.
2021-05-30 12:52:35 -07:00
Khopa
d4fe893539 Updated "Russia Small" campaign to 5.0 (ammo depots) 2021-05-30 17:58:56 +02:00
Khopa
1af95955b6 Base menu UI : Added ammo depots & factory information (WIP UX) 2021-05-30 17:49:15 +02:00
Khopa
a43e926dd2 Updated campaign "Golan Height" for v5.0 (Ammo depots) 2021-05-30 16:59:02 +02:00
Khopa
ff49046bfa Updated locales for a few factions 2021-05-30 16:57:51 +02:00
SnappyComebacks
95b0b851a5 Limit front line size with ammo depots.
This limit is determined by the number of buildings that belong to Ammo
Depots at the front line's connected Control Point. The limit increases
for every surviving building at ammo depot objectives.

There is a lower limit to the number of units that will spawn, so that
if there are no surviving ammo depot buildings at a control point, there
will still be some ground conflict.
2021-05-29 13:25:23 -07:00
Dan Albert
077ca19912 Add ammo depots to Abu Dhabi and Inherent Resolve.
IR gets one per base to maintain the old behavior. Abu Dhabi has a bit
more variety, with major bases like Al Dhafra and Bandar Abbas getting
two and FOBs getting none.
2021-05-29 12:36:05 -07:00
Brock Greman
089cc23648 Fixing duplicate connected CPs between Beslan and Modok and Beslan and Nalchik. 2021-05-29 03:05:22 -07:00
Dan Albert
e6b9a73d03 Improve AI air defense target prioritization.
Target the air defenses whose *threat ranges* come closest to friendly
bases rather than the closest sites themselves. In other words, the
SA-10 that is 5 miles behind the SA-6 will now be the priority.

This also treats EWRs a bit differently. If they are not protected by a
SAM their detection range will be used for determining their "threat"
range. Otherwise a heuristic is used to determine whether or not they
can be safely attacked without encroaching on the covering SAM.
2021-05-28 19:27:02 -07:00
Dan Albert
cea264e871 Remove special case behavior for FOB missions.
The only difference from the main CP types was that it didn't support
AEW&C (which shouldn't have been on the main ControlPoint class anyway)
and add strike.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1132
2021-05-28 18:02:57 -07:00
Dan Albert
d0bde7b016 Check for interesection when localizing.
Apparently it is possible to place an objective exactly on the boundary
of a navpoly.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1140
2021-05-28 17:26:46 -07:00
Dan Albert
5b271df66f Propagate planning error for package waypoints.
We did this for the flight plan itself, but did not if the package
waypoints failed to generate.

https://github.com/dcs-liberation/dcs_liberation/issues/1140
2021-05-28 17:26:31 -07:00
Dan Albert
bc7faee880 Add navmesh map mode to the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-28 17:00:33 -07:00
Dan Albert
a2abdcf5d3 Ensure that a transit path exists for recruitment.
Networks can be disconnected even by airlift because FOBs are not
airports.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1147
2021-05-28 16:28:50 -07:00
Dan Albert
d4e843983d Add more tracing for turn processing.
Most of the time (1-2 seconds) is going to flight plan layout. There
don't seem to be any easy opportunities for improvement.
2021-05-28 15:31:33 -07:00
Dan Albert
6e41c36a44 Fix replacing unassigned pilot slots. 2021-05-27 22:51:11 -07:00
Dan Albert
1fe3451120 Set locales for some factions.
Far from complete.
2021-05-27 21:37:04 -07:00
Dan Albert
bc4a95d0a5 Un-WIP squadrons in the changelog.
This is feature complete.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 21:05:01 -07:00
Dan Albert
14dc6d1604 Add squadron selector to flight creator.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1142
2021-05-27 21:00:32 -07:00
Dan Albert
1795ed7617 Limit squadron tasks to those of the aircraft.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 20:27:45 -07:00
Dan Albert
e8edb31be3 Revert "Don't assign pilots to unused aircraft."
The unitmap needs to account for this.

This reverts commit dae3835eb0.
2021-05-27 20:02:39 -07:00
Dan Albert
58fd30e6ad Add the 15th Airlift Squadron. 2021-05-27 19:58:45 -07:00
Dan Albert
9a34ada258 Clear last turn's procurement requests each turn.
Without this, *this turn's* urgent requests are lower priority than last
turn's stretch goals. The requests are remade every turn so we lose
nothing by removing this.

Bug was introduced by f69450e2ae, so this
doesn't affect 2.5.
2021-05-27 19:58:45 -07:00
Dan Albert
748a752e29 Fix pydcs loadout cache for command line launcher. 2021-05-27 19:58:45 -07:00
Dan Albert
37748ef3bd Obey squadron mission types when planning airlift.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
d41007de8e Add VFA-192. 2021-05-27 19:58:45 -07:00
Dan Albert
45befd440c Consider squadron for task capability checking.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
a2c10f1c7a Check for compatible squadrons when buying planes.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
d7768f86d3 Obey squadron mission types in auto-planning.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 19:58:45 -07:00
Dan Albert
dae3835eb0 Don't assign pilots to unused aircraft.
These "flights" are only created so that we can spawn the aircraft on
the ramp for OCA strikes. They shouldn't have pilots assigned.
2021-05-27 19:58:45 -07:00
Dan Albert
e9b5784d30 Update player slot advice for update UI.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 17:35:54 -07:00
Dan Albert
1521f0a9b1 Add on-leave toggle for pilots.
Pilots on leave will not be assignable to any flights (but will not be
unassigned from any already scheduled this turn).

https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 17:09:09 -07:00
Dan Albert
9a9c351f47 Copy the list of random names as was intended.
Every name generated depopulated the global list which made reset do
nothing. Large numbers of TGOs (or generation of many campaigns) would
drain the list fully and new squadrons would no longer have a name list
to pull from.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1144
2021-05-27 16:32:21 -07:00
Dan Albert
4ec11ddea5 Auto-ASAP player packages based on preferences.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 16:04:51 -07:00
Dan Albert
f619b6b9fc Choose player pilots based on player preference.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 16:00:27 -07:00
Dan Albert
bcccb3206d Connect auto-ATO disable option.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/315
2021-05-27 15:43:47 -07:00
Dan Albert
11a8ff7f70 Add settings UI for auto-ato behavior.
https://github.com/dcs-liberation/dcs_liberation/issues/276
https://github.com/dcs-liberation/dcs_liberation/issues/315
2021-05-27 15:41:08 -07:00
Dan Albert
f6ab1aad77 Clean up signal handling in pilot roster editor. 2021-05-27 15:06:36 -07:00
Dan Albert
5a732acf64 Fix creating new fights to create player pilots.
This is just a hack that automatically converts the assigned pilots to
players if needed. This really needs to be replaced with a roster editor
like the flight edit screen has, but that also requires squadron
selection, which isn't a thing we're ready for yet.

https://github.com/dcs-liberation/dcs_liberation/issues/1139
2021-05-27 14:15:27 -07:00
Dan Albert
e4e06e0a6e Add player toggle to flight settings.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 02:18:57 -07:00
Dan Albert
28f20d47d3 Add player pilot invulnerability option.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 01:40:23 -07:00
Dan Albert
82ce688a0d Allow players to be defined in the squadron file.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 01:15:52 -07:00
Dan Albert
f36757b650 Add livery selection for predefined squadrons.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 00:56:05 -07:00
Dan Albert
ac4a7441e9 Add predefined squadron support.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-27 00:23:40 -07:00
Dan Albert
9091afe682 Remove very dead campaign.
This was never updated to the miz format. Patches welcome if someone
wants to bring it up to date.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1129
2021-05-26 23:05:57 -07:00
Dan Albert
e2034b19e7 Update missing clients advice.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 20:41:22 -07:00
Dan Albert
1b7a225f9d Replace client count with player pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 20:33:23 -07:00
Dan Albert
a52043ef29 Allow toggling player/AI pilot state.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 20:33:15 -07:00
Dan Albert
b38d271f10 Show player/AI status for pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 19:47:53 -07:00
Dan Albert
e480519855 Increase skill level for experienced pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 19:34:24 -07:00
Dan Albert
8b8d1e87e7 Track missions flown for each pilot.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 19:04:06 -07:00
Dan Albert
cd6de191d1 Track pilot deaths.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 18:54:07 -07:00
Dan Albert
8b8e018521 Fix cases where pilots were not returned.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 18:49:19 -07:00
Dan Albert
5277beede3 Show active and available pilots in air wing view.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 18:18:22 -07:00
Dan Albert
57a2457050 Show aircraft type in the squadron list.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
2f8656d54f Generate random squadron nicknames.
A little weird that the animal names aren't plural, but good enough.

https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
49102e510d Disallow creating missions with missing pilots.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
e7b8548698 Clean up more delegates.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
9c2bad85d5 Show number of missing pilots in the UI.
https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
4147d2f684 Initial squadrons implementation.
Doesn't actually do anything yet, but squadrons are created for each
aircraft type and pilots will be created as needed to fill flights.

https://github.com/dcs-liberation/dcs_liberation/issues/276
2021-05-26 17:49:53 -07:00
Dan Albert
6b30f47588 Fix airlifts always using player country. 2021-05-26 16:48:36 -07:00
Dan Albert
e49da6afd6 Inject the saved games path from preferences.
pydcs can't guess the saved games path accurately for all users, so
inject the path that they've told us is correct to work around that.

It seems pydcs has two problems:

1. Only `DCS` is checked, not `DCS.openbeta`.
2. Only `%USERPROFILE%/Saved Games` is used, so if the user has moved
   their Saved Games directory (but not their whole user profile) pydcs
   cannot find the location.

https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid would
be the starting place for fixing problem 2 properly. 1 is just a matter
of trying both.
2021-05-26 16:02:54 -07:00
Dan Albert
6fa0a29249 Update to Python 3.9.
This is what I've been using locally for ages and it seems to work fine.
2021-05-26 13:15:27 -07:00
bgreman
c163e2c981 Inject mod plane weapons into pydcs.
Adds a simple injector that iterates over attrs of an input class and
injects things that look like custom weapons into pydcs's weapons
classes.

Also updated all current mod aircraft configs to perform the injection.
2021-05-26 12:49:49 -07:00
Brock Greman
372bf9d97f Fixing F-22 loadouts 2021-05-26 01:46:29 -07:00
Dan Albert
619d5dd1b9 Teach sweep to care about multi-role too.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1124
2021-05-25 22:58:09 -07:00
Dan Albert
4939faf5fa Schedule SEAD with a one minute lead time. 2021-05-25 22:53:57 -07:00
bgreman
205e4aa707 Add ability to toggle between own/enemy intel. 2021-05-25 22:23:33 -07:00
Dan Albert
81ce7fbb62 Fix handling of empty polys in the new UI.
This was copied from the Qt map and tweaked, but the use cases are
slightly different so this needs to return an empty list for an empty
polygon instead of None.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1121
2021-05-25 19:19:45 -07:00
Dan Albert
de9651533f Load map.js explicitly from canvas.html.
Without this it's hard to get to map.js in the debug tools since Chrome
doesn't know about the anonymous js. Probably improves logging too.
2021-05-25 18:46:41 -07:00
Dan Albert
e6e31fd234 Actually ASAP the ASAP packages.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1117
2021-05-25 14:23:00 -07:00
Brock Greman
d242079a74 Making Sweep flight types have appropriate aircraft 2021-05-25 13:20:47 -07:00
Dan Albert
48f26cb181 Fix Bandar Abbas airfield data.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1114
2021-05-25 00:14:20 -07:00
bgreman
c37a5b2405 Prevent empty transfers.
Disables the 'create transfer order' button in the unit transfer dialog if no units are actually selected for transfer (including when the dialog is first loaded).
2021-05-24 20:43:33 -07:00
Dan Albert
d15bfaac76 Fix group targeting for multi-group TGOs.
AI flights were only getting a single Attack Group task against multi-
group TGOs (currently only a small group of SAMs like the SA-10, I
believe), so the AI would never attack the point defense SA-15 or AAA
groups.
2021-05-24 19:26:03 -07:00
Dan Albert
e94657875f Reselect same row after deleting waypoint. 2021-05-24 18:20:19 -07:00
Dan Albert
f2bd7300aa Improve precision of kneeboard lat/lon.
DMS with decimal seconds is what the hornet uses for PP targest. In the
future we'll want to make this aircraft specific (and potentially user
preference for jets like the A-10 that can handle both L/L and MGRS).
2021-05-24 18:06:47 -07:00
Dan Albert
c255aee3b9 Make default AEW&C missions ASAP. 2021-05-24 17:21:33 -07:00
Dan Albert
305d1f0523 Reset non-custom loadout when changing task. 2021-05-24 16:57:36 -07:00
Dan Albert
970f2c25dd Fix loadout reset when disabling custom loadouts. 2021-05-24 16:49:31 -07:00
Dan Albert
b7b3b35816 Make some waypoint types undraggable.
None of these (takeoff, landing, divert, bullseye, precise target
locations) can be usefully moved, so prevent it.
2021-05-24 16:45:21 -07:00
Dan Albert
e8f326ebce Update Skynet to 2.1.0. 2021-05-24 14:35:22 -07:00
Dan Albert
62b743025a Fix supply route clobbering, make immutable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1083
2021-05-23 17:30:50 -07:00
Dan Albert
7934463a53 Add base capture cheat to the new UI.
We don't have a context menu yet, so just add it to the base menu.

https://github.com/dcs-liberation/dcs_liberation/issues/1108
2021-05-23 17:18:01 -07:00
Dan Albert
d15ef63182 Remove unused method of ControlPoint. 2021-05-23 17:17:59 -07:00
Dan Albert
c7edba5120 Add TGO-specific layers.
This also splits the main and debug controls because the main list was
getting too long.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1069
2021-05-23 15:36:22 -07:00
Dan Albert
188f871bc8 Remove errant whitespace. 2021-05-23 15:01:10 -07:00
Dan Albert
31eba975fd Note flight planner changes.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1106
2021-05-23 15:00:27 -07:00
Dan Albert
2ea0bccd25 Hide dead opfor objectives.
These are just clutter.
2021-05-23 14:44:24 -07:00
Dan Albert
fa321c7ddc Don't plan SEAD when only a search radar remains. 2021-05-23 13:15:42 -07:00
Dan Albert
1d7b0c9b17 Tweak F/A-18 BARCAP and CAS loadouts.
Add bags to each. CAS only gets one but that should be plenty for only
four Mavericks. CAP gets two.
2021-05-23 13:07:16 -07:00
Dan Albert
a4fbcd2d02 Update pydcs for loadout loading fix. 2021-05-23 13:07:16 -07:00
Dan Albert
d788b286aa Remove unused UI classes. 2021-05-23 13:03:18 -07:00
Dan Albert
eedb5c26a9 Ignore non-escorted regions when planning escorts.
We shouldn't consider the non-escorted parts of the flight path when
checking for threats to determine if escorts should be used or not,
since escorts can't help in those areas anyway. This was causing escorts
to be overly requested since the bullseye is now a part of the
"flight plan", but could have also triggered for divert waypoints, or
for aircraft taking off in a retreat from a threatened location.
2021-05-23 13:01:44 -07:00
Dan Albert
ddd6e7d18f Improve detection of functional radar SAMs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1109
2021-05-23 13:01:44 -07:00
Dan Albert
eae0d6be94 Add threat zone drawing for the new map.
https://github.com/dcs-liberation/dcs_liberation/issues/1097
2021-05-23 12:25:15 -07:00
Dan Albert
5e68dbe1ca Correct the list of units with radars.
Probably.

https://github.com/dcs-liberation/dcs_liberation/issues/1109
2021-05-23 01:30:23 -07:00
Dan Albert
98e0be6be9 Revert "Correct radar detection."
We need this after all, but we do need to audit the list. Will follow
up with that fix.

https://github.com/dcs-liberation/dcs_liberation/issues/1109

This reverts commit f68935735d.
2021-05-23 01:10:29 -07:00
Dan Albert
7450a6b7eb Configure more loadout fallbacks. 2021-05-22 20:12:56 -07:00
Dan Albert
c3802e5a37 Make UI created packages ASAP by default. 2021-05-22 19:35:56 -07:00
Dan Albert
43cd9bce67 Fix cargo ship locations in Abu Dhabi.
Apparently these ships have a > 32ft draft and these ports are shallow.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1104
2021-05-22 19:17:53 -07:00
Dan Albert
2f6ab6d2b0 Update Hornet loadouts, add DEAD loadout. 2021-05-22 17:11:10 -07:00
Dan Albert
2df17c32cd Clean up aircraft selector. 2021-05-22 17:11:10 -07:00
Dan Albert
16fff8d87a Fall back to SEAD loadout for SEAD escort.
These usually do not need to differ.
2021-05-22 17:11:10 -07:00
Dan Albert
1087069277 Fix F-14 SEAD/DEAD loadouts.
The AI doesn't use mixed loadouts effectively (only the TALDs get used
and the flight returns home with a full bomb load):
https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/

This makes the SEAD loadout TALDs only and the DEAD loadout bombs only.
2021-05-22 16:53:48 -07:00
Dan Albert
1b624e7e6f Remove center tank for F-16 loadouts. 2021-05-22 16:53:48 -07:00
Dan Albert
7223ae327a Add DEAD loadout for the F-16. 2021-05-22 16:45:06 -07:00
Dan Albert
bcdefda0db Make the F-15E CAS loadout less empty. 2021-05-22 16:45:06 -07:00
Dan Albert
fc56642631 Fix default mission length in the UI too. 2021-05-22 16:15:29 -07:00
Dan Albert
69299d395c Empty gun for AI missions that do not need a gun.
There is no "all but gun" RTB winchester option, so air to ground
missions with mixed weapon types will insist on using all of their
bullets after running out of missiles and bombs. Take away their bullets
so they don't strafe a Tor.

Exceptions are made for player flights and for airframes where the gun
is essential like the A-10 or warbirds.
2021-05-22 16:14:20 -07:00
Dan Albert
a789f58068 Allow SEAD escorts against broken SAMs and EWRs. 2021-05-22 15:37:31 -07:00
Dan Albert
f68935735d Correct radar detection.
This list includede units without radars and also missed units with
radars. Stop curating the list and just query the unit type.
2021-05-22 15:36:56 -07:00
Dan Albert
ba2157cc43 Tweak winchester behavior for SEAD escort.
They can't suppress air defenses after running out of TALDs or ARMs.
2021-05-22 15:09:10 -07:00
Dan Albert
57fe5c04ec Improve DEAD mission planning.
We don't need to include a SEAD flight in missions against EWRs or SAMs
that no longer have a radar.

Also plan DEAD missions against air defenses that have no radars.
Previously we would never finish killing launcher only sites (which
cannot defend any more, but are cheaper to return to working order than
a fully destroyed site) nor would we plan DEAD against IR SAMs or AAA.
2021-05-22 14:56:18 -07:00
Dan Albert
3a08944c99 Use the CAS DCS type for SEAD so F-14s can do it.
The CAS task type appears to be a superset of the SEAD task in every
way. Larger task variety as well as larger aircraft pool.
2021-05-22 14:27:30 -07:00
Dan Albert
b6154b273c Differentiate SEAD and SEAD escorts.
SEAD suppresses the package's target. SEAD escort protects the package
from any SAM threat along its flight path.
2021-05-22 14:24:13 -07:00
Dan Albert
e332bff362 Decrease error margin on TOT planning.
Everyone seems to do pretty okay generally, with the exception of
estimating ground ops time, which I've also increased (and is a
non-issue for runway/air start defaults).
2021-05-22 01:11:28 -07:00
Dan Albert
59e03434e4 Increase flight speeds to mach 0.85 or 85% of max.
Everyone seems a bit slow, generally. 0.85 is probably a better cruise
speed for supersonic jets and 85% of max is probably fine for subsonic.
2021-05-22 01:10:23 -07:00
Dan Albert
2ca0edf5fd Increase estimate for airfield ground ops.
5 minutes is pretty optimistic at most airfields.
2021-05-22 01:09:43 -07:00
Dan Albert
90dca9072e Change default mission duration to 60 minutes.
This seems like it works better for the number of missions we usually
frag, plus the fact that players will almost always choose an ASAP
package.
2021-05-22 01:04:10 -07:00
Dan Albert
c0ead4a484 Add icons for CPs. 2021-05-21 23:26:15 -07:00
Dan Albert
f8cb9e2bd3 Update Inherent Resolve to use non-random TGOs.
Also adds Ramat David to blue.
2021-05-21 19:13:32 -07:00
Dan Albert
df4dabf68f Add an LHA to Abu Dhabi. 2021-05-21 17:59:19 -07:00
Dan Albert
40720f9949 Fix convoy spawn point from Tabqa to Jirah.
The current spawn point is a disonnected road network according to the
map, so the convoy will never start moving because they have no route to
the destination.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1101
2021-05-21 17:54:01 -07:00
Dan Albert
7e7a1dce7b Fix icons for dead SAMs showing as damaged.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1099
2021-05-21 11:11:44 -07:00
Dan Albert
39b9a7f0ed Note DCS bugs that are common issues. 2021-05-21 00:24:12 -07:00
Dan Albert
43010779d4 Enable multirole fighter targeting for escorts.
pydcs didn't support this until now :(
2021-05-20 23:58:05 -07:00
Dan Albert
a1a4fc8c7c Update pydcs. 2021-05-20 23:57:52 -07:00
Dan Albert
621e4a513c Fix DEAD flights to use more than just missiles.
It doesn't seem like AI pilots are capable of using more than one weapon
effectively (see link below), but this at least makes DEAD flights work
when the DEAD flight is carrying only one type of weapon and some other
flight is performing SEAD.

https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/
2021-05-20 23:51:39 -07:00
Dan Albert
6c821039b5 Add a spectator slot.
So I stop accidentally giving orders while testing AI behavior after
waiting 20 minutes for them to get to their objective.
2021-05-20 21:46:20 -07:00
Dan Albert
f80b948fb1 Fix loadout downgrading bugs.
If the pylon had no weapon this would raise, and if no replacement was
found we wouldn't clear the pylon.
2021-05-20 21:10:02 -07:00
Dan Albert
d4c27da892 Move the strike eagle down the CAP list.
Most of the pylons are not actually capable of carrying air to air
missiles.
2021-05-20 20:12:49 -07:00
Dan Albert
11bf0ca868 Replace JSOW fallbacks with walleyes.
The GBU-12 that was chosen doesn't fit on the hornet in the first place,
and we ought to replace glide bombs with glide bombs.
2021-05-20 19:58:26 -07:00
Dan Albert
664092c023 Remove non-CP FOBs. 2021-05-20 19:01:43 -07:00
Dan Albert
0cd2c4a90c Stop awarding income for FOB structures.
The CP already grants income.

Fixes: https://github.com/dcs-liberation/dcs_liberation/issues/685
2021-05-20 18:59:03 -07:00
Dan Albert
2f6c04a86d Add bullseye to the kneeboard.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/136
2021-05-20 18:35:32 -07:00
Dan Albert
a382e74a89 Set up bullseye early, create waypoints.
Setting this up as part of the game makes it possible for us to show in
the UI.

https://github.com/dcs-liberation/dcs_liberation/issues/136
2021-05-20 18:29:35 -07:00
Khopa
3c8c76f50d Unit support : La Combattante II class ship 2021-05-21 02:45:15 +02:00
Khopa
cbce379132 Unit support : T-155 Firtina 2021-05-21 02:29:31 +02:00
Dan Albert
e795e96bfb Don't show red support units on blue kneeboards.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1080
2021-05-20 17:24:39 -07:00
Khopa
e12e2c4b0b Unit support : Chieftain Mk3 2021-05-21 02:24:14 +02:00
Khopa
9a1b21a2fa Unit support : PT-76 2021-05-21 02:19:52 +02:00
Khopa
79708f9ba6 Unit support : VAB Mephisto 2021-05-21 02:06:35 +02:00
Dan Albert
102544877d Fix common cases of kneeboard overflow. 2021-05-20 16:57:35 -07:00
Dan Albert
1c32ae1227 Create default strike target implementation. 2021-05-20 16:50:01 -07:00
Dan Albert
55d7e444c7 Split support info into its own kneeboard page.
The first page is getting very crowded.
2021-05-20 16:25:06 -07:00
Khopa
9243fd499b pydcs update for DCS 2.7.1 2021-05-21 01:15:54 +02:00
Khopa
844dc48d65 Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2021-05-20 13:23:06 +02:00
Dan Albert
52d96b8518 Include steerpoint number in strike page.
https://github.com/dcs-liberation/dcs_liberation/issues/1001
2021-05-20 00:20:48 -07:00
Dan Albert
8274e68846 Add SEAD/DEAD target info kneeboard page.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/965
2021-05-20 00:13:49 -07:00
Dan Albert
f1adcd1836 Be permissive about presumably incorrect loadouts.
These might be broken loadouts, or might be broken pydcs data. In case
it's the latter, attempt to load the pylon. DCS will remove the weapon
if it's not compatible automatically.
2021-05-19 23:33:24 -07:00
Dan Albert
2a77f57aa4 Improve AI strike targeting.
We were setting up all the correct *target* waypoints but the AI doesn't
use the target waypoints; they use the targets property of the ingress
waypoint. This meant that the flight plan looked correct in the UI and
was correct for players but the tasks were set up incorrectly for the AI
because building TGOs are aggravatingly multiple TGOs with the same name
in the implementation.

Mission targets now enumerate their own strike targets so that this
mistake is harder to make in the future.

This won't be perfect, the AI is still not able to parallelize tasks and
since buildings aren't groups they can only attack one structure at a
time, but they'll now at least switch to the next target after hitting
the first one.

As a bonus, stop bombing the dead buildings.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/235
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/244
2021-05-19 23:33:15 -07:00
Dan Albert
04ebe4c68a Add the rest of the TGO icons. 2021-05-19 21:40:36 -07:00
Dan Albert
1c7e065c52 Add more icons. 2021-05-19 21:24:54 -07:00
Dan Albert
80f3857f44 Stop forcing open CP tooltips.
These obscure the TGOs around them. Easy enough to mouse over, and the
names aren't the important part.
2021-05-19 20:49:51 -07:00
Dan Albert
3b62831401 Replace icons for some TGOs.
Only covered about half of the types so far.

This also removes the clustering, since that doesn't really seem to be
needed with the newer icons.
2021-05-19 20:47:52 -07:00
Khopa
a047e1d063 Updated Operation Dynamo campaign 2021-05-19 14:03:24 +02:00
Khopa
45985e1684 Updated payloads for Ju-88A4 2021-05-19 13:24:11 +02:00
Dan Albert
7dac886375 Remove fixed todo from the map. 2021-05-18 23:55:00 -07:00
Dan Albert
af3b8a9902 Set up icons for TGOs.
These are just the old icons, but it's better than nothing.
2021-05-18 23:35:25 -07:00
Dan Albert
4e37666037 Clean up the category property of TGOs.
This really needs to be a proper type, but this is a start: create new
categories for the types of TGOs that are missing. This removes some
icon special cases.
2021-05-18 23:35:25 -07:00
Dan Albert
2769d32c81 Mark enemy transfers in the transfer menu.
https://github.com/dcs-liberation/dcs_liberation/issues/1069
2021-05-18 21:20:38 -07:00
Dan Albert
4b004320a4 Make the transfer button more obvious.
https://github.com/dcs-liberation/dcs_liberation/issues/1069
2021-05-18 21:17:38 -07:00
Dan Albert
edfc879b41 Reorder some CAS aircraft priorities.
Move the harrier up, and the F-14s and B-1 down.
2021-05-18 21:04:11 -07:00
Dan Albert
0879d1da0d Use the best aircraft rather than first found.
The priority list was guiding the purchase decision which largely meant
that this was working correctly, but there were suboptimal cases where
the list was being taken in FIFO order by purchased type. This fixes the
search to be locally optimal, although this does still mean that a worse
but closer aircraft will be chosen over a better but slightly farther
away aircraft. We'd need to have a quality vs distance rating to do
better.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/755
2021-05-18 21:01:26 -07:00
Dan Albert
c5159f8a87 Fix swapped list order for purchase priorities.
We were accidentally iterating over the faction list and checking it
against the priority list rather than the other way around, so the
faction's aircraft list was being used for purchase priority rather than
the actual priority list in the game.
2021-05-18 19:54:39 -07:00
Dan Albert
a0d9bf0f26 Add Ju-88 to the CAS, strike, and DEAD lists.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1008
2021-05-18 19:51:27 -07:00
Dan Albert
a3cce8ff72 Invert the random location warning.
Campaigns should no longer be using these. Remove the warning when a
location cannot be found, and emit a warning when they are used.
2021-05-18 19:36:20 -07:00
Dan Albert
242f00390d Fix purchasing past 0 budget for ground units too.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1032
2021-05-18 19:23:29 -07:00
Dan Albert
f4b64370bb Remove fallback for old non-convoy behavior. 2021-05-18 19:21:43 -07:00
MaHuJa
ae57e4da83 Black sea campaign, inverted: "Russia" now blue. 2021-05-18 19:17:32 -07:00
Dan Albert
cd391a360c Add support for AAA objectives.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/999
2021-05-18 17:29:42 -07:00
Schneefl0cke
dcbe12f1af Changed latest planned flight to desired game settings. 2021-05-18 11:54:56 -07:00
Dan Albert
5b61cfe922 Remove random objectives from Abu Dhabi. 2021-05-18 00:48:12 -07:00
Dan Albert
739406614d Add support for required variants of all TGOs.
Adds required variants of:

* SHORADS
* Armor groups
* Buildings
* Oil rigs
* Coastal defenses
* Missile sites
* Ships

This is prep work for removing random generation.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1076
2021-05-18 00:48:12 -07:00
Dan Albert
8076206a90 Add major/minor versioning of the campaign schema.
Many of the schema version increases are just to add new features that
don't render old campaigns obsolete. Convert the version number to a
major/minor format so we can detect the difference between changes that
render old campaigns obsolete (major versions) and new features that
will not work on older builds of Liberation (minor versions).
2021-05-18 00:12:55 -07:00
Dan Albert
f63d218aae Fix loadouts to work with clean pylons. 2021-05-17 21:28:12 -07:00
Dan Albert
f2e3ccd18c Add loadout names for every Liberation task type.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/867
2021-05-17 01:51:21 -07:00
Dan Albert
d41e69d770 Add DCS loadout selector.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/273.
2021-05-17 01:24:55 -07:00
Dan Albert
0e3bc1ce43 Loadout implementation cleanup.
Loadout selection no longer has two (disagreeing) implementations. What
the UI shows is now what the miz will have.

We now store the chosen layout in the Flight *always*, not just for
custom loadouts. This means that we do loadout lookups at the start of
each turn, but the data is cached in pydcs.

Era-specific loadout degradation is still done at generation (and
presentation) time. This is so that players can toggle that option and
have it affect the *current* turn, rather than the next one.
2021-05-17 01:23:00 -07:00
Dan Albert
6ca175345f Fix DEAD for many bombers, audit DEAD/SEAD lists.
Many of the aircraft that we use for DEAD are not actually capable of
the SEAD task in DCS, so they were being loaded as some other task type,
usually one that doesn't support Attack Group, which made them lose
their waypoint actions and do nothing.

This switches them to using CAS which supports a superset of the SEAD
capable aircraft.

I've also audited the SEAD/DEAD lists. The F-117 was removed because it
is not capable of Attack Group *at all*, and all the non-SEAD aircraft
that are capable of ground attack moved from SEAD to DEAD.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1086
2021-05-16 22:23:01 -07:00
Dan Albert
c063a638cd Disable double click zoom action.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1087
2021-05-16 21:59:48 -07:00
Dan Albert
2dfe1420bc Add "Show all" option for flight plans.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1085
2021-05-16 13:15:51 -07:00
Dan Albert
7dd379c5c3 Show active supply routes in the new UI. 2021-05-16 12:58:03 -07:00
Dan Albert
752eb6235d Remove errant factory from Abu Dhabi.
This wasn't supposed to be here, and it wasn't showing up until I fixed
the bug preventing FOBs from spawning factories.
2021-05-16 12:55:20 -07:00
Dan Albert
3f077727ae Group allied and enemy SAM controls. 2021-05-15 22:08:00 -07:00
Dan Albert
51d557524d Fix unit list for non-building TGOs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1082
2021-05-15 22:05:14 -07:00
Dan Albert
5d9563304f Update commit boundaries after moving waypoints.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1084
2021-05-15 21:57:37 -07:00
Dan Albert
53cb68f82c Make waypoints draggable. 2021-05-15 16:21:03 -07:00
Dan Albert
95b107ffad Pop up CV move errors. 2021-05-15 14:22:19 -07:00
Dan Albert
06dedf51aa Improve CV waypointing UX. 2021-05-15 14:12:43 -07:00
Dan Albert
ed7c8c11d9 Make CV waypoint less janky.
The CP is now draggable when there is no destination. Otherwise the
destination is draggable.
2021-05-15 13:23:56 -07:00
Dan Albert
643e5954f3 Cluster TGOs less aggressively. 2021-05-15 12:41:04 -07:00
Dan Albert
c7cc9d2a65 Disable SAM threat fill, thicken outline.
As predicted, this only runs well on my machine.
2021-05-15 12:36:56 -07:00
Dan Albert
5050914d25 Make the new UI the default. 2021-05-15 03:38:29 -07:00
Dan Albert
31fa2d866f Allow setting CV routes in the new UI.
This is a pretty janky system until we get add context menu support. For
now the destination is set by dragging the CV marker and cleared by
right clicking the destination marker. Once we have a context menu a
context action will begin setting the destination the way it did in the
old UI, and the destination marker will be draggable.
2021-05-15 03:35:35 -07:00
Dan Albert
4a096cb728 Use load instead of setHtml so paths resolve.
Without this we can't resolve local paths to files.
2021-05-15 01:21:25 -07:00
Dan Albert
16b52f929c Bring CP markers to the front. 2021-05-15 00:21:22 -07:00
Dan Albert
8ec133830f Don't tell the UI about CP TGOs.
These are an implementation quirk, and passing them to the UI just means
that we put TGO pins on top of the CP, which makes the base menu
unopenable.

In the old UI we avoided this by not drawing anything that was
`for_airbase`, but now that we can zoom in further we're drawing base
defenses.
2021-05-15 00:10:05 -07:00
Dan Albert
c144799a11 Shade SAM threat ranges.
This runs a little bit worse but looks a lot better. Will flip back off
if this runs poorly on less powerful computers.
2021-05-14 23:58:37 -07:00
Dan Albert
e56511a05a Ack campaign version 4 for Abu Dhabi.
V4 is scenery objects, so not required.
2021-05-14 23:48:58 -07:00
Dan Albert
bdb959d986 Draw patrol commit ranges in the new map. 2021-05-14 23:41:55 -07:00
Dan Albert
dae9c368b7 Replace var with const/let in map.js. 2021-05-14 23:18:34 -07:00
Dan Albert
2a401a302d Add a topographic map layer. 2021-05-14 23:10:04 -07:00
Dan Albert
ff3b8e5270 Note scenery objective support in the changelog. 2021-05-14 17:19:49 -07:00
SnappyComebacks
bb1a066ff7 Let map objects be Strike targets.
This PR allows campaign creators to incorporate map objects (referred to as Scenery in the code) into their Liberation campaign.

Map objects are defined using white trigger zones created by right clicking on scenery and clicking `assign as...`.   Objective groups are defined by creating a blue TriggerZone surrounding the centers of the white trigger zones.  The type of objective is determined by the campaign creator, assigning the value of the first property of the blue TriggerZone with the objective type.

Map objects maintain their visually dead state by assigning a `Mission Start` `Scenery Object Dead` trigger to the trigger zone.  It is important for the Liberation generated TriggerZone to be as small as possible so that no other scenery is marked dead by DCS.

TriggerZones are hidden during gameplay (DCS behavior.  I don't know if it's possible to turn that off.)  TriggerZones are visible in the mission editor and mission planner however.  If a player is using an older plane, it is important for them to remember where the target is.

In the mission planner, the trigger zones' will be blue or red depending on which faction the map objects belong to.

Inherent Resolve campaign has been modified to integrate scenery objects.

### **Limitations:**
- Objective definitions (Any Blue TriggerZones) in campaign definition cannot overlap.
- Map object deaths in `state.json` is tracking integers.  You won't know what died until debriefing.
- No images for the various buildings.  In theory it can be done, but an unreasonable amount of work.
- Every blue trigger zone must have a unique name.  (If you let DCS auto increment the names this is not a concern.
- No output to screen when scenery object is dead.  You can see the building drawn as dead in the F10 map though.


### **Pictures:**

An objective:
![CampaignCreation](https://user-images.githubusercontent.com/74509817/117526797-c294af00-af84-11eb-9fb7-3940db64c5d8.png)

How the objective looks once in the mission planner/editor.  This objective belongs to the enemy faction:
![MissionPlanner](https://user-images.githubusercontent.com/74509817/117526819-ece66c80-af84-11eb-9db0-64000dedcf89.png)
2021-05-14 17:18:03 -07:00
Dan Albert
9a9872812f Generate factories at FOBs.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1021
2021-05-14 11:38:14 -07:00
Khopa
969e4a2d65 Merge remote-tracking branch 'khopa/develop' into develop 2021-05-14 16:16:11 +02:00
Khopa
77f0b87c54 Campaigns : Migrated campaign Golan Height to version 3 2021-05-14 15:31:46 +02:00
Dan Albert
eec56256e8 Add AEW&C aircraft to the faction aircraft list.
To avoid confusion, use only the aircraft list for the purchasable
aircraft. This fix also caught a faction's Tu-142 that was not actually
purchasable. Invalid aircraft in the faction aircraft list will now
raise an error.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1074
2021-05-13 21:44:07 -07:00
Dan Albert
99dc91dcb4 Fix game break when capturing factory.
We need to recompute the transit networks after a capture *before*
processing transfers. Otherwise units deployed that turn will not be
able to find their destination.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1070
2021-05-13 21:00:13 -07:00
Dan Albert
5adcfbd7bd Work around PySide2 bug in Property.
https://bugreports.qt.io/browse/PYSIDE-1426

For whatever reason this only shows up in packaged builds for us, and
also the recommended workaround of using a member property rather than a
decorated method does not work for us.

Until PySide2 5.15.3 (or later) is released, we need to use a named
signal for every property we expose.
2021-05-13 20:24:22 -07:00
Dan Albert
3c5f1f7c4b Log python version at startup. 2021-05-13 19:16:54 -07:00
Dan Albert
4415429661 Stop requiring user input for mkrelease.py. 2021-05-13 19:16:40 -07:00
Dan Albert
92c404fbb6 Persist DCS configuration across installs. 2021-05-13 18:55:48 -07:00
Khopa
956b9aaf95 Updated campaign "Russia Small" to campaign version 3 2021-05-13 19:30:10 +02:00
Khopa
c8348f1b44 Added campaign Operation Dynamo for The Channel map. 2021-05-13 18:59:53 +02:00
Dan Albert
d73ceb374c Add front lines to new map UI.
Only shows an approximate front line. Still need support for "actual".
2021-05-13 01:49:35 -07:00
Dan Albert
3e01953a3a Supply route styling, line weight rebalancing. 2021-05-13 01:23:15 -07:00
Dan Albert
1a65b1affb Connect CP/TGO package fragging dialogs. 2021-05-13 01:05:15 -07:00
Dan Albert
dd75078019 Connect TGO dialogs. 2021-05-13 00:56:35 -07:00
Dan Albert
1ab205cb46 Add tooltips for TGOs. 2021-05-13 00:44:47 -07:00
Dan Albert
eb26d54ac1 Show tooltips automatically based on zoom level. 2021-05-13 00:16:59 -07:00
Dan Albert
d884645f37 Cluster TGO markers. 2021-05-12 23:43:47 -07:00
Dan Albert
45f0c3c85f Add map scale widget. 2021-05-12 23:30:54 -07:00
Dan Albert
4e498e6932 Add waypoint info tooltip to the new map. 2021-05-12 23:03:53 -07:00
Dan Albert
d9d68cd37c Add a new Leaflet based map UI.
This is extremely WIP. It is not usable for play yet. Enable with
`--new-map`.
2021-05-12 21:52:23 -07:00
Schneefl0cke
56abd0bb7f Add option for setting desired mission length. 2021-05-11 03:13:15 -07:00
Dan Albert
747683e9e8 Allow other TGO types to be factories.
The `FactoryGroundObject` is just a special case of
`BuildingGroundObject` that we maybe don't actually need. For now it
provides some special case logic for the layout, but this allows any TGO
with the "factory" category to behave as a ground unit source.

Note that the "factory" random strike targets are *not* generated
anymore, so this doesn't affect campaign design currently.
2021-05-10 20:21:19 -07:00
Hornet2041
5b191d72a6 Add F-4E Phantom to the list of CAS/STRIKE capable planes. 2021-05-10 18:14:56 -07:00
Dan Albert
b7619630cf Add logged_duration context manager for profiling. 2021-05-08 18:06:55 -07:00
Dan Albert
de07f10e57 Remove fixed TODO. 2021-05-08 16:50:35 -07:00
Dan Albert
87e6080215 Fix "show actual front line location". 2021-05-08 16:49:05 -07:00
Dan Albert
e721a234e1 Clean up front line code.
The routes do not need be be recreated each time we create a
`FrontLine`. The front lines follow the convoy routes, which are static.
Add the convoy route data to the `ControlPoint` the way we do for
shipping lanes and have `FrontLine` load the data from there.
2021-05-08 16:46:02 -07:00
Dan Albert
67289bbba2 Fix now obvious reversal of convoy friendliness.
Convoy attack and shipping attacks were being planned against *only*
friendly targets. The renaming made the bug obvious.
2021-05-07 21:10:22 -07:00
Dan Albert
b0c24f6e51 Refactor front line code to make sides explicit.
A was intended to be the blue point and B was intended to be the red
point. Make this a part of the name so that's clear, and clean up
related code to keep that reliable.
2021-05-07 21:10:22 -07:00
Dan Albert
12f474ecbe Fix name of Al Minhad for latest DCS version. 2021-05-07 19:59:34 -07:00
Dan Albert
e2f20a7a65 Fix initialization order of turn 0.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/674
2021-05-07 19:58:39 -07:00
Dan Albert
58ffabe2d6 Fix aggressive objectives in Abu Dhabi.
Some of the objective locations for the starting front line are too
aggressive and put opfor at a disadvantage since blue ships might start
so close to their bases.
2021-05-07 19:37:25 -07:00
SnappyComebacks
426f06045e Updated pydcs version. 2021-05-07 19:13:45 -07:00
Dan Albert
2ca875192a Save budget for filling whole packages.
No sense filling airbases with cheap escorts if we'll never afford the
rest of the package. Filling the airbases with cheap escorts also makes
it impossible to buy the rest of the package when the faction eventually
does have the money since there's nowhere to park the needed aircraft.

https://github.com/dcs-liberation/dcs_liberation/issues/1058
2021-05-07 18:22:52 -07:00
Dan Albert
36b2f24de9 Skip planning for faction incompatible missions.
Required for improving purchasing as well, since we need to not halt
purchasing when a faction has no AEW&C aircraft.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/683
2021-05-07 18:22:52 -07:00
Dan Albert
2cf3b3be2b Fix bug causing overpurchase of aircraft.
After fulfilling a request we were not exiting the loop, so we'd fulfill
the request for the aircraft at _all_ the bases capable of operating it
until either the bases were full or the budget ran out. In factions like
Iraq 1991 this could cause the budget to be spent on tons of cheap
MiG-19s while never buying the more expensive Su-17s or Su-24s that they
need to actually complete a package.

https://github.com/dcs-liberation/dcs_liberation/issues/1058
2021-05-07 17:25:14 -07:00
Dan Albert
8320c6940b Fix map centering for CLI generated games. 2021-05-07 17:13:53 -07:00
Dan Albert
3c9d21e38d Fix CLI campaign generator.
Factions can be loaded from the user directory now so we need to know
where that is.
2021-05-07 17:13:03 -07:00
Dan Albert
0d7f00aef6 Fix lint error. 2021-05-07 16:58:42 -07:00
Dan Albert
1640763a7f Show parking status for enemy airfields. 2021-05-07 15:30:09 -07:00
Dan Albert
0ec5346574 Auto center the map on load.
Stops all PG campaigns from starting with their view pointed way up
north at the empty mountains.
2021-05-07 14:55:33 -07:00
Khopa
977845e2f4 Fixed links to github in the repo to account for the transfer to the new github organization. 2021-05-07 13:08:39 +02:00
Khopa
4bb2ab73c1 Changelog update 2021-05-07 13:03:15 +02:00
Khopa
af5584d244 Added a settings to control the amount smoke effects on frontlines. Default smoke spacing changed from 800 to 1600 (half the current amount) 2021-05-07 12:49:56 +02:00
Khopa
b289e41a0d Merge remote-tracking branch 'khopa/develop' into develop 2021-05-07 01:01:00 +02:00
Khopa
c0b4eef948 Updated readme for new organization repo link 2021-05-07 01:00:41 +02:00
Marcos Sigueros Fernández
b8e6c2fe78 Check for empty package before estimating TOT.
Fixes https://github.com/Khopa/dcs_liberation/issues/1014.
2021-05-03 11:01:41 -07:00
Dan Albert
b10e86e484 Add support for user faction directory.
This allows users to install custom factions to their home directory
rather than the Liberation install directory. Makes it easier to keep
mods across Liberation downloads, and easier for us devs to keep custom
factions without git always wanting us to add them.
2021-05-02 14:35:21 -07:00
Dan Albert
1c31cffe4b More Python 3.9 compat. 2021-05-02 14:33:24 -07:00
Dan Albert
b9822cd5d1 Python 3.9 compatibility.
This argument was removed and wasn't needed anyway since the file itself
is already opened UTF-8.
2021-05-02 14:31:08 -07:00
Dan Albert
ef1c70123c Remove duplicate SH-60s from factions. 2021-05-02 14:19:47 -07:00
Dan Albert
c245531d65 Update changelog for 2.5.1.
(cherry picked from commit 4555a4968d)
2021-05-02 13:18:21 -07:00
Dan Albert
522495fd11 Update pyinstaller.
Need a version smart enough to handle pyproj.

Fixes https://github.com/Khopa/dcs_liberation/issues/1049.
2021-05-01 11:06:01 -07:00
Marcos Sigueros Fernández
b2a551dc63 FIX: Purchase from airfield or anywhere allows negative budget. 2021-04-29 00:57:48 -07:00
SnappyComebacks
840107c69e 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:08:54 -07:00
Dan Albert
2a06a1ffdf Add proof-of-concept target info kneeboard page.
This is extremely rough and just serves as an example of how to use the
map projection API.
2021-04-27 21:20:11 -07:00
Dan Albert
8a01209ded Add data for lat/lon conversions. 2021-04-27 21:20:11 -07:00
Dan Albert
2b8dfc9dbc Stop counting neutral base captures in status.
These had no effect but were being counted on the waiting for mission
results page. Cleaned up the implementation a bunch while I was here.

Fixes https://github.com/Khopa/dcs_liberation/issues/1037
2021-04-26 17:54:32 -07:00
Dan Albert
e9f25eb562 Remove unused file.
mypy is flagging problems with this in the github action but not locally
for whatever reason, but it's not used so just delete it.
2021-04-25 20:30:55 -07:00
Dan Albert
475c7fd6db Update black and mypy requirements. 2021-04-25 18:46:37 -07:00
HerrTom
fa5d64022d Condense budget and intel sections of the top panel.
Budget and Intel panels now house a single button instead of separate Details buttons. Makes the top bar more compact and can fit in a 1080p monitor now.
2021-04-25 18:38:45 -07:00
Dan Albert
5c0f6cf65e Flip default for factory feature flag.
This is feature complete, we have a handful of campaigns that work with
the new mode now and it will be the only option at some point.
2021-04-25 18:20:20 -07:00
Dan Albert
0f8d366e31 Reformat with the latest black. 2021-04-25 18:09:33 -07:00
Dan Albert
9e2e593825 Improve transport descriptions. 2021-04-25 17:38:05 -07:00
Dan Albert
028bfc11eb Fix convoys skipping intermediate stops. 2021-04-25 17:38:05 -07:00
Dan Albert
b6cf7a4534 Fix cancelling convoys and cargo ships.
Not sure when I broke this but all transports were being cancelled as if
they were airlifts.
2021-04-25 17:10:24 -07:00
Dan Albert
0779679b99 Connect networks to enable multi-mode transfers.
Removing the per-transit type supply routes allows us to find the best
route from A to B even if the unit needs to switch transit modes along
the way.

The "best" route is the one that will generate better gameplay. That is,
convoys are preferred to ships (use cases for GMT are rare in DCS), and
ships are preferred to airlift (reasons to attack cargo ships are also
rare). Avoiding airlift is also a good strategic choice generally since
it consumes aircraft that could be performing other missions.

The extreme weight against airlift in the pathfinding algorithm could
probably be scaled way down so that airlift would be given preference
over a very long trip, possibly only for urgent transfers.

Later when we add rail that will probably be given the most preference,
but possibly between road and shipping.

https://github.com/Khopa/dcs_liberation/issues/823
2021-04-25 17:02:18 -07:00
Dan Albert
a48ef69e41 Disband cargo ships after processing.
Shipping lanes that received a ship would never lose their ships when
transfers completed, so the line on the map was staying solid (and
probably targetable).
2021-04-25 14:43:25 -07:00
Dan Albert
64d7953e50 Fix crash when opening FOB menus. 2021-04-25 14:39:32 -07:00
Dan Albert
21c35b31d4 AI planning for anti shipping missions.
Same as for anti convoy, these are rarely planned due to ordering issue
between mission planning and procurement.

Fixes https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 14:37:49 -07:00
Dan Albert
2d64acf299 Add mission targeting for cargo ships.
https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 14:31:13 -07:00
Dan Albert
e80819fc06 Add campaign inversion support to Abu Dhabi. 2021-04-25 14:17:24 -07:00
Dan Albert
7e40d58d04 Add cargo ships to the sim, track kills.
Not targetable yet.

https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 14:12:59 -07:00
Dan Albert
ba8fafcc95 First pass at cargo ships.
The simple form of this works, but without the multi-mode routing it'll
only get used when the final destination is a port with a link to a port
with a factory.

These also aren't targetable or simulated yet.

https://github.com/Khopa/dcs_liberation/issues/826
2021-04-25 12:10:06 -07:00
Dan Albert
42694d2004 Update 3.0 compatible campaigns to latest version.
Adds shipping lanes to Battle of Abu Dhabi. The others are acking the
new requirement but don't have an viable shipping lanes so no changes
are needed.
2021-04-24 21:55:40 -07:00
Dan Albert
5e67ce0ab2 Add shipping lane support to campaign files.
These don't actually do anything yet, this is just the campaign support
and UI.

https://github.com/Khopa/dcs_liberation/issues/826
2021-04-24 21:33:21 -07:00
Dan Albert
97b73e1a01 Add a north-to-south PG campaign.
Going to use this as the test bed for shipping transfers.
2021-04-24 19:43:40 -07:00
Dan Albert
4239257000 Add UAE 2015 faction.
Same as 2005 but with the Patriot  and some additional utilit aircraft.
2021-04-24 18:09:53 -07:00
Dan Albert
6bd94761d0 Update pydcs. 2021-04-24 15:33:51 -07:00
Starfire13
bc54e57fd4 Update campaigns and the splash damage plugin. 2021-04-24 15:30:31 -07:00
Dan Albert
17751e52fd Clear transports when disbanding convoys.
Also changes when we clear the convoys. Because we plan when transfers
are added (to plan UI orders immediately) we were planning convoys when
delivering units, then clearing the convoys, then planning them again.

Aside from the wasted effort, when we cleared the convoys we forgot to
tell the transfer that they no longer had transport, so when replanning
they did not get a new convoy.
2021-04-24 15:18:45 -07:00
Dan Albert
6016ebd3b4 Get transports from the closest airfield.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 23:24:25 -07:00
Dan Albert
8a44fc19ee Limit range for transport helicopters.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 23:24:25 -07:00
Dan Albert
f69450e2ae Add auto-procurment for airlift assets.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 23:24:25 -07:00
Mustang-25
3161ccced3 Re-adds GAR-8 (now AIM-9B in DCS) Fallback dates (#1025) 2021-04-23 22:25:11 -07:00
Dan Albert
909aad22a6 Make landing stops for cargo missions.
Until pydcs supports the timeReFuAr property this will have a wait time
of zero minutes, but it does seem to work.

Updating to https://github.com/pydcs/dcs/pull/132 will make the wait
time work automatically.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 22:19:54 -07:00
Dan Albert
c8b4fd1690 Use any (and only) transport aircraft for airlift.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 22:06:38 -07:00
Dan Albert
dac2271084 Add transport aircraft to US factions. 2021-04-23 22:06:38 -07:00
Dan Albert
5320d20f71 Add C-130 to the CAS capable list, clean up. 2021-04-23 21:21:05 -07:00
Dan Albert
20d8cc2b47 Plan transports at the beginning of the turn.
https://github.com/Khopa/dcs_liberation/issues/823
2021-04-23 20:31:52 -07:00
Dan Albert
d3fdbdbca5 Fix convoys not spawning where they should. 2021-04-23 20:23:23 -07:00
Dan Albert
d80f7ebf3b Refactor transfers to support unfulfilled orders.
This gives a clean break between the transfer request and the type of
transport allocated to make way for transports that need to switch
types (to support driving to a port, then getting on a ship, to a train,
then back on the road, etc).

https://github.com/Khopa/dcs_liberation/issues/823
2021-04-23 20:10:29 -07:00
C. Perreau
d6c84e362f Merge pull request #1012 from SpaceEnthusiast/develop
Added and modified factions
2021-04-24 00:13:45 +02:00
Khopa
6aba07c33b Fixed ai_flight_planner for maps lacking frontlines (such as battle of britain on The Channel map) 2021-04-24 00:10:57 +02:00
Dan Albert
45913b0add Bump to 3.0.
DCS version numbers and Liberation version numbers are getting
confusing. Push us ahead before we're both on 2.7.
2021-04-23 01:11:46 -07:00
Dan Albert
c258409a8d Add AI planning for airlifts.
Downside to the current implementation is that whether or not transports
that were purchased last turn will be available for airlift this turn is
arbitrary. This is because transfers are created at the same time as
units are delivered, and units are delivered in an arbitrary order per
CP. If the helicopters are delivered before the ground units they'll
have access to the transports, otherwise they'll be refunded. This will
be fixed later when I rework the transfer requests to not require
immediate fulfillment.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-23 01:10:03 -07:00
Dan Albert
26cd2d3fef Add todo for transfer refactor. 2021-04-22 22:24:57 -07:00
Dan Albert
4069074f41 Move unit delivery out of an unrelated file.
Historically this inherited from Event but there was no reason for that.
That's gone now. Finish the separation and move the unit order tracking
class out of the combat results reaction class's file.
2021-04-22 22:15:45 -07:00
Dan Albert
182422249f Remove support for mizdata/random locations.
The introduction of factories broke every existing campaign so we don't
need to keep support for the ancient formats any more.
2021-04-22 22:12:52 -07:00
Dan Albert
29b70b3247 Track airlift cargo kills.
https://github.com/Khopa/dcs_liberation/issues/825
2021-04-22 19:22:41 -07:00
Dan Albert
e474748f4d 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.
2021-04-22 18:20:26 -07:00
Dan Albert
132ba905c7 Generalize commit range display for all patrols.
Fixes https://github.com/Khopa/dcs_liberation/issues/890
2021-04-22 17:55:14 -07:00
Dan Albert
208d1b82b5 Show BARCAP commit ranges by default.
BARCAP placement confuses a lot of people but this should make it more
clear.
2021-04-22 17:45:19 -07:00
Simon Clark
1fd7c95f1b Fix Russian carrier name; Kuznetov -> Kuznetsov. 2021-04-22 23:08:56 +01:00
Dan Albert
481f195725 Airlift support.
UI isn't finished. Bulk transfers where the player doesn't care what
aircraft get used work (though they're chosen with no thought at all),
but being able to plan your own airlift flight isn't here yet.

Cargo planes are not implemented yet.

No way to view the cargo of a flight (will come with the cargo flight
planning UI).

The airlift flight/package creation should probably be moved out of the
UI and into the game code.

AI doesn't use these yet.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-22 00:30:18 -07:00
SpaceEnthusiast
d6c1550a1d Added Australia with C130 Faction 2021-04-21 23:58:18 -04:00
SpaceEnthusiast
60b9ae0a70 Added Canada with C130 Faction 2021-04-21 23:43:31 -04:00
SpaceEnthusiast
bf71351e6d Canadian, Australian, and Spanish Hornets now use proper liveries 2021-04-21 22:45:44 -04:00
Dan Albert
8e361a8776 Fix missing index valid check in transfer menu.
Fixes https://github.com/Khopa/dcs_liberation/issues/1006
2021-04-21 17:14:45 -07:00
Khopa
e39fd53727 Fixed Lint issue 2021-04-21 22:55:08 +02:00
Khopa
76efcca64b Fixed error with Ramat David frequency (typo) 2021-04-21 22:41:04 +02:00
Khopa
35f49d9bc0 Fixed airfields frequency on Persian Gulf 2021-04-21 22:31:52 +02:00
Khopa
a0a55797a9 Fixed airfields frequency on Syria 2021-04-21 22:31:49 +02:00
Khopa
696a429e9e Pydcs update to latest version 2021-04-21 13:20:06 +02:00
Dan Albert
29cd55e795 Add WIP airlift UI.
Doesn't do anything yet.

https://github.com/Khopa/dcs_liberation/issues/825
2021-04-21 01:02:02 -07:00
Dan Albert
c2ebf61fd3 Disable cancel actions on enemy transfers.
Decided to leave these in the menu as intel for the player, but the
player can no longer cancel them.

Fixes https://github.com/Khopa/dcs_liberation/issues/995
2021-04-20 23:20:08 -07:00
Dan Albert
489b4d6acf Add AI convoy attack planning.
Like the comment says this rarely has any effect due to the ordering of
flight planning and convoy creation. Could be separated, but opfor will
still not be able to target any convoys that the player creates in the
UI on that turn because they planning is done before the player can use
the UI.

Multi-turn transfers will be targetable, however.
2021-04-20 22:56:53 -07:00
Dan Albert
6cffc47f3c Clean up convoy code. 2021-04-20 22:21:42 -07:00
Dan Albert
50d8e08a34 Redo convoy attack flight plans.
The previous flight plan only makes sense if the convoy will make it a
significant distance from its starting point. At road speeds over the
typical mission duration this is not true, so we can actually plan this
as if it was a strike mission near the origin point and that's close
enough.

There's some cleanup work to do here that I've added todos for.

Fixes https://github.com/Khopa/dcs_liberation/issues/996
2021-04-20 22:04:57 -07:00
Dan Albert
3f16c0378a Add BAI planning against supply routes.
This currently is only supported for player flights. I have no idea how
to create an AI flight plan that won't just get them killed. AI-only BAI
missions against supply routes will warn the player on mission creation.
2021-04-19 23:09:39 -07:00
Dan Albert
2b06d8a096 Convert front line segments to proper class.
Needed so we can add context menus to the lines.
2021-04-19 21:41:56 -07:00
Dan Albert
2a5b37b9ad Show convoys on the map. 2021-04-19 20:37:15 -07:00
Dan Albert
cabbd234af Fix incorrect docs. 2021-04-19 20:24:20 -07:00
Dan Albert
480039ca50 Add --cheats to the CLI mission generator.
Doesn't enable the red ATO display because that's a lot of clutter, but
enables the commonly needed for debugging things.
2021-04-19 17:22:31 -07:00
Dan Albert
81d5cddac9 Remove weird single-CP supply route edge case.
A CP with a factory would be able to supply itself, but was not in a
supply route if it was the only connected friendly CP. When the player
starts with only one base against an enemy base this meant that it was
in no supply route, causing it to not be a recruitment location or a
place to buy more than a reserve of vehicles automatically.
2021-04-19 17:15:41 -07:00
Dan Albert
bb3e83548c Remove support for old-style campaigns.
These won't work any more since they won't be able to define factories
or supply routes. They were made obsolete ages ago, so just remove them.
2021-04-19 01:54:16 -07:00
Dan Albert
a3ff58c42d Turn feature flags on for development mode. 2021-04-19 00:04:46 -07:00
Dan Albert
30bf4542f0 Special case turn 0 for recruitment.
We want there to be units on the front line on turn 1 regardless of
factory locations, so bypass the recruitment restrictions on turn 0.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-19 00:03:47 -07:00
Dan Albert
d11c9a4615 Use convoy spawn points defined by the campaign.
The start/end points of the waypoints that define the course of the
front line also define the spawn points for convoys. Use them.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 23:57:13 -07:00
Dan Albert
d4679e0029 Note the semi-completion of the factory feature.
https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:35:18 -07:00
Dan Albert
3c4d6eb8e4 Automate transfers for purchases in the UI.
Buying a unit places the order, but the unit will appear at the nearest
connected source and a transfer will be automatically created next turn.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:31:45 -07:00
Dan Albert
df98e1f8ac Fix campaign format version.
Updatd the comment but forgot to update the number...
2021-04-18 23:25:44 -07:00
Dan Albert
56fc2986e9 Automate transfers from factories.
The purchase system will seek out a source for its units when the
purchase is completed. If no source is available the order will be
refunded. Orders with no source available are prevented, so this only
happens when the source was cut off from the destination during the
turn.

There's still some funkiness going on with the first turn (but possibly
only when the first turn includes a cheat to capture) where the AI buys
a ton of units somewhere other than the front line. First turn behavior
should probably be different anyway, with the first turn allowing
purchases anywhere to avoid empty front lines while troops reinforce if
the front line isn't a factory.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
627f18c42b Require factories for purchasing ground units.
https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
67b341cbd6 Add feature flag for new ground unit recruitment.
This is going to render most campaigns unusable because they won't have
any places to spawn ground units, so flagging off for now.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
eff5b94db7 Add factory placement to the campaign files.
This also removes the "factory" type from the normal strike target
(money generating) generators to avoid confusion. Later only control
points with factories will be able to spawn ground units, at which point
these will no longer generate income.

https://github.com/Khopa/dcs_liberation/issues/986
2021-04-18 23:22:30 -07:00
Dan Albert
707323ca12 Update Inherent Resolve to latest camapign format.
* Moves front line endpoints to roads for convoys (not used yet).
* Adds EWR sites.
2021-04-18 23:22:30 -07:00
Dan Albert
cb2ba2f53a Update pydcs, move back to upstream. 2021-04-18 23:22:11 -07:00
Dan Albert
777cd310ef Clarify which game we're talking about. 2021-04-18 17:45:27 -07:00
Dan Albert
9a4ec5a899 Fix campaign description template. 2021-04-18 17:41:27 -07:00
Dan Albert
c92e4e06cc Move campaign format version to a stable location. 2021-04-18 17:37:55 -07:00
Dan Albert
39135f8c80 Add version field to campaign descriptor file.
This is used to provide a UI hint to guide players towards campaigns
that have been updated to work with the current version of the game.
All the campaigns we currently have were made for an unknown version of
the game, so they're all flagged as incompatible.

The version field is not the DCS Liberation version number because the
campaign format may change multiple times during development. Instead
the version number is a monotonically increasing integer that we
increment whenever a game change requires campaign updates.
2021-04-18 17:30:49 -07:00
Dan Albert
5e054cfc77 Disallow selling ground units.
Ground units should be transferred to a new location, not sold and
repurchased.

https://github.com/Khopa/dcs_liberation/issues/823
2021-04-18 16:32:02 -07:00
Dan Albert
3b72c13f9d Add ground unit transfers to the changelog.
Also documented the behavior on the wiki (link in the changelog).

This is currently fully functional for players, but since units can be
bought and sold at any base there's no real reason to use these yet.
Will follow up with making ground units only purchasable at bases with
factories (the UI will still allow the purchase directly at the base,
but it will automatically create the transfer order) so convoys end up
being used, and to make factories a more interesting strategic target.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
65ed110ab7 Track convoy kills.
https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
5dd7ea3060 Spawn convoys for transfers.
Destroying these units currently has no effect.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
bd9cbf5e3b Move transfers one CP per turn.
https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 16:22:51 -07:00
Dan Albert
65f6a4eddd Restrict transfers to connected bases.
https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 15:59:15 -07:00
Dan Albert
e9ff554f39 Basic implementation of road based transfers.
This adds the models and UIs for creating ground unit transfer orders.
Most of the feature is still missing:

* The AI doesn't do them.
* Transfers can move across the whole map in one turn.
* Transfers between disconnected bases are allowed.
* Transfers are not modeled in the simulation, so they can't be
  interdicted.

https://github.com/Khopa/dcs_liberation/issues/824
2021-04-18 15:59:15 -07:00
Dan Albert
b65d178cf1 Move develop to 2.6. 2021-04-18 15:57:18 -07:00
1051 changed files with 26783 additions and 16661 deletions

View File

@@ -9,6 +9,8 @@ assignees: ''
Before filing, please search the issue tracker to see if the issue has already been reported.
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Describe the bug**
A clear and concise description of what the bug is.

View File

@@ -0,0 +1,28 @@
---
name: Campaign update submission
about: Submit an update to a campaign you maintain.
title: 'Update for <campaign name>'
labels: campaign-update-submission
assignees: ''
---
This form should only be used for submitted updated miz/json files for campaigns
distributed with Liberation. If you are _requesting_ an update to a campaign, see
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
campaign has an owner, it will be updated before release. If it does not, you can
volunteer to own it.
If you are not the owner of the campaign listed on
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
there.
Otherwise, delete everything above the line below and fill out the following form. Note:
GitHub does not accept .miz files. You can either rename the file to .miz.txt or add the
file to a .zip file.
---
* Campaign name:
* Files:
* Update summary (optional):

View File

@@ -9,6 +9,8 @@ assignees: ''
Before filing, please search the issue tracker to see if this feature has already been requested.
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

@@ -11,10 +11,10 @@ jobs:
with:
submodules: true
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9
- name: Install environment
run: |

View File

@@ -13,10 +13,10 @@ jobs:
with:
submodules: true
- name: Set up Python 3.8
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.8
python-version: 3.9
- name: Install environment
run: |

1
.gitignore vendored
View File

@@ -12,6 +12,7 @@ resources/tools/a.miz
# User-specific stuff
.idea/
.env
env/
/kneeboards
/liberation_preferences.json

4
.gitmodules vendored
View File

@@ -1,4 +0,0 @@
[submodule "pydcs"]
path = pydcs
url = https://github.com/pydcs/dcs
branch = master

View File

@@ -11,7 +11,7 @@ Note that you may need to remove the filter for open bugs if it's something we'v
## Making content for Liberation
You can create new campaigns : See [campaign creation wiki](https://github.com/Khopa/dcs_liberation/wiki/Custom-Campaigns).
You can create new campaigns : See [campaign creation wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns).
You can also improve existing campaigns.
You can then submit new campaigns on the "campaigns" channel on Discord, or by making a pull request if you are comfortable with git.

View File

@@ -1,39 +1,46 @@
![Logo](https://i.imgur.com/c2k18E1.png)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?logo=paypal)](https://www.paypal.com/paypalme/KhopaDCSL)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
[![Download](https://img.shields.io/github/downloads/khopa/dcs_liberation/total?label=Download)](https://github.com/Khopa/dcs_liberation/releases)
[![Download](https://img.shields.io/github/downloads/dcs-liberation/dcs_liberation/total?label=Download)](https://github.com/dcs-liberation/dcs_liberation/releases)
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/khopa/dcs_liberation)](https://github.com/Khopa/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/khopa/dcs_liberation)](https://github.com/Khopa/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/khopa/dcs_liberation?style=social)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/dcs-liberation/dcs_liberation?style=social)
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Logo](https://i.imgur.com/4hq0rLq.png)
![Screenshot](https://user-images.githubusercontent.com/315852/120939254-0b4a9f80-c6cc-11eb-82f5-ce3f8d714bfe.png)
## Downloads
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
Latest release is available here : https://github.com/dcs-liberation/dcs_liberation/releases
To download preview builds of the next version of DCS Liberation, see https://github.com/Khopa/dcs_liberation/wiki/Preview-builds.
To download preview builds of the next version of DCS Liberation, see https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds.
## DCS bugs
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
_don't_ spam them with comments):
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)
## Bugs and feature requests
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/Khopa/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/dcs-liberation/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
## Roadmap
Our plans for future releases can be found on our [Projects page](https://github.com/Khopa/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/Khopa/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
Our plans for future releases can be found on our [Projects page](https://github.com/dcs-liberation/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
## Resources
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/)
## Special Thanks

View File

@@ -1,3 +1,162 @@
# 4.1.1
Saves from 4.1.0 are compatible with 4.1.1.
## Fixes
* **[Campaign]** Fixed broken support for Mariana Islands map.
* **[Mission Generation]** Fix SAM sites pointing towards the center of the conflict.
* **[Flight Planning]** No longer using Su-34 for CAP missions.
# 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.
* **[Mission Generation]** SAM sites are now headed towards the center of the conflict
* **[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
* **[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]** 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.
* **[UI]** Removed ability to buy (useless) ground units at carriers and LHAs.
* **[UI]** Fixed enable/disable of buy/sell buttons.
* **[UI]** EWRs now appear in the custom waypoint list.
# 3.0.0
Saves from 2.5 are not compatible with 3.0.
## Features/Improvements
* **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information.
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
* **[Campaign]** Non-control point FOBs will no longer spawn.
* **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
* **[Campaign]** Capturing a base now depopulates all of its attached objectives with units: air defenses, EWRs, ships, armor groups, etc. Buildings are captured.
* **[Campaign]** Ammunition Depots determine how many ground units can be deployed on the frontline by a control point.
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
* **[Campaign AI]** Auto purchase will aim to purchase enough ground units to support the frontline, plus 30% reserve units.
* **[Campaign AI]** Auto purchase will now adjust its air/ground balance to favor whichever is under-funded.
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
* **[Flight Planner]** Flight plans now include bullseye waypoints.
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
* **[Flight Planner]** Planned airspeed increased to 0.85 mach for supersonic airframes and 85% of max speed for subsonic.
* **[Flight Planner]** Taxi time estimation for airfields increased from 5 minutes to 8 minutes.
* **[Flight Planner]** Reduce expected error margin for flight plans from 10% to 5%.
* **[Flight Planner]** SEAD flights are scheduled one minute ahead of the package's TOT so that they can suppress the site ahead of the strike.
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map.
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
* **[UI]** Added global aircraft inventory view under Air Wing dialog.
* **[UI]** Base menu now shows information about ground unit deployment limits.
* **[Modding]** Campaigns now choose locations for factories to spawn.
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
* **[Modding]** Campaigns now use map structures as strike targets.
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance. Support for random objective generation was removed.
* **[Modding]** Campaigns may now place AAA objectives.
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
* **[Skynet]** Updated to 2.1.0.
## Fixes
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
* **[Campaign AI]** Fixed bug causing AI to over-purchase cheap aircraft.
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
* **[Campaign]** EWR sites are now purchasable.
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
* **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites).
* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game.
# 2.5.1
## Features/Improvements

0
game/data/__init__.py Normal file
View File

View File

@@ -1,22 +0,0 @@
from dcs.vehicles import AirDefence
AAA_UNITS = [
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed_Emplacement,
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Closed_Emplacement_Insurgent,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.AAA_ZU_23_Insurgent,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_Flak_38_20mm,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38_Quad_20mm,
AirDefence.AAA_SP_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_40mm_Bofors,
AirDefence.AAA_S_60_57mm,
]

42
game/data/alic.py Normal file
View File

@@ -0,0 +1,42 @@
from dcs.unit import Unit
from dcs.vehicles import AirDefence
class AlicCodes:
CODES = {
AirDefence._1L13_EWR.id: 101,
AirDefence._55G6_EWR.id: 102,
AirDefence.S_300PS_40B6MD_sr.id: 103,
AirDefence.S_300PS_64H6E_sr.id: 104,
AirDefence.SA_11_Buk_SR_9S18M1.id: 107,
AirDefence.Kub_1S91_str.id: 108,
AirDefence.Dog_Ear_radar.id: 109,
AirDefence.S_300PS_40B6M_tr.id: 110,
AirDefence.SA_11_Buk_LN_9A310M1.id: 115,
AirDefence.Osa_9A33_ln.id: 117,
AirDefence.Strela_10M3.id: 118,
AirDefence.Tor_9A331.id: 119,
AirDefence._2S6_Tunguska.id: 120,
AirDefence.ZSU_23_4_Shilka.id: 121,
AirDefence.P_19_s_125_sr.id: 122,
AirDefence.Snr_s_125_tr.id: 123,
AirDefence.Rapier_fsa_blindfire_radar.id: 124,
AirDefence.Rapier_fsa_launcher.id: 125,
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,
AirDefence.Hawk_tr.id: 204,
AirDefence.Roland_Radar.id: 205,
AirDefence.Hawk_cwar.id: 206,
AirDefence.Gepard.id: 207,
AirDefence.Vulcan.id: 208,
AirDefence.NASAMS_Radar_MPQ64F1.id: 209,
}
@classmethod
def code_for(cls, unit: Unit) -> int:
return cls.CODES[unit.type]

View File

@@ -3,37 +3,30 @@ import dcs
DEFAULT_AVAILABLE_BUILDINGS = [
"fuel",
"ammo",
"comms",
"oil",
"ware",
"farp",
"fob",
"power",
"factory",
"derrick",
]
WW2_FREE = ["fuel", "factory", "ware", "fob"]
WW2_FREE = ["fuel", "ware"]
WW2_GERMANY_BUILDINGS = [
"fuel",
"factory",
"ww2bunker",
"ww2bunker",
"ww2bunker",
"allycamp",
"allycamp",
"fob",
]
WW2_ALLIES_BUILDINGS = [
"fuel",
"factory",
"allycamp",
"allycamp",
"allycamp",
"allycamp",
"allycamp",
"fob",
]
FORTIFICATION_BUILDINGS = [

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

@@ -1,7 +1,20 @@
from dataclasses import dataclass
from datetime import timedelta
from dcs.task import Reconnaissance
from game.utils import Distance, feet, nautical_miles
from game.data.groundunitclass import GroundUnitClass
@dataclass
class GroundUnitProcurementRatios:
ratios: dict[GroundUnitClass, float]
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
try:
return self.ratios[unit_class] / sum(self.ratios.values())
except KeyError:
return 0.0
@dataclass(frozen=True)
@@ -50,6 +63,8 @@ class Doctrine:
sweep_distance: Distance
ground_unit_procurement_ratios: GroundUnitProcurementRatios
MODERN_DOCTRINE = Doctrine(
cap=True,
@@ -76,6 +91,17 @@ MODERN_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(50),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(60),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 3,
GroundUnitClass.Atgm: 2,
GroundUnitClass.Apc: 2,
GroundUnitClass.Ifv: 3,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2,
GroundUnitClass.Recon: 1,
}
),
)
COLDWAR_DOCTRINE = Doctrine(
@@ -103,6 +129,17 @@ COLDWAR_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(35),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(40),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 4,
GroundUnitClass.Atgm: 2,
GroundUnitClass.Apc: 3,
GroundUnitClass.Ifv: 2,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 2,
GroundUnitClass.Recon: 1,
}
),
)
WWII_DOCTRINE = Doctrine(
@@ -130,4 +167,14 @@ WWII_DOCTRINE = Doctrine(
cap_engagement_range=nautical_miles(20),
cas_duration=timedelta(minutes=30),
sweep_distance=nautical_miles(10),
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
{
GroundUnitClass.Tank: 3,
GroundUnitClass.Atgm: 3,
GroundUnitClass.Apc: 3,
GroundUnitClass.Artillery: 1,
GroundUnitClass.Shorads: 3,
GroundUnitClass.Recon: 1,
}
),
)

View File

@@ -0,0 +1,17 @@
from __future__ import annotations
from enum import unique, Enum
@unique
class GroundUnitClass(Enum):
Tank = "Tank"
Atgm = "ATGM"
Ifv = "IFV"
Apc = "APC"
Artillery = "Artillery"
Logistics = "Logistics"
Recon = "Recon"
Infantry = "Infantry"
Shorads = "SHORADS"
Manpads = "MANPADS"

View File

@@ -1,72 +1,108 @@
from dcs.ships import (
Battlecruiser_1144_2_Pyotr_Velikiy,
Cruiser_1164_Moskva,
CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
Frigate_11540_Neustrashimy,
Corvette_1124_4_Grisha,
Frigate_1135M_Rezky,
Corvette_1241_1_Molniya,
LHA_1_Tarawa,
FFG_Oliver_Hazzard_Perry,
CG_Ticonderoga,
Type_052B_Destroyer,
Type_052C_Destroyer,
Type_054A_Frigate,
DDG_Arleigh_Burke_IIa,
PIOTR,
MOSCOW,
VINSON,
CVN_71,
CVN_72,
CVN_73,
Stennis,
KUZNECOW,
CV_1143_5,
NEUSTRASH,
ALBATROS,
REZKY,
MOLNIYA,
LHA_Tarawa,
PERRY,
TICONDEROG,
Type_052B,
Type_052C,
Type_054A,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import AirDefence
UNITS_WITH_RADAR = [
TELARS = {
AirDefence._2S6_Tunguska,
AirDefence.SA_11_Buk_SR_9S18M1,
AirDefence.Osa_9A33_ln,
AirDefence.Tor_9A331,
AirDefence.Roland_ADS,
}
TRACK_RADARS = {
AirDefence.Kub_1S91_str,
AirDefence.Snr_s_125_tr,
AirDefence.S_300PS_40B6M_tr,
AirDefence.Hawk_tr,
AirDefence.Patriot_str,
AirDefence.SNR_75V,
AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_STR_SP,
}
LAUNCHER_TRACKER_PAIRS = {
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr,
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
AirDefence.Hawk_ln: AirDefence.Hawk_tr,
AirDefence.Patriot_ln: AirDefence.Patriot_str,
AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
}
UNITS_WITH_RADAR = {
# Radars
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_11_Buk_Gadfly_C2,
AirDefence.SAM_Patriot_CR__AMG_AN_MRC_137,
AirDefence.SAM_Patriot_ECS,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_Long_Track_STR,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR,
AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR,
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Hawk_SR__AN_MPQ_50,
AirDefence.SAM_Patriot_STR,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3,
AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.HQ_7_Self_Propelled_STR,
AirDefence._2S6_Tunguska,
AirDefence.SA_11_Buk_LN_9A310M1,
AirDefence.Osa_9A33_ln,
AirDefence.Tor_9A331,
AirDefence.Gepard,
AirDefence.Vulcan,
AirDefence.Roland_ADS,
AirDefence.ZSU_23_4_Shilka,
AirDefence._1L13_EWR,
AirDefence.Kub_1S91_str,
AirDefence.S_300PS_40B6M_tr,
AirDefence.S_300PS_40B6MD_sr,
AirDefence._55G6_EWR,
AirDefence.S_300PS_64H6E_sr,
AirDefence.SA_11_Buk_SR_9S18M1,
AirDefence.Dog_Ear_radar,
AirDefence.Hawk_tr,
AirDefence.Hawk_sr,
AirDefence.Patriot_str,
AirDefence.Hawk_cwar,
AirDefence.P_19_s_125_sr,
AirDefence.Roland_Radar,
AirDefence.Snr_s_125_tr,
AirDefence.SNR_75V,
AirDefence.Rapier_fsa_blindfire_radar,
AirDefence.HQ_7_LN_SP,
AirDefence.HQ_7_STR_SP,
AirDefence.FuMG_401,
AirDefence.FuSe_65,
# Ships
CVN_70_Carl_Vinson,
FFG_Oliver_Hazzard_Perry,
CG_Ticonderoga,
Corvette_1124_4_Grisha,
CV_1143_5_Admiral_Kuznetsov,
Corvette_1241_1_Molniya,
Cruiser_1164_Moskva,
Frigate_11540_Neustrashimy,
Battlecruiser_1144_2_Pyotr_Velikiy,
Frigate_1135M_Rezky,
CV_1143_5_Admiral_Kuznetsov_2017,
CVN_74_John_C__Stennis,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa,
Type_052B_Destroyer,
Type_054A_Frigate,
Type_052C_Destroyer,
]
VINSON,
PERRY,
TICONDEROG,
ALBATROS,
KUZNECOW,
MOLNIYA,
MOSCOW,
NEUSTRASH,
PIOTR,
REZKY,
CV_1143_5,
Stennis,
CVN_71,
CVN_72,
CVN_73,
USS_Arleigh_Burke_IIa,
LHA_Tarawa,
Type_052B,
Type_054A,
Type_052C,
}

View File

@@ -4,15 +4,15 @@ import datetime
import inspect
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Iterator, Optional, Set, Tuple, Type, Union, cast
from dataclasses import dataclass, field
from typing import Dict, Iterator, Optional, Set, Tuple, cast, Any
from dcs.unitgroup import FlyingGroup
from dcs.unittype import FlyingType
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]
@@ -21,8 +21,8 @@ class Weapon:
"""Wraps a pydcs weapon dict in a hashable type."""
cls_id: str
name: str
weight: int
name: str = field(compare=False)
weight: int = field(compare=False)
def available_on(self, date: datetime.date) -> bool:
introduction_year = WEAPON_INTRODUCTION_YEARS.get(self)
@@ -59,6 +59,9 @@ class Weapon:
@classmethod
def from_clsid(cls, clsid: str) -> Optional[Weapon]:
data = weapon_ids.get(clsid)
if clsid == "<CLEAN>":
# Special case for a "weapon" that isn't exposed by pydcs.
return Weapon(clsid, "Clean", 0)
if data is None:
return None
return cls.from_pydcs(data)
@@ -70,11 +73,19 @@ class Pylon:
allowed: Set[Weapon]
def can_equip(self, weapon: Weapon) -> bool:
return weapon in self.allowed
# TODO: Fix pydcs to support the <CLEAN> "weapon".
# <CLEAN> is a special case because pydcs doesn't know about that "weapon", so
# it's not compatible with *any* pylon. Just trust the loadout and try to equip
# it.
#
# A similar hack exists in QPylonEditor to forcibly add "Clean" to the list of
# valid configurations for that pylon if a loadout has been seen with that
# 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):
raise ValueError(f"Pylon {self.number} cannot equip {weapon.name}")
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
@@ -86,12 +97,12 @@ class Pylon:
yield weapon
@classmethod
def for_aircraft(cls, aircraft: Type[FlyingType], number: int) -> Pylon:
def for_aircraft(cls, aircraft: AircraftType, number: int) -> Pylon:
# In pydcs these are all arbitrary inner classes of the aircraft type.
# The only way to identify them is by their name.
pylons = [
v
for v in aircraft.__dict__.values()
for v in aircraft.dcs_unit_type.__dict__.values()
if inspect.isclass(v) and v.__name__.startswith("Pylon")
]
@@ -110,8 +121,8 @@ class Pylon:
return cls(number, allowed)
@classmethod
def iter_pylons(cls, aircraft: Type[FlyingType]) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.pylons)):
def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]:
for pylon in sorted(list(aircraft.dcs_unit_type.pylons)):
yield cls.for_aircraft(aircraft, pylon)
@@ -134,20 +145,26 @@ _WEAPON_FALLBACKS = [
Weapons.LAU_117_with_AGM_65E___Maverick_E__Laser_ASM___Lg_Whd_,
), # internal pylons harrier
# AGM-154 JSOW
(Weapons.AGM_154A___JSOW_CEB__CBU_type_, Weapons.GBU_12),
(
Weapons.AGM_154A___JSOW_CEB__CBU_type_,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
(
Weapons.BRU_55_with_2_x_AGM_154A___JSOW_CEB__CBU_type_,
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
(
Weapons.BRU_57_with_2_x_AGM_154A___JSOW_CEB__CBU_type_,
None,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
), # doesn't exist on any aircraft yet
(Weapons.AGM_154B___JSOW_Anti_Armour, Weapons.CBU_105___10_x_CEM__CBU_with_WCMD),
(Weapons.AGM_154C___JSOW_Unitary_BROACH, Weapons.GBU_12),
(Weapons.AGM_154B___JSOW_Anti_Armour, Weapons.CBU_105___10_x_SFW__CBU_with_WCMD),
(
Weapons.AGM_154C___JSOW_Unitary_BROACH,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
(
Weapons.BRU_55_with_2_x_AGM_154C___JSOW_Unitary_BROACH,
Weapons.BRU_33_with_2_x_GBU_12___500lb_Laser_Guided_Bomb,
Weapons.AGM_62_Walleye_II___Guided_Weapon_Mk_5__TV_Guided_,
),
# AGM-45 Shrike
(Weapons.AGM_45A_Shrike_ARM, None),
@@ -472,29 +489,29 @@ _WEAPON_FALLBACKS = [
# CBU-87 CEM
(Weapons.CBU_87___202_x_CEM_Cluster_Bomb, Weapons.Mk_82),
(
Weapons.TER_9A_with_2_x_CBU_87___202_x_Anti_Armor_Skeet_SFW_Cluster_Bomb,
Weapons.TER_9A_with_2_x_CBU_87___202_x_CEM_Cluster_Bomb,
Weapons.TER_9A_with_2_x_Mk_82___500lb_GP_Bomb_LD,
),
(
Weapons.TER_9A_with_2_x_CBU_87___202_x_Anti_Armor_Skeet_SFW_Cluster_Bomb_,
Weapons.TER_9A_with_2_x_CBU_87___202_x_CEM_Cluster_Bomb_,
Weapons.TER_9A_with_2_x_Mk_82___500lb_GP_Bomb_LD,
),
(
Weapons.TER_9A_with_3_x_CBU_87___202_x_Anti_Armor_Skeet_SFW_Cluster_Bomb,
Weapons.TER_9A_with_3_x_CBU_87___202_x_CEM_Cluster_Bomb,
Weapons.TER_9A_with_3_x_Mk_82___500lb_GP_Bomb_LD,
),
# CBU-97
(Weapons.CBU_97___10_x_CEM_Cluster_Bomb, Weapons.Mk_82),
(Weapons.CBU_97___10_x_SFW_Cluster_Bomb, Weapons.Mk_82),
(
Weapons.TER_9A_with_2_x_CBU_97___10_x_Anti_Armor_Skeet_SFW_Cluster_Bomb,
Weapons.TER_9A_with_2_x_CBU_97___10_x_SFW_Cluster_Bomb,
Weapons.TER_9A_with_2_x_Mk_82___500lb_GP_Bomb_LD,
),
(
Weapons.TER_9A_with_2_x_CBU_97___10_x_Anti_Armor_Skeet_SFW_Cluster_Bomb_,
Weapons.TER_9A_with_2_x_CBU_97___10_x_SFW_Cluster_Bomb_,
Weapons.TER_9A_with_2_x_Mk_82___500lb_GP_Bomb_LD_,
),
(
Weapons.TER_9A_with_3_x_CBU_97___10_x_Anti_Armor_Skeet_SFW_Cluster_Bomb,
Weapons.TER_9A_with_3_x_CBU_97___10_x_SFW_Cluster_Bomb,
Weapons.TER_9A_with_3_x_Mk_82___500lb_GP_Bomb_LD,
),
# CBU-99 (It's a bomb made in 1968, I'm not bothering right now with backups)
@@ -504,7 +521,7 @@ _WEAPON_FALLBACKS = [
Weapons.CBU_87___202_x_CEM_Cluster_Bomb,
),
# CBU-105
(Weapons.CBU_105___10_x_CEM__CBU_with_WCMD, Weapons.CBU_97___10_x_CEM_Cluster_Bomb),
(Weapons.CBU_105___10_x_SFW__CBU_with_WCMD, Weapons.CBU_97___10_x_SFW_Cluster_Bomb),
(
Weapons.LAU_131_pod___7_x_2_75_Hydra__Laser_Guided_Rkts_M151__HE_APKWS,
Weapons.LAU_131_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
@@ -830,6 +847,8 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.LAU_115C_with_AIM_7F_Sparrow_Semi_Active_Radar): 1976,
Weapon.from_pydcs(Weapons.LAU_115C_with_AIM_7MH_Sparrow_Semi_Active_Radar): 1987,
# AIM-9 Sidewinder
Weapon.from_pydcs(Weapons.LAU_7_with_AIM_9B_Sidewinder_IR_AAM): 1956,
Weapon.from_pydcs(Weapons.LAU_7_with_2_x_AIM_9B_Sidewinder_IR_AAM): 1956,
Weapon.from_pydcs(Weapons.AIM_9L_Sidewinder_IR_AAM): 1977,
Weapon.from_pydcs(Weapons.AIM_9M_Sidewinder_IR_AAM): 1982,
Weapon.from_pydcs(Weapons.AIM_9P5_Sidewinder_IR_AAM): 1980,
@@ -869,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,
@@ -958,26 +977,14 @@ WEAPON_INTRODUCTION_YEARS = {
Weapon.from_pydcs(Weapons.CBU_52B___220_x_HE_Frag_bomblets): 1970,
# CBU-87 CEM
Weapon.from_pydcs(Weapons.CBU_87___202_x_CEM_Cluster_Bomb): 1986,
Weapon.from_pydcs(
Weapons.TER_9A_with_2_x_CBU_87___202_x_Anti_Armor_Skeet_SFW_Cluster_Bomb
): 1986,
Weapon.from_pydcs(
Weapons.TER_9A_with_2_x_CBU_87___202_x_Anti_Armor_Skeet_SFW_Cluster_Bomb_
): 1986,
Weapon.from_pydcs(
Weapons.TER_9A_with_3_x_CBU_87___202_x_Anti_Armor_Skeet_SFW_Cluster_Bomb
): 1986,
Weapon.from_pydcs(Weapons.TER_9A_with_2_x_CBU_87___202_x_CEM_Cluster_Bomb): 1986,
Weapon.from_pydcs(Weapons.TER_9A_with_2_x_CBU_87___202_x_CEM_Cluster_Bomb_): 1986,
Weapon.from_pydcs(Weapons.TER_9A_with_3_x_CBU_87___202_x_CEM_Cluster_Bomb): 1986,
# CBU-97
Weapon.from_pydcs(Weapons.CBU_97___10_x_CEM_Cluster_Bomb): 1992,
Weapon.from_pydcs(
Weapons.TER_9A_with_2_x_CBU_97___10_x_Anti_Armor_Skeet_SFW_Cluster_Bomb
): 1992,
Weapon.from_pydcs(
Weapons.TER_9A_with_2_x_CBU_97___10_x_Anti_Armor_Skeet_SFW_Cluster_Bomb_
): 1992,
Weapon.from_pydcs(
Weapons.TER_9A_with_3_x_CBU_97___10_x_Anti_Armor_Skeet_SFW_Cluster_Bomb
): 1992,
Weapon.from_pydcs(Weapons.CBU_97___10_x_SFW_Cluster_Bomb): 1992,
Weapon.from_pydcs(Weapons.TER_9A_with_2_x_CBU_97___10_x_SFW_Cluster_Bomb): 1992,
Weapon.from_pydcs(Weapons.TER_9A_with_2_x_CBU_97___10_x_SFW_Cluster_Bomb_): 1992,
Weapon.from_pydcs(Weapons.TER_9A_with_3_x_CBU_97___10_x_SFW_Cluster_Bomb): 1992,
# CBU-99
Weapon.from_pydcs(
Weapons.BRU_33_with_2_x_CBU_99___490lbs__247_x_HEAT_Bomblets
@@ -1019,11 +1026,11 @@ WEAPON_INTRODUCTION_YEARS = {
Weapons.MER2_with_2_x_Mk_20_Rockeye___490lbs_CBUs__247_x_HEAT_Bomblets
): 1968,
# CBU-103
Weapon.from_pydcs(Weapons.BRU_57_with_2_x_CBU_103): 2000,
Weapon.from_pydcs(Weapons.BRU_57_with_2_x_CBU_103___202_x_CEM__CBU_with_WCMD): 2000,
Weapon.from_pydcs(Weapons.CBU_103___202_x_CEM__CBU_with_WCMD): 2000,
# CBU-105
Weapon.from_pydcs(Weapons.BRU_57_with_2_x_CBU_105): 2000,
Weapon.from_pydcs(Weapons.CBU_105___10_x_CEM__CBU_with_WCMD): 2000,
Weapon.from_pydcs(Weapons.BRU_57_with_2_x_CBU_105___10_x_SFW__CBU_with_WCMD): 2000,
Weapon.from_pydcs(Weapons.CBU_105___10_x_SFW__CBU_with_WCMD): 2000,
# APKWS
Weapon.from_pydcs(
Weapons.LAU_131_pod___7_x_2_75_Hydra__Laser_Guided_Rkts_M151__HE_APKWS

1397
game/db.py

File diff suppressed because it is too large Load Diff

332
game/dcs/aircrafttype.py Normal file
View File

@@ -0,0 +1,332 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from pathlib import Path
from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
import yaml
from dcs.helicopters import helicopter_map
from dcs.planes import plane_map
from dcs.unittype import FlyingType
from game.dcs.unittype import UnitType
from game.radio.channels import (
ChannelNamer,
RadioChannelAllocator,
CommonRadioChannelAllocator,
HueyChannelNamer,
SCR522ChannelNamer,
ViggenChannelNamer,
ViperChannelNamer,
TomcatChannelNamer,
MirageChannelNamer,
SingleRadioChannelNamer,
FarmerRadioChannelAllocator,
SCR522RadioChannelAllocator,
ViggenRadioChannelAllocator,
NoOpChannelAllocator,
)
from game.utils import (
Distance,
SPEED_OF_SOUND_AT_SEA_LEVEL,
Speed,
feet,
kph,
knots,
)
if TYPE_CHECKING:
from gen.aircraft import FlightData
from gen import AirSupport, RadioFrequency, RadioRegistry
from gen.radios import Radio
@dataclass(frozen=True)
class RadioConfig:
inter_flight: Optional[Radio]
intra_flight: Optional[Radio]
channel_allocator: Optional[RadioChannelAllocator]
channel_namer: Type[ChannelNamer]
@classmethod
def from_data(cls, data: dict[str, Any]) -> RadioConfig:
return RadioConfig(
cls.make_radio(data.get("inter_flight", None)),
cls.make_radio(data.get("intra_flight", None)),
cls.make_allocator(data.get("channels", {})),
cls.make_namer(data.get("channels", {})),
)
@classmethod
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
from gen.radios import get_radio
if name is None:
return None
return get_radio(name)
@classmethod
def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
try:
alloc_type = data["type"]
except KeyError:
return None
allocator_type: Type[RadioChannelAllocator] = {
"SCR-522": SCR522RadioChannelAllocator,
"common": CommonRadioChannelAllocator,
"farmer": FarmerRadioChannelAllocator,
"noop": NoOpChannelAllocator,
"viggen": ViggenRadioChannelAllocator,
}[alloc_type]
return allocator_type.from_cfg(data)
@classmethod
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
return {
"SCR-522": SCR522ChannelNamer,
"default": ChannelNamer,
"huey": HueyChannelNamer,
"mirage": MirageChannelNamer,
"single": SingleRadioChannelNamer,
"tomcat": TomcatChannelNamer,
"viggen": ViggenChannelNamer,
"viper": ViperChannelNamer,
}[config.get("namer", "default")]
@dataclass(frozen=True)
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]
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@cached_property
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, kHz
if self.intra_flight_radio is not None:
return radio_registry.alloc_for_radio(self.intra_flight_radio)
# 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:
pass
return freq
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.channel_allocator is not None:
self.channel_allocator.assign_channels_for_flight(flight, air_support)
def channel_name(self, radio_id: int, channel_id: int) -> str:
return self.channel_namer.channel_name(radio_id, channel_id)
def __setstate__(self, state: dict[str, Any]) -> None:
# Update any existing models with new data on load.
updated = AircraftType.named(state["name"])
state.update(updated.__dict__)
self.__dict__.update(state)
@classmethod
def register(cls, aircraft_type: AircraftType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@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
def _each_unit_type() -> Iterator[Type[FlyingType]]:
yield from helicopter_map.values()
yield from plane_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {aircraft.id}; it will not be available")
return
with data_path.open() as data_file:
data = yaml.safe_load(data_file)
try:
price = data["price"]
except KeyError as ex:
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"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
name=variant,
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."),
role=data.get("role", "No data."),
price=price,
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,
)

100
game/dcs/groundunittype.py Normal file
View File

@@ -0,0 +1,100 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Type, Optional, ClassVar, Iterator
import yaml
from dcs.unittype import VehicleType
from dcs.vehicles import vehicle_map
from game.data.groundunitclass import GroundUnitClass
from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class GroundUnitType(UnitType[Type[VehicleType]]):
unit_class: Optional[GroundUnitClass]
spawn_weight: int
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[Type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
return self.name
@property
def dcs_id(self) -> str:
return self.dcs_unit_type.id
@classmethod
def register(cls, aircraft_type: GroundUnitType) -> None:
cls._by_name[aircraft_type.name] = aircraft_type
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
@classmethod
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
@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
def _each_unit_type() -> Iterator[Type[VehicleType]]:
yield from vehicle_map.values()
@classmethod
def _load_all(cls) -> None:
for unit_type in cls._each_unit_type():
for data in cls._each_variant_of(unit_type):
cls.register(data)
cls._loaded = True
@classmethod
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {vehicle.id}; it will not be available")
return
with data_path.open() as data_file:
data = yaml.safe_load(data_file)
try:
introduction = data["introduced"]
if introduction is None:
introduction = "N/A"
except KeyError:
introduction = "No data."
class_name = data.get("class")
unit_class: Optional[GroundUnitClass] = None
if class_name is not None:
unit_class = GroundUnitClass(class_name)
for variant in data.get("variants", [vehicle.id]):
yield GroundUnitType(
dcs_unit_type=vehicle,
unit_class=unit_class,
spawn_weight=data.get("spawn_weight", 0),
name=variant,
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."),
role=data.get("role", "No data."),
price=data.get("price", 1),
)

26
game/dcs/unittype.py Normal file
View File

@@ -0,0 +1,26 @@
from dataclasses import dataclass
from functools import cached_property
from typing import TypeVar, Generic, Type
from dcs.unittype import UnitType as DcsUnitType
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
@dataclass(frozen=True)
class UnitType(Generic[DcsUnitTypeT]):
dcs_unit_type: DcsUnitTypeT
name: str
description: str
year_introduced: str
country_of_origin: str
manufacturer: str
role: str
price: int
def __str__(self) -> str:
return self.name
@cached_property
def eplrs_capable(self) -> bool:
return getattr(self.dcs_unit_type, "eplrs", False)

View File

@@ -14,15 +14,24 @@ from typing import (
Dict,
Iterator,
List,
Type,
TYPE_CHECKING,
Union,
)
from dcs.unittype import FlyingType, UnitType
from game import db
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater import Airfield, ControlPoint
from game.unitmap import Building, FrontLineUnit, GroundObjectUnit, UnitMap
from game.transfers import CargoShip
from game.unitmap import (
AirliftUnits,
Building,
ConvoyUnit,
FrontLineUnit,
GroundObjectUnit,
UnitMap,
FlyingUnit,
)
from gen.flights.flight import Flight
if TYPE_CHECKING:
@@ -33,24 +42,24 @@ DEBRIEFING_LOG_EXTENSION = "log"
@dataclass(frozen=True)
class AirLosses:
player: List[Flight]
enemy: List[Flight]
player: List[FlyingUnit]
enemy: List[FlyingUnit]
@property
def losses(self) -> Iterator[Flight]:
def losses(self) -> Iterator[FlyingUnit]:
return itertools.chain(self.player, self.enemy)
def by_type(self, player: bool) -> Dict[Type[FlyingType], int]:
losses_by_type: Dict[Type[FlyingType], int] = defaultdict(int)
def by_type(self, player: bool) -> Dict[AircraftType, int]:
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
losses = self.player if player else self.enemy
for loss in losses:
losses_by_type[loss.unit_type] += 1
losses_by_type[loss.flight.unit_type] += 1
return losses_by_type
def surviving_flight_members(self, flight: Flight) -> int:
losses = 0
for loss in self.losses:
if loss == flight:
if loss.flight == flight:
losses += 1
return flight.count - losses
@@ -60,8 +69,17 @@ class GroundLosses:
player_front_line: List[FrontLineUnit] = field(default_factory=list)
enemy_front_line: List[FrontLineUnit] = field(default_factory=list)
player_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
enemy_ground_objects: List[GroundObjectUnit] = field(default_factory=list)
player_convoy: List[ConvoyUnit] = field(default_factory=list)
enemy_convoy: List[ConvoyUnit] = field(default_factory=list)
player_cargo_ships: List[CargoShip] = field(default_factory=list)
enemy_cargo_ships: List[CargoShip] = field(default_factory=list)
player_airlifts: List[AirliftUnits] = field(default_factory=list)
enemy_airlifts: List[AirliftUnits] = 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)
@@ -70,6 +88,12 @@ class GroundLosses:
enemy_airfields: List[Airfield] = field(default_factory=list)
@dataclass(frozen=True)
class BaseCaptureEvent:
control_point: ControlPoint
captured_by_player: bool
@dataclass(frozen=True)
class StateData:
#: True if the mission ended. If False, the mission exited abnormally.
@@ -81,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]
@@ -94,7 +119,10 @@ class StateData:
killed_aircraft=data["killed_aircrafts"],
# Airfields emit a new "dead" event every time a bomb is dropped on
# them when they've already dead. Dedup.
killed_ground_units=list(set(data["killed_ground_units"])),
#
# Also normalize dead map objects (which are ints) to strings. The unit map
# only stores strings.
killed_ground_units=list({str(u) for u in data["killed_ground_units"]}),
destroyed_statics=data["destroyed_objects_positions"],
base_capture_events=data["base_capture_events"],
)
@@ -105,6 +133,7 @@ class Debriefing:
self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
) -> None:
self.state_data = StateData.from_json(state_data)
self.game = game
self.unit_map = unit_map
self.player_country = game.player_country
@@ -114,6 +143,7 @@ class Debriefing:
self.air_losses = self.dead_aircraft()
self.ground_losses = self.dead_ground_units()
self.base_captures = self.base_capture_events()
@property
def front_line_losses(self) -> Iterator[FrontLineUnit]:
@@ -121,7 +151,22 @@ class Debriefing:
yield from self.ground_losses.enemy_front_line
@property
def ground_object_losses(self) -> Iterator[GroundObjectUnit]:
def convoy_losses(self) -> Iterator[ConvoyUnit]:
yield from self.ground_losses.player_convoy
yield from self.ground_losses.enemy_convoy
@property
def cargo_ship_losses(self) -> Iterator[CargoShip]:
yield from self.ground_losses.player_cargo_ships
yield from self.ground_losses.enemy_cargo_ships
@property
def airlift_losses(self) -> Iterator[AirliftUnits]:
yield from self.ground_losses.player_airlifts
yield from self.ground_losses.enemy_airlifts
@property
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
yield from self.ground_losses.player_ground_objects
yield from self.ground_losses.enemy_ground_objects
@@ -138,8 +183,8 @@ class Debriefing:
def casualty_count(self, control_point: ControlPoint) -> int:
return len([x for x in self.front_line_losses if x.origin == control_point])
def front_line_losses_by_type(self, player: bool) -> Dict[Type[UnitType], int]:
losses_by_type: Dict[Type[UnitType], int] = defaultdict(int)
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_front_line
else:
@@ -148,6 +193,38 @@ class Debriefing:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_convoy
else:
losses = self.ground_losses.enemy_convoy
for loss in losses:
losses_by_type[loss.unit_type] += 1
return losses_by_type
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
ships = self.ground_losses.player_cargo_ships
else:
ships = self.ground_losses.enemy_cargo_ships
for ship in ships:
for unit_type, count in ship.units.items():
losses_by_type[unit_type] += count
return losses_by_type
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
if player:
losses = self.ground_losses.player_airlifts
else:
losses = self.ground_losses.enemy_airlifts
for loss in losses:
for unit_type in loss.cargo:
losses_by_type[unit_type] += 1
return losses_by_type
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
losses_by_type: Dict[str, int] = defaultdict(int)
if player:
@@ -165,14 +242,14 @@ class Debriefing:
player_losses = []
enemy_losses = []
for unit_name in self.state_data.killed_aircraft:
flight = self.unit_map.flight(unit_name)
if flight is None:
aircraft = self.unit_map.flight(unit_name)
if aircraft is None:
logging.error(f"Could not find Flight matching {unit_name}")
continue
if flight.departure.captured:
player_losses.append(flight)
if aircraft.flight.departure.captured:
player_losses.append(aircraft)
else:
enemy_losses.append(flight)
enemy_losses.append(aircraft)
return AirLosses(player_losses, enemy_losses)
def dead_ground_units(self) -> GroundLosses:
@@ -186,6 +263,22 @@ class Debriefing:
losses.enemy_front_line.append(front_line_unit)
continue
convoy_unit = self.unit_map.convoy_unit(unit_name)
if convoy_unit is not None:
if convoy_unit.convoy.player_owned:
losses.player_convoy.append(convoy_unit)
else:
losses.enemy_convoy.append(convoy_unit)
continue
cargo_ship = self.unit_map.cargo_ship(unit_name)
if cargo_ship is not None:
if cargo_ship.player_owned:
losses.player_cargo_ships.append(cargo_ship)
else:
losses.enemy_cargo_ships.append(cargo_ship)
continue
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
if ground_object_unit is not None:
if ground_object_unit.ground_object.control_point.captured:
@@ -224,17 +317,46 @@ class Debriefing:
"have no effect. This may be normal behavior."
)
for unit_name in self.state_data.killed_aircraft:
airlift_unit = self.unit_map.airlift_unit(unit_name)
if airlift_unit is not None:
if airlift_unit.transfer.player:
losses.player_airlifts.append(airlift_unit)
else:
losses.enemy_airlifts.append(airlift_unit)
continue
return losses
@property
def base_capture_events(self):
def base_capture_events(self) -> List[BaseCaptureEvent]:
"""Keeps only the last instance of a base capture event for each base ID."""
reversed_captures = list(reversed(self.state_data.base_capture_events))
last_base_cap_indexes = []
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
if base not in [x[1] for x in last_base_cap_indexes]:
last_base_cap_indexes.append((idx, base))
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
blue_coalition_id = 2
seen = set()
captures = []
for capture in reversed(self.state_data.base_capture_events):
cp_id_str, new_owner_id_str, _name = capture.split("||")
cp_id = int(cp_id_str)
# Only the most recent capture event matters.
if cp_id in seen:
continue
seen.add(cp_id)
try:
control_point = self.game.theater.find_control_point_by_id(cp_id)
except KeyError:
# Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do.
continue
captured_by_player = int(new_owner_id_str) == blue_coalition_id
if control_point.is_friendly(to_player=captured_by_player):
# Base is currently friendly to the new owner. Was captured and
# recaptured in the same mission. Nothing to do.
continue
captures.append(BaseCaptureEvent(control_point, captured_by_player))
return captures
class PollDebriefingFileThread(threading.Thread):
@@ -250,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

@@ -1,12 +1,10 @@
from __future__ import annotations
import logging
import math
from typing import Dict, Iterator, List, TYPE_CHECKING, Tuple, Type
from typing import List, TYPE_CHECKING, Type
from dcs.mapping import Point
from dcs.task import Task
from dcs.unittype import UnitType, VehicleType
from game import persistency
from game.debriefing import AirLosses, Debriefing
@@ -15,7 +13,7 @@ from game.operation.operation import Operation
from game.theater import ControlPoint
from gen import AirTaskingOrder
from gen.ground_forces.combat_stance import CombatStance
from ..db import PRICES
from ..dcs.groundunittype import GroundUnitType
from ..unitmap import UnitMap
if TYPE_CHECKING:
@@ -39,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
@@ -55,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]]:
@@ -122,11 +120,15 @@ class Event:
self.game.red_ato, debriefing.air_losses, for_player=False
)
@staticmethod
def commit_air_losses(debriefing: Debriefing) -> None:
def commit_air_losses(self, debriefing: Debriefing) -> None:
for loss in debriefing.air_losses.losses:
aircraft = loss.unit_type
cp = loss.departure
if loss.pilot is not None and (
not loss.pilot.player
or not self.game.settings.invulnerable_player_pilots
):
loss.pilot.kill()
aircraft = loss.flight.unit_type
cp = loss.flight.departure
available = cp.base.total_units_of_type(aircraft)
if available <= 0:
logging.error(
@@ -138,6 +140,23 @@ class Event:
logging.info(f"{aircraft} destroyed from {cp}")
cp.base.aircraft[aircraft] -= 1
@staticmethod
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
for package in ato.packages:
for flight in package.flights:
for idx, pilot in enumerate(flight.roster.pilots):
if pilot is None:
logging.error(
f"Cannot award experience to pilot #{idx} of {flight} "
"because no pilot is assigned"
)
continue
pilot.record.missions_flown += 1
def commit_pilot_experience(self) -> None:
self._commit_pilot_experience(self.game.blue_ato)
self._commit_pilot_experience(self.game.red_ato)
@staticmethod
def commit_front_line_losses(debriefing: Debriefing) -> None:
for loss in debriefing.front_line_losses:
@@ -154,15 +173,56 @@ class Event:
logging.info(f"{unit_type} destroyed from {control_point}")
control_point.base.armor[unit_type] -= 1
@staticmethod
def commit_convoy_losses(debriefing: Debriefing) -> None:
for loss in debriefing.convoy_losses:
unit_type = loss.unit_type
convoy = loss.convoy
available = loss.convoy.units.get(unit_type, 0)
convoy_name = f"convoy from {convoy.origin} to {convoy.destination}"
if available <= 0:
logging.error(
f"Found killed {unit_type} in {convoy_name} but that convoy has "
"none available."
)
continue
logging.info(f"{unit_type} destroyed in {convoy_name}")
convoy.kill_unit(unit_type)
@staticmethod
def commit_cargo_ship_losses(debriefing: Debriefing) -> None:
for ship in debriefing.cargo_ship_losses:
logging.info(
f"All units destroyed in cargo ship from {ship.origin} to "
f"{ship.destination}."
)
ship.kill_all()
@staticmethod
def commit_airlift_losses(debriefing: Debriefing) -> None:
for loss in debriefing.airlift_losses:
transfer = loss.transfer
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
for unit_type in loss.cargo:
try:
transfer.kill_unit(unit_type)
logging.info(f"{unit_type} destroyed in {airlift_name}")
except KeyError:
logging.exception(
f"Found killed {unit_type} in {airlift_name} but that airlift "
"has none available."
)
@staticmethod
def commit_ground_object_losses(debriefing: Debriefing) -> None:
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:
@@ -181,62 +241,41 @@ class Event:
for damaged_runway in debriefing.damaged_runways:
damaged_runway.damage_runway()
def commit(self, debriefing: Debriefing):
logging.info("Committing mission results")
self.commit_air_losses(debriefing)
self.commit_front_line_losses(debriefing)
self.commit_ground_object_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing)
# ------------------------------
# Captured bases
# if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE
# else:
# coalition = 1 # Value in DCS mission event for RED
for captured in debriefing.base_capture_events:
def commit_captures(self, debriefing: Debriefing) -> None:
for captured in debriefing.base_captures:
try:
id = int(captured.split("||")[0])
new_owner_coalition = int(captured.split("||")[1])
if captured.captured_by_player:
info = Information(
f"{captured.control_point} captured!",
f"We took control of {captured.control_point}.",
self.game.turn,
)
else:
info = Information(
f"{captured.control_point} lost!",
f"The enemy took control of {captured.control_point}.",
self.game.turn,
)
captured_cps = []
for cp in self.game.theater.controlpoints:
if cp.id == id:
if cp.captured and new_owner_coalition != coalition:
for_player = False
info = Information(
cp.name + " lost !",
"The ennemy took control of "
+ cp.name
+ "\nShame on us !",
self.game.turn,
)
self.game.informations.append(info)
captured_cps.append(cp)
elif not (cp.captured) and new_owner_coalition == coalition:
for_player = True
info = Information(
cp.name + " captured !",
"We took control of " + cp.name + "! Great job !",
self.game.turn,
)
self.game.informations.append(info)
captured_cps.append(cp)
else:
continue
cp.capture(self.game, for_player)
for cp in captured_cps:
logging.info("Will run redeploy for " + cp.name)
self.redeploy_units(cp)
self.game.informations.append(info)
captured.control_point.capture(self.game, captured.captured_by_player)
logging.info(f"Will run redeploy for {captured.control_point}")
self.redeploy_units(captured.control_point)
except Exception:
logging.exception(f"Could not process base capture {captured}")
def commit(self, debriefing: Debriefing) -> None:
logging.info("Committing mission results")
self.commit_air_losses(debriefing)
self.commit_pilot_experience()
self.commit_front_line_losses(debriefing)
self.commit_convoy_losses(debriefing)
self.commit_airlift_losses(debriefing)
self.commit_ground_object_losses(debriefing)
self.commit_building_losses(debriefing)
self.commit_damaged_runways(debriefing)
self.commit_captures(debriefing)
self.complete_aircraft_transfers(debriefing)
# Destroyed units carcass
@@ -258,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)
@@ -279,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 (
@@ -306,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:
""" "
@@ -395,12 +454,12 @@ class Event:
moved_units[frontline_unit] = int(count * move_factor)
total_units_redeployed = total_units_redeployed + int(count * move_factor)
destination.base.commision_units(moved_units)
destination.base.commission_units(moved_units)
source.base.commit_losses(moved_units)
# Also transfer pending deliveries.
for unit_type, count in source.pending_unit_deliveries.units.items():
if not issubclass(unit_type, VehicleType):
if not isinstance(unit_type, GroundUnitType):
continue
if count <= 0:
# Don't transfer *sales*...
@@ -418,63 +477,3 @@ class Event:
info = Information("Units redeployed", text, self.game.turn)
self.game.informations.append(info)
logging.info(text)
class UnitsDeliveryEvent:
def __init__(self, control_point: ControlPoint) -> None:
self.to_cp = control_point
self.units: Dict[Type[UnitType], int] = {}
def __str__(self) -> str:
return "Pending delivery to {}".format(self.to_cp)
def order(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
def sell(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] = self.units.get(k, 0) - v
def consume_each_order(self) -> Iterator[Tuple[Type[UnitType], int]]:
while self.units:
yield self.units.popitem()
def refund_all(self, game: Game) -> None:
for unit_type, count in self.consume_each_order():
try:
price = PRICES[unit_type]
except KeyError:
logging.error(f"Could not refund {unit_type.id}, price unknown")
continue
logging.info(f"Refunding {count} {unit_type.id} at {self.to_cp.name}")
game.adjust_budget(price * count, player=self.to_cp.captured)
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
pending_units = self.units.get(unit_type)
if pending_units is None:
pending_units = 0
current_units = self.to_cp.base.total_units_of_type(unit_type)
return pending_units + current_units
def process(self, game: Game) -> None:
bought_units: Dict[Type[UnitType], int] = {}
sold_units: Dict[Type[UnitType], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.to_cp.captured else "Enemy"
aircraft = unit_type.id
name = self.to_cp.name
if count >= 0:
bought_units[unit_type] = count
game.message(
f"{coalition} reinforcements: {aircraft} x {count} at {name}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {aircraft} x {-count} at {name}")
self.to_cp.base.commision_units(bought_units)
self.to_cp.base.commit_losses(sold_units)
self.units = {}
bought_units = {}
sold_units = {}

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

@@ -1,14 +1,13 @@
from __future__ import annotations
import itertools
import logging
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, cast
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from dcs.unittype import ShipType, UnitType
from game.data.building_data import (
WW2_ALLIES_BUILDINGS,
@@ -22,11 +21,19 @@ from game.data.doctrine import (
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
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:
#: List of locales to use when generating random names. If not set, Faker will
#: choose the default locale.
locales: Optional[List[str]]
# Country used by this faction
country: str = field(default="")
@@ -41,25 +48,25 @@ class Faction:
description: str = field(default="")
# Available aircraft
aircrafts: List[Type[FlyingType]] = field(default_factory=list)
aircrafts: List[AircraftType] = field(default_factory=list)
# Available awacs aircraft
awacs: List[Type[FlyingType]] = field(default_factory=list)
awacs: List[AircraftType] = field(default_factory=list)
# Available tanker aircraft
tankers: List[Type[FlyingType]] = field(default_factory=list)
tankers: List[AircraftType] = field(default_factory=list)
# Available frontline units
frontline_units: List[Type[VehicleType]] = field(default_factory=list)
frontline_units: List[GroundUnitType] = field(default_factory=list)
# Available artillery units
artillery_units: List[Type[VehicleType]] = field(default_factory=list)
artillery_units: List[GroundUnitType] = field(default_factory=list)
# Infantry units used
infantry_units: List[Type[VehicleType]] = field(default_factory=list)
infantry_units: List[GroundUnitType] = field(default_factory=list)
# Logistics units used
logistics_units: List[Type[VehicleType]] = field(default_factory=list)
logistics_units: List[GroundUnitType] = field(default_factory=list)
# Possible SAMS site generators for this faction
air_defenses: List[str] = field(default_factory=list)
@@ -77,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)
@@ -110,7 +117,7 @@ class Faction:
has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction
jtac_unit: Optional[Type[FlyingType]] = field(default=None)
jtac_unit: Optional[AircraftType] = field(default=None)
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
@@ -119,7 +126,7 @@ class Faction:
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
liveries_overrides: Dict[Type[UnitType], List[str]] = field(default_factory=dict)
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
#: Set to True if the faction should force the "Unrestricted satnav" option
#: for the mission. This option enables GPS for capable aircraft regardless
@@ -130,10 +137,15 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
if vehicle.unit_class is unit_class:
return True
return False
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction()
faction = Faction(locales=json.get("locales"))
faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]:
@@ -150,14 +162,26 @@ class Faction:
faction.authors = json.get("authors", "")
faction.description = json.get("description", "")
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(json.get("logistics_units", []))
faction.aircrafts = list(
set(faction.aircrafts + faction.awacs + faction.tankers)
)
faction.frontline_units = [
GroundUnitType.named(n) for n in json.get("frontline_units", [])
]
faction.artillery_units = [
GroundUnitType.named(n) for n in json.get("artillery_units", [])
]
faction.infantry_units = [
GroundUnitType.named(n) for n in json.get("infantry_units", [])
]
faction.logistics_units = [
GroundUnitType.named(n) for n in json.get("logistics_units", [])
]
faction.ewrs = json.get("ewrs", [])
@@ -181,7 +205,7 @@ class Faction:
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
faction.jtac_unit = load_aircraft(jtac_name)
faction.jtac_unit = AircraftType.named(jtac_name)
else:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
@@ -215,90 +239,113 @@ class Faction:
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
for k, v in liveries_overrides.items():
k = load_aircraft(k)
if k is not None:
faction.liveries_overrides[k] = [s.lower() for s in v]
for name, livery in liveries_overrides.items():
aircraft = AircraftType.named(name)
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction
@property
def units(self) -> List[Type[UnitType]]:
return (
self.infantry_units
+ self.aircrafts
+ self.awacs
+ self.artillery_units
+ self.frontline_units
+ self.tankers
+ self.logistics_units
)
def ground_units(self) -> Iterator[GroundUnitType]:
yield from self.artillery_units
yield from self.frontline_units
yield from self.logistics_units
def infantry_with_class(
self, unit_class: GroundUnitClass
) -> Iterator[GroundUnitType]:
for unit in self.infantry_units:
if unit.unit_class is unit_class:
yield unit
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[Type[UnitType]]:
"""
Find unit by name
:param unit: Unit name as string
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
:return: The unit as a PyDCS type
"""
if unit is None:
return None
elif unit in plane_map.keys():
return plane_map[unit]
else:
for mother_class in class_repository:
if getattr(mother_class, unit, None) is not None:
return getattr(mother_class, unit)
if type(mother_class) is list:
for m in mother_class:
if m.__name__ == unit:
return m
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
return None
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 load_aircraft(name: str) -> Optional[Type[FlyingType]]:
return cast(
Optional[FlyingType],
unit_loader(name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]),
)
def remove_air_defenses(self, name: str) -> None:
for i in self.air_defenses:
if i == name:
self.air_defenses.remove(i)
def load_all_aircraft(data) -> List[Type[FlyingType]]:
items = []
for name in data:
item = load_aircraft(name)
if item is not None:
items.append(item)
return items
def load_vehicle(name: str) -> Optional[Type[VehicleType]]:
return cast(
Optional[FlyingType],
unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
),
)
def load_all_vehicles(data) -> List[Type[VehicleType]]:
items = []
for name in data:
item = load_vehicle(name)
if item is not None:
items.append(item)
return items
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]]:
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
if (ship := getattr(dcs.ships, name, None)) is not None:
return ship
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
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

@@ -2,8 +2,9 @@ from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Iterator, Optional, Type
from typing import Dict, Iterator, List, Optional, Type
from game import persistency
from game.factions.faction import Faction
FACTION_DIRECTORY = Path("./resources/factions/")
@@ -23,9 +24,16 @@ class FactionLoader:
if self._factions is None:
self._factions = self.load_factions()
@staticmethod
def find_faction_files_in(path: Path) -> List[Path]:
return [f for f in path.glob("*.json") if f.is_file()]
@classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
files = cls.find_faction_files_in(
FACTION_DIRECTORY
) + cls.find_faction_files_in(user_faction_path)
factions = {}
for f in files:

View File

@@ -1,20 +1,22 @@
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
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
from gen import naming
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -23,17 +25,21 @@ from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency
from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent
from .event.event import Event
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .income import Income
from .infos.information import Information
from .navmesh import NavMesh
from .procurement import ProcurementAi
from .settings import Settings
from .theater import ConflictTheater, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import MissileSiteGroundObject
from .procurement import AircraftProcurementRequest, ProcurementAi
from .profiling import logged_duration
from .settings import Settings, AutoAtoBehavior
from .squadrons import AirWing
from .theater import ConflictTheater, ControlPoint
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones
from .transfers import PendingTransfers
from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay
@@ -80,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,
@@ -91,52 +97,59 @@ 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.turn = 0
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] = []
# Culling Points are for individual theater ground objects that we don't wish to cull.
self.__culling_points: 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
self.current_unit_id = 0
self.current_group_id = 0
self.name_generator = naming.namegen
self.conditions = self.generate_conditions()
self.blue_transit_network = TransitNetwork()
self.red_transit_network = TransitNetwork()
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
self.red_procurement_requests: List[AircraftProcurementRequest] = []
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.blue_bullseye = Bullseye(Point(0, 0))
self.red_bullseye = Bullseye(Point(0, 0))
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers(self)
self.sanitize_sides()
self.on_load()
self.blue_faker = Faker(self.player_faction.locales)
self.red_faker = Faker(self.enemy_faction.locales)
# Turn 0 procurement. We don't actually have any missions to plan, but
# the planner will tell us what it would like to plan so we can use that
# to drive purchase decisions.
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
self.blue_air_wing = AirWing(self, player=True)
self.red_air_wing = AirWing(self, player=False)
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
self.on_load(game_still_initializing=True)
self.plan_procurement(blue_planner, red_planner)
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.
@@ -144,19 +157,38 @@ class Game:
del state["red_threat_zone"]
del state["blue_navmesh"]
del state["red_navmesh"]
del state["blue_faker"]
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()
def ato_for(self, player: bool) -> AirTaskingOrder:
if player:
return self.blue_ato
return self.red_ato
def procurement_requests_for(
self, player: bool
) -> List[AircraftProcurementRequest]:
if player:
return self.blue_procurement_requests
return self.red_procurement_requests
def transit_network_for(self, player: bool) -> TransitNetwork:
if player:
return self.blue_transit_network
return self.red_transit_network
def generate_conditions(self) -> Conditions:
return Conditions.generate(
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:
@@ -169,44 +201,51 @@ 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
return self.enemy_faction
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 faker_for(self, player: bool) -> Faker:
if player:
return self.blue_faker
return self.red_faker
def _generate_player_event(self, event_class, player_cp, enemy_cp):
def air_wing_for(self, player: bool) -> AirWing:
if player:
return self.blue_air_wing
return self.red_air_wing
def country_for(self, player: bool) -> str:
if player:
return self.player_country
return self.enemy_country
def bullseye_for(self, player: bool) -> Bullseye:
if player:
return self.blue_bullseye
return self.red_bullseye
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):
for front_line in self.theater.conflicts(True):
def _generate_events(self) -> None:
for front_line in self.theater.conflicts():
self._generate_player_event(
FrontlineAttackEvent,
front_line.control_point_a,
front_line.control_point_b,
front_line.blue_cp,
front_line.red_cp,
)
def adjust_budget(self, amount: float, player: bool) -> None:
@@ -215,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)
@@ -238,52 +278,113 @@ 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) -> None:
def on_load(self, game_still_initializing: bool = False) -> None:
if not hasattr(self, "name_generator"):
self.name_generator = naming.namegen
# Hack: Replace the global name generator state with the state from the save
# game.
#
# We need to persist this state so that names generated after game load don't
# conflict with those generated before exit.
naming.namegen = self.name_generator
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position()
self.compute_threat_zones()
if not game_still_initializing:
self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).locales)
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
def reset_ato(self) -> None:
self.blue_ato.clear()
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)
)
self.turn += 1
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.compute_transit_networks()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel them.
self.reset_ato()
for control_point in self.theater.controlpoints:
control_point.process_turn(self)
self.process_enemy_income()
self.blue_air_wing.replenish()
self.red_air_wing.replenish()
self.process_player_income()
if not no_action 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)
self.conditions = self.generate_conditions()
self.process_enemy_income()
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)
with logged_duration("Turn initialization"):
self.initialize_turn()
# 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()
}
@@ -298,73 +399,146 @@ class Game:
return TurnState.CONTINUE
def initialize_turn(self) -> None:
def set_bullseye(self) -> None:
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
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.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 flights & combat for next turn
self.compute_conflicts_position()
self.compute_threat_zones()
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
# 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
self.plan_procurement(blue_planner, red_planner)
def initialize_turn_for(self, player: bool) -> None:
"""Processes coalition-specific turn initialization.
def plan_procurement(
self,
blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner,
) -> None:
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()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
with logged_duration("Transit network identification"):
self.compute_transit_networks()
self.ground_planners = {}
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()
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()
self.plan_procurement_for(player)
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.
ground_portion = 0.3 if self.turn == 0 else 0.5
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,
front_line_budget_share=ground_portion,
).spend_budget(self.budget, blue_planner.procurement_requests)
# aircraft. After that the budget will be spend proportionally based on how much is already invested
self.enemy_budget = ProcurementAi(
self,
for_player=False,
faction=self.enemy_faction,
manage_runways=True,
manage_front_line=True,
manage_aircraft=True,
front_line_budget_share=ground_portion,
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
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))
@@ -377,20 +551,27 @@ 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
"""
self.current_group_id += 1
return self.current_group_id
def compute_transit_networks(self) -> None:
self.blue_transit_network = self.compute_transit_network_for(player=True)
self.red_transit_network = self.compute_transit_network_for(player=False)
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False)
@@ -411,29 +592,21 @@ 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
"""
zones = []
points = []
# By default, use the existing frontline conflict position
for front_line in self.theater.conflicts():
position = Conflict.frontline_position(
front_line.control_point_a, front_line.control_point_b, self.theater
)
position = Conflict.frontline_position(front_line, self.theater)
zones.append(position[0])
zones.append(front_line.control_point_a.position)
zones.append(front_line.control_point_b.position)
zones.append(front_line.blue_cp.position)
zones.append(front_line.red_cp.position)
for cp in self.theater.controlpoints:
# Don't cull missile sites - their range is long enough to make them
# easily culled despite being a threat.
for tgo in cp.ground_objects:
if isinstance(tgo, MissileSiteGroundObject):
points.append(tgo.position)
# If do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier:
if cp.is_carrier or cp.is_lha:
@@ -442,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)
@@ -477,75 +650,58 @@ class Game:
zones.append(Point(0, 0))
self.__culling_zones = zones
self.__culling_points = points
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
:return: True if units can not be added at given position
"""
if self.settings.perf_culling == False:
if not self.settings.perf_culling:
return False
else:
for z in self.__culling_zones:
if (
z.distance_to_point(pos)
< self.settings.perf_culling_distance * 1000
):
return False
for p in self.__culling_points:
if p.distance_to_point(pos) < 2500:
return False
return True
for z in self.__culling_zones:
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
return False
return True
def get_culling_zones(self):
def get_culling_zones(self) -> list[Point]:
"""
Check culling points
:return: List of culling zones
"""
return self.__culling_zones
def get_culling_points(self):
"""
Check culling points
:return: List of culling points
"""
return self.__culling_points
# 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

@@ -6,6 +6,7 @@ from typing import Dict, Iterable, Iterator, Set, Tuple, TYPE_CHECKING, Type
from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from gen.flights.flight import Flight
if TYPE_CHECKING:
@@ -17,9 +18,9 @@ class ControlPointAircraftInventory:
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
self.inventory: Dict[Type[FlyingType], int] = defaultdict(int)
self.inventory: Dict[AircraftType, int] = defaultdict(int)
def add_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
def add_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Adds aircraft to the inventory.
Args:
@@ -28,7 +29,7 @@ class ControlPointAircraftInventory:
"""
self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: Type[FlyingType], count: int) -> None:
def remove_aircraft(self, aircraft: AircraftType, count: int) -> None:
"""Removes aircraft from the inventory.
Args:
@@ -42,12 +43,12 @@ class ControlPointAircraftInventory:
available = self.inventory[aircraft]
if available < count:
raise ValueError(
f"Cannot remove {count} {aircraft.id} from "
f"Cannot remove {count} {aircraft} from "
f"{self.control_point.name}. Only have {available}."
)
self.inventory[aircraft] -= count
def available(self, aircraft: Type[FlyingType]) -> int:
def available(self, aircraft: AircraftType) -> int:
"""Returns the number of available aircraft of the given type.
Args:
@@ -59,14 +60,14 @@ class ControlPointAircraftInventory:
return 0
@property
def types_available(self) -> Iterator[Type[FlyingType]]:
def types_available(self) -> Iterator[AircraftType]:
"""Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft
@property
def all_aircraft(self) -> Iterator[Tuple[Type[FlyingType], int]]:
def all_aircraft(self) -> Iterator[Tuple[AircraftType, int]]:
"""Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items():
if count > 0:
@@ -107,9 +108,9 @@ class GlobalAircraftInventory:
return self.inventories[control_point]
@property
def available_types_for_player(self) -> Iterator[Type[FlyingType]]:
def available_types_for_player(self) -> Iterator[AircraftType]:
"""Iterates over all aircraft types available to the player."""
seen: Set[Type[FlyingType]] = set()
seen: Set[AircraftType] = set()
for control_point, inventory in self.inventories.items():
if control_point.captured:
for aircraft in inventory.types_available:

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,13 +0,0 @@
from game.theater import ControlPoint
class FrontlineData:
"""
This Data structure will store information about an existing frontline
"""
def __init__(self, from_cp: ControlPoint, to_cp: ControlPoint):
self.to_cp = to_cp
self.from_cp = from_cp
self.enemy_units_position = []
self.blue_units_position = []

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

@@ -103,7 +103,7 @@ class NavMesh:
# currently.
p = ShapelyPoint(point.x, point.y)
for navpoly in self.polys:
if navpoly.poly.contains(p):
if navpoly.poly.intersects(p):
return navpoly
return None

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
from game.theater.theatergroundobject import TheaterGroundObject
import logging
import os
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, List, Optional, Set
from typing import Iterable, List, Set, TYPE_CHECKING, cast
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@@ -14,14 +13,18 @@ from dcs.lua.parse import loads
from dcs.mapping import Point
from dcs.translation import String
from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.aircraft import AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
from gen.cargoshipgen import CargoShipGenerator
from gen.convoygen import ConvoyGenerator
from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
@@ -30,9 +33,8 @@ from gen.naming import namegen
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from .. import db
from ..theater import Airfield
from ..theater import Airfield, FrontLine
from ..unitmap import UnitMap
if TYPE_CHECKING:
@@ -42,18 +44,13 @@ if TYPE_CHECKING:
class Operation:
"""Static class for managing the final Mission generation"""
current_mission = None # type: Mission
airgen = None # type: AircraftConflictGenerator
triggersgen = None # type: TriggersGenerator
airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None
game = None # type: Game
environment_settings = None
current_mission: Mission
airgen: AircraftConflictGenerator
airsupportgen: AirSupportConflictGenerator
groundobjectgen: GroundObjectsGenerator
radio_registry: RadioRegistry
tacan_registry: TacanRegistry
game: Game
trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None
player_awacs_enabled = True
@@ -65,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))
@@ -73,21 +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.control_point_a,
frontline.control_point_b,
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
@@ -98,12 +80,11 @@ class Operation:
)
return Conflict(
cls.game.theater,
player_cp,
enemy_cp,
cls.game.player_name,
cls.game.enemy_name,
cls.game.player_country,
cls.game.enemy_country,
FrontLine(player_cp, enemy_cp),
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,
)
@@ -112,9 +93,13 @@ class Operation:
cls.current_mission = mission
@classmethod
def _setup_mission_coalitions(cls):
cls.current_mission.coalition["blue"] = Coalition("blue")
cls.current_mission.coalition["red"] = Coalition("red")
def _setup_mission_coalitions(cls) -> None:
cls.current_mission.coalition["blue"] = Coalition(
"blue", bullseye=cls.game.blue_bullseye.to_pydcs()
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red_bullseye.to_pydcs()
)
p_country = cls.game.player_country
e_country = cls.game.enemy_country
@@ -164,7 +149,7 @@ class Operation:
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator,
):
) -> None:
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [
@@ -176,13 +161,16 @@ class Operation:
gen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker)
if tanker.blue:
gen.add_tanker(tanker)
for aewc in airsupportgen.air_support.awacs:
gen.add_awacs(aewc)
if aewc.blue:
gen.add_awacs(aewc)
for jtac in jtacs:
gen.add_jtac(jtac)
if jtac.blue:
gen.add_jtac(jtac)
for flight in airgen.flights:
gen.add_flight(flight)
@@ -213,23 +201,7 @@ class Operation:
for flight in flights:
if not flight.client_units:
continue
cls.assign_channels_to_flight(flight, air_support)
@staticmethod
def assign_channels_to_flight(flight: FlightData, air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)
flight.aircraft_type.assign_channels_for_flight(flight, air_support)
@classmethod
def _create_tacan_registry(
@@ -265,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,
@@ -280,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)
@@ -308,6 +285,7 @@ class Operation:
# Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls._generate_ground_units()
cls._generate_transports()
cls._generate_destroyed_units()
cls._generate_air_units()
cls.assign_channels_to_flights(
@@ -321,13 +299,8 @@ class Operation:
# Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
if cls.game.player_country in [
country.name
for country in cls.current_mission.coalition["blue"].countries.values()
]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
cls.current_mission.groundControl.blue_observer = 1
# Options
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
@@ -377,6 +350,7 @@ class Operation:
cls.game.settings,
cls.game,
cls.radio_registry,
cls.tacan_registry,
cls.unit_map,
air_support=cls.airsupportgen.air_support,
)
@@ -401,16 +375,16 @@ class Operation:
@classmethod
def _generate_ground_conflicts(cls) -> None:
"""For each frontline in the Operation, generate the ground conflicts and JTACs"""
for front_line in cls.game.theater.conflicts(True):
player_cp = front_line.control_point_a
enemy_cp = front_line.control_point_b
cls.jtacs = []
for front_line in cls.game.theater.conflicts():
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),
player_cp,
enemy_cp,
front_line,
cls.game.theater,
)
# Generate frontline ops
@@ -429,7 +403,13 @@ class Operation:
cls.jtacs.extend(ground_conflict_gen.jtacs)
@classmethod
def reset_naming_ids(cls):
def _generate_transports(cls) -> None:
"""Generates convoys for unit transfers by road."""
ConvoyGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
@classmethod
def reset_naming_ids(cls) -> None:
namegen.reset_numbers()
@classmethod
@@ -450,32 +430,31 @@ class Operation:
"BlueAA": {},
} # type: ignore
for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.dcsGroupName,
for i, tanker in enumerate(airsupportgen.air_support.tankers):
luaData["Tankers"][i] = {
"dcsGroupName": tanker.group_name,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"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.dcsGroupName,
"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] = {
"dcsGroupName": jtac.dcsGroupName,
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,
@@ -496,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": {
@@ -504,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:
@@ -589,8 +569,7 @@ class Operation:
zone = data["zone"]
laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone={repr(zone)}, laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
lua += "}"
# Process the Target Points

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

@@ -3,22 +3,24 @@ from __future__ import annotations
import math
import random
from dataclasses import dataclass
from typing import Iterator, List, Optional, TYPE_CHECKING, Type
from dcs.unittype import FlyingType, VehicleType
from typing import Iterator, List, Optional, TYPE_CHECKING, Tuple
from game import db
from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.factions.faction import Faction
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance
from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
if TYPE_CHECKING:
from game import Game
FRONTLINE_RESERVES_FACTOR = 1.3
@dataclass(frozen=True)
class AircraftProcurementRequest:
@@ -43,27 +45,55 @@ class ProcurementAi:
manage_runways: bool,
manage_front_line: bool,
manage_aircraft: bool,
front_line_budget_share: float,
) -> None:
if front_line_budget_share > 1.0:
raise ValueError
self.game = game
self.is_player = for_player
self.air_wing = game.air_wing_for(for_player)
self.faction = faction
self.manage_runways = manage_runways
self.manage_front_line = manage_front_line
self.manage_aircraft = manage_aircraft
self.front_line_budget_share = front_line_budget_share
self.threat_zones = self.game.threat_zone_for(not self.is_player)
def spend_budget(
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
) -> float:
def calculate_ground_unit_budget_share(self) -> float:
armor_investment = 0
aircraft_investment = 0
# faction has no ground units
if (
len(self.faction.artillery_units) == 0
and len(self.faction.frontline_units) == 0
):
return 0
# faction has no planes
if len(self.faction.aircrafts) == 0:
return 1
for cp in self.owned_points:
cp_ground_units = cp.allocated_ground_units(self.game.transfers)
armor_investment += cp_ground_units.total_value
cp_aircraft = cp.allocated_aircraft(self.game)
aircraft_investment += cp_aircraft.total_value
total_investment = aircraft_investment + armor_investment
if total_investment == 0:
# Turn 0 or all units were destroyed. Either way, split 30/70.
return 0.3
# the more planes we have, the more ground units we want and vice versa
ground_unit_share = aircraft_investment / total_investment
if ground_unit_share > 1.0:
raise ValueError
return ground_unit_share
def spend_budget(self, budget: float) -> float:
if self.manage_runways:
budget = self.repair_runways(budget)
if self.manage_front_line:
armor_budget = math.ceil(budget * self.front_line_budget_share)
armor_budget = budget * self.calculate_ground_unit_budget_share()
budget -= armor_budget
budget += self.reinforce_front_line(armor_budget)
@@ -72,20 +102,20 @@ class ProcurementAi:
if not self.is_player:
budget += self.sell_incomplete_squadrons()
if self.manage_aircraft:
budget = self.purchase_aircraft(budget, aircraft_requests)
budget = self.purchase_aircraft(budget)
return budget
def sell_incomplete_squadrons(self) -> float:
# Selling incomplete squadrons gives us more money to spend on the next
# turn. This serves as a short term fix for
# https://github.com/Khopa/dcs_liberation/issues/41.
# https://github.com/dcs-liberation/dcs_liberation/issues/41.
#
# Only incomplete squadrons which are unlikely to get used will be sold
# rather than all unused aircraft because the unused aircraft are what
# make OCA strikes worthwhile.
#
# This option is only used by the AI since players cannot cancel sales
# (https://github.com/Khopa/dcs_liberation/issues/365).
# (https://github.com/dcs-liberation/dcs_liberation/issues/365).
total = 0.0
for cp in self.game.theater.control_points_for(self.is_player):
inventory = self.game.aircraft_inventory.for_control_point(cp)
@@ -95,7 +125,7 @@ class ProcurementAi:
if available % 2 == 0:
continue
inventory.remove_aircraft(aircraft, 1)
total += db.PRICES[aircraft]
total += aircraft.price
return total
def repair_runways(self, budget: float) -> float:
@@ -115,28 +145,19 @@ class ProcurementAi:
)
return budget
def random_affordable_ground_unit(
self, budget: float, cp: ControlPoint
) -> Optional[Type[VehicleType]]:
affordable_units = [
u
for u in self.faction.frontline_units + self.faction.artillery_units
if db.PRICES[u] <= budget
]
total_number_aa = (
cp.base.total_frontline_aa + cp.pending_frontline_aa_deliveries_count
def affordable_ground_unit_of_class(
self, budget: float, unit_class: GroundUnitClass
) -> Optional[GroundUnitType]:
faction_units = set(self.faction.frontline_units) | set(
self.faction.artillery_units
)
total_non_aa = (
cp.base.total_armor + cp.pending_deliveries_count - total_number_aa
)
max_aa = math.ceil(total_non_aa / 8)
of_class = {u for u in faction_units if u.unit_class is unit_class}
# Limit the number of AA units the AI will buy
if not total_number_aa < max_aa:
for unit in [u for u in affordable_units if u in TYPE_SHORAD]:
affordable_units.remove(unit)
# faction has no access to needed unit type, take a random unit
if not of_class:
of_class = faction_units
affordable_units = [u for u in of_class if u.price <= budget]
if not affordable_units:
return None
return random.choice(affordable_units)
@@ -145,39 +166,74 @@ class ProcurementAi:
if not self.faction.frontline_units and not self.faction.artillery_units:
return budget
# TODO: Attempt to transfer from reserves.
while budget > 0:
candidates = self.front_line_candidates()
if not candidates:
cp = self.ground_reinforcement_candidate()
if cp is None:
break
cp = random.choice(candidates)
unit = self.random_affordable_ground_unit(budget, cp)
most_needed_type = self.most_needed_unit_class(cp)
unit = self.affordable_ground_unit_of_class(budget, most_needed_type)
if unit is None:
# Can't afford any more units.
break
budget -= db.PRICES[unit]
budget -= unit.price
cp.pending_unit_deliveries.order({unit: 1})
return budget
def _affordable_aircraft_of_types(
def most_needed_unit_class(self, cp: ControlPoint) -> GroundUnitClass:
worst_balanced: Optional[GroundUnitClass] = None
worst_fulfillment = math.inf
for unit_class in GroundUnitClass:
if not self.faction.has_access_to_unittype(unit_class):
continue
current_ratio = self.cost_ratio_of_ground_unit(cp, unit_class)
desired_ratio = (
self.faction.doctrine.ground_unit_procurement_ratios.for_unit_class(
unit_class
)
)
if not desired_ratio:
continue
if current_ratio >= desired_ratio:
continue
fulfillment = current_ratio / desired_ratio
if fulfillment < worst_fulfillment:
worst_fulfillment = fulfillment
worst_balanced = unit_class
if worst_balanced is None:
return GroundUnitClass.Tank
return worst_balanced
def _affordable_aircraft_for_task(
self,
types: List[Type[FlyingType]],
task: FlightType,
airbase: ControlPoint,
number: int,
max_price: float,
) -> Optional[Type[FlyingType]]:
best_choice: Optional[Type[FlyingType]] = None
for unit in [u for u in self.faction.aircrafts if u in types]:
if db.PRICES[unit] * number > max_price:
) -> Optional[AircraftType]:
best_choice: Optional[AircraftType] = None
for unit in aircraft_for_task(task):
if unit not in self.faction.aircrafts:
continue
if unit.price * number > max_price:
continue
if not airbase.can_operate(unit):
continue
# Affordable and compatible. To keep some variety, skip with a 50/50
# chance. Might be a good idea to have the chance to skip based on
# the price compared to the rest of the choices.
for squadron in self.air_wing.squadrons_for(unit):
if task in squadron.auto_assignable_mission_types:
break
else:
continue
# Affordable, compatible, and we have a squadron capable of the task. To
# keep some variety, skip with a 50/50 chance. Might be a good idea to have
# the chance to skip based on the price compared to the rest of the choices.
best_choice = unit
if random.choice([True, False]):
break
@@ -185,28 +241,42 @@ class ProcurementAi:
def affordable_aircraft_for(
self, request: AircraftProcurementRequest, airbase: ControlPoint, budget: float
) -> Optional[Type[FlyingType]]:
return self._affordable_aircraft_of_types(
aircraft_for_task(request.task_capability), airbase, request.number, budget
) -> Optional[AircraftType]:
return self._affordable_aircraft_for_task(
request.task_capability, airbase, request.number, budget
)
def purchase_aircraft(
self, budget: float, aircraft_requests: List[AircraftProcurementRequest]
) -> float:
for request in aircraft_requests:
for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget)
if unit is None:
# Can't afford any aircraft capable of performing the
# required mission that can operate from this airbase. We
# might be able to afford aircraft at other airbases though,
# in the case where the airbase we attempted to use is only
# able to operate expensive aircraft.
continue
def fulfill_aircraft_request(
self, request: AircraftProcurementRequest, budget: float
) -> Tuple[float, bool]:
for airbase in self.best_airbases_for(request):
unit = self.affordable_aircraft_for(request, airbase, budget)
if unit is None:
# Can't afford any aircraft capable of performing the
# required mission that can operate from this airbase. We
# might be able to afford aircraft at other airbases though,
# in the case where the airbase we attempted to use is only
# able to operate expensive aircraft.
continue
budget -= db.PRICES[unit] * request.number
airbase.pending_unit_deliveries.order({unit: request.number})
budget -= unit.price * request.number
airbase.pending_unit_deliveries.order({unit: request.number})
return budget, True
return budget, False
def purchase_aircraft(self, budget: float) -> float:
for request in self.game.procurement_requests_for(self.is_player):
if not list(self.best_airbases_for(request)):
# No airbases in range of this request. Skip it.
continue
budget, fulfilled = self.fulfill_aircraft_request(request, budget)
if not fulfilled:
# The request was not fulfilled because we could not afford any suitable
# aircraft. Rather than continuing, which could proceed to buy tons of
# cheap escorts that will never allow us to plan a strike package, stop
# buying so we can save the budget until a turn where we *can* afford to
# fill the package.
break
return budget
@property
@@ -221,11 +291,9 @@ class ProcurementAi:
) -> Iterator[ControlPoint]:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near)
threatened = []
for cp in distance_cache.airfields_within(request.range):
for cp in distance_cache.operational_airfields_within(request.range):
if not cp.is_friendly(self.is_player):
continue
if not cp.runway_is_operational():
continue
if cp.unclaimed_parking(self.game) < request.number:
continue
if self.threat_zones.threatened(cp.position):
@@ -233,37 +301,69 @@ class ProcurementAi:
yield cp
yield from threatened
def front_line_candidates(self) -> List[ControlPoint]:
candidates = []
def ground_reinforcement_candidate(self) -> Optional[ControlPoint]:
worst_supply = math.inf
understaffed: Optional[ControlPoint] = None
# Prefer to buy front line units at active front lines that are not
# already overloaded.
for cp in self.owned_points:
if cp.expected_ground_units_next_turn.total >= 30:
if not cp.has_active_frontline:
continue
if not cp.has_ground_unit_source(self.game):
# No source of ground units, so can't buy anything.
continue
purchase_target = cp.frontline_unit_count_limit * FRONTLINE_RESERVES_FACTOR
allocated = cp.allocated_ground_units(self.game.transfers)
if allocated.total >= purchase_target:
# Control point is already sufficiently defended.
continue
for connected in cp.connected_points:
if not connected.is_friendly(to_player=self.is_player):
candidates.append(cp)
if allocated.total < worst_supply:
worst_supply = allocated.total
understaffed = cp
if not candidates:
# Otherwise buy reserves, but don't exceed 10 reserve units per CP.
# These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP.
#
# To avoid sudden overwhelming numbers of units we avoid buying
# many.
#
# Also, do not bother buying units at bases that will never connect
# to a front line.
for cp in self.owned_points:
if not cp.can_deploy_ground_units:
continue
if cp.expected_ground_units_next_turn.total >= 10:
continue
if cp.is_global:
continue
candidates.append(cp)
if understaffed is not None:
return understaffed
return candidates
# Otherwise buy reserves, but don't exceed the amount defined in the settings.
# These units do not exist in the world until the CP becomes
# connected to an active front line, at which point all these units
# will suddenly appear at the gates of the newly captured CP.
#
# To avoid sudden overwhelming numbers of units we avoid buying
# many.
#
# Also, do not bother buying units at bases that will never connect
# to a front line.
for cp in self.owned_points:
if cp.is_global:
continue
if not cp.can_recruit_ground_units(self.game):
continue
allocated = cp.allocated_ground_units(self.game.transfers)
if allocated.total >= self.game.settings.reserves_procurement_target:
continue
if allocated.total < worst_supply:
worst_supply = allocated.total
understaffed = cp
return understaffed
def cost_ratio_of_ground_unit(
self, control_point: ControlPoint, unit_class: GroundUnitClass
) -> float:
allocations = control_point.allocated_ground_units(self.game.transfers)
class_cost = 0
total_cost = 0
for unit_type, count in allocations.all.items():
cost = unit_type.price * count
total_cost += cost
if unit_type.unit_class is unit_class:
class_cost += cost
if not total_cost:
return 0
return class_cost / total_cost

41
game/profiling.py Normal file
View File

@@ -0,0 +1,41 @@
from __future__ import annotations
import logging
import timeit
from collections import defaultdict
from contextlib import contextmanager
from datetime import timedelta
from types import TracebackType
from typing import Iterator, Optional, Type
@contextmanager
def logged_duration(event: str) -> Iterator[None]:
start = timeit.default_timer()
yield
end = timeit.default_timer()
logging.debug("%s took %s", event, timedelta(seconds=end - start))
class MultiEventTracer:
def __init__(self) -> None:
self.events: dict[str, timedelta] = defaultdict(timedelta)
def __enter__(self) -> MultiEventTracer:
return self
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)
@contextmanager
def trace(self, event: str) -> Iterator[None]:
start = timeit.default_timer()
yield
end = timeit.default_timer()
self.events[event] += timedelta(seconds=end - start)

298
game/radio/channels.py Normal file
View File

@@ -0,0 +1,298 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Any, TYPE_CHECKING
if TYPE_CHECKING:
from gen import FlightData, AirSupport
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> RadioChannelAllocator:
return cls()
@classmethod
def name(cls) -> str:
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
if self.intra_flight_radio_index is not None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel
)
if self.inter_flight_radio_index is None:
return
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
if flight.departure.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
if flight.arrival != flight.departure and flight.arrival.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
flight.assign_channel(radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None and flight.divert.atc is not None:
flight.assign_channel(radio_id, next(channel_alloc), flight.divert.atc)
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass
@classmethod
def from_cfg(cls, cfg: dict[str, Any]) -> CommonRadioChannelAllocator:
return CommonRadioChannelAllocator(
inter_flight_radio_index=cfg["inter_flight_radio_index"],
intra_flight_radio_index=cfg["intra_flight_radio_index"],
)
@classmethod
def name(cls) -> str:
return "common"
@dataclass(frozen=True)
class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
pass
@classmethod
def name(cls) -> str:
return "noop"
@dataclass(frozen=True)
class FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
# TODO: Assign 2 and 3 to AWACS if it is VHF.
@classmethod
def name(cls) -> str:
return "farmer"
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. The aircraft automatically configures channels for every
# allied flight in the game (including AWACS) and for every airfield. As
# such, we don't need to allocate any of those. There are seven presets
# we can modify, however: three channels for the main radio intended for
# communication with wingmen, and four emergency channels for the backup
# radio. We'll set the first channel of the main radio to the
# intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 4, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@classmethod
def name(cls) -> str:
return "viggen"
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(
self, flight: FlightData, air_support: AirSupport
) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
if flight.departure.atc is not None:
flight.assign_channel(radio_id, 2, flight.departure.atc)
if flight.arrival.atc is not None:
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@classmethod
def name(cls) -> str:
return "SCR-522"
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "default"
class SingleRadioChannelNamer(ChannelNamer):
"""Channel namer for the aircraft with only a single radio.
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
it's not necessary for us to name the radio when naming the channel.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "single"
class HueyChannelNamer(ChannelNamer):
"""Channel namer for the UH-1H."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM3 Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "huey"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "mirage"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "tomcat"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id >= 4:
channel_letter = "EFGH"[channel_id - 4]
return f"FR 24 {channel_letter}"
return f"FR 22 Special {channel_id}"
@classmethod
def name(cls) -> str:
return "viggen"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
@classmethod
def name(cls) -> str:
return "viper"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@classmethod
def name(cls) -> str:
return "SCR-522"

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

89
game/scenery_group.py Normal file
View File

@@ -0,0 +1,89 @@
from __future__ import annotations
from game.theater.theatergroundobject import NAME_BY_CATEGORY
from dcs.triggers import TriggerZone
from typing import Iterable, List
class SceneryGroupError(RuntimeError):
"""Error for when there are insufficient conditions to create a SceneryGroup."""
pass
class SceneryGroup:
"""Store information about a scenery objective."""
def __init__(
self, zone_def: TriggerZone, zones: Iterable[TriggerZone], category: str
) -> None:
self.zone_def = zone_def
self.zones = zones
self.position = zone_def.position
self.category = category
@staticmethod
def from_trigger_zones(trigger_zones: Iterable[TriggerZone]) -> List[SceneryGroup]:
"""Define scenery objectives based on their encompassing blue/red circle."""
zone_definitions = []
white_zones = []
scenery_groups = []
# Aggregate trigger zones into different groups based on color.
for zone in trigger_zones:
if SceneryGroup.is_blue(zone):
zone_definitions.append(zone)
if SceneryGroup.is_white(zone):
white_zones.append(zone)
# For each objective definition.
for zone_def in zone_definitions:
zone_def_radius = zone_def.radius
zone_def_position = zone_def.position
zone_def_name = zone_def.name
if len(zone_def.properties) == 0:
raise SceneryGroupError(
"Undefined SceneryGroup category in TriggerZone: " + zone_def_name
)
# Arbitrary campaign design requirement: First property must define the category.
zone_def_category = zone_def.properties[1].get("value").lower()
valid_white_zones = []
for zone in list(white_zones):
if zone.position.distance_to_point(zone_def_position) < zone_def_radius:
valid_white_zones.append(zone)
white_zones.remove(zone)
if len(valid_white_zones) > 0 and zone_def_category in NAME_BY_CATEGORY:
scenery_groups.append(
SceneryGroup(zone_def, valid_white_zones, zone_def_category)
)
elif len(valid_white_zones) == 0:
raise SceneryGroupError(
"No white triggerzones found in: " + zone_def_name
)
elif zone_def_category not in NAME_BY_CATEGORY:
raise SceneryGroupError(
"Incorrect TriggerZone category definition for: "
+ zone_def_name
+ " in campaign definition. TriggerZone category: "
+ zone_def_category
)
return scenery_groups
@staticmethod
def is_blue(zone: TriggerZone) -> bool:
# Blue in RGB is [0 Red], [0 Green], [1 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 0 and zone.color[2] == 0 and zone.color[3] == 1
@staticmethod
def is_white(zone: TriggerZone) -> bool:
# White in RGB is [1 Red], [1 Green], [1 Blue]. Ignore the fourth position: Transparency.
return zone.color[1] == 1 and zone.color[2] == 1 and zone.color[3] == 1

View File

@@ -1,15 +1,26 @@
from dataclasses import dataclass, field
from typing import Dict, Optional
from datetime import timedelta
from enum import Enum, unique
from typing import Dict, Optional, Any
from dcs.forcedoptions import ForcedOptions
@unique
class AutoAtoBehavior(Enum):
Disabled = "Disabled"
Never = "Never assign player pilots"
Default = "No preference"
Prefer = "Prefer player pilots"
@dataclass
class Settings:
# Difficulty settings
player_skill: str = "Good"
enemy_skill: str = "Average"
ai_pilot_levelling: bool = True
enemy_vehicle_skill: str = "Average"
map_coalition_visibility: ForcedOptions.Views = ForcedOptions.Views.All
labels: str = "Full"
@@ -23,23 +34,44 @@ 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 = 12
#: The number of pilots a squadron can replace per turn.
squadron_replenishment_rate: int = 4
default_start_type: str = "Cold"
# Mission specific
desired_player_mission_duration: timedelta = timedelta(minutes=60)
# Campaign management
automate_runway_repair: bool = False
automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = False
disable_legacy_aewc: bool = True
disable_legacy_tanker: bool = True
generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
auto_ato_player_missions_asap: bool = True
# Performance oriented
perf_red_alert_state: bool = True
perf_smoke_gen: bool = True
perf_smoke_spacing = 1600
perf_artillery: bool = True
perf_moving_units: bool = True
perf_infantry: bool = True
perf_destroyed_units: bool = True
reserves_procurement_target: int = 10
# Performance culling
perf_culling: bool = False
@@ -72,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

456
game/squadrons.py Normal file
View File

@@ -0,0 +1,456 @@
from __future__ import annotations
import itertools
import logging
import random
from collections import defaultdict
from dataclasses import dataclass, field
from enum import unique, Enum
from pathlib import Path
from typing import (
Tuple,
TYPE_CHECKING,
Optional,
Iterator,
Sequence,
Any,
)
import yaml
from faker import Faker
from game.dcs.aircrafttype import AircraftType
from game.settings import AutoAtoBehavior
if TYPE_CHECKING:
from game import Game
from gen.flights.flight import FlightType
@dataclass
class PilotRecord:
missions_flown: int = field(default=0)
@unique
class PilotStatus(Enum):
Active = "Active"
OnLeave = "On leave"
Dead = "Dead"
@dataclass
class Pilot:
name: str
player: bool = field(default=False)
status: PilotStatus = field(default=PilotStatus.Active)
record: PilotRecord = field(default_factory=PilotRecord)
@property
def alive(self) -> bool:
return self.status is not PilotStatus.Dead
@property
def on_leave(self) -> bool:
return self.status is PilotStatus.OnLeave
def send_on_leave(self) -> None:
if self.status is not PilotStatus.Active:
raise RuntimeError("Only active pilots may be sent on leave")
self.status = PilotStatus.OnLeave
def return_from_leave(self) -> None:
if self.status is not PilotStatus.OnLeave:
raise RuntimeError("Only pilots on leave may be returned from leave")
self.status = PilotStatus.Active
def kill(self) -> None:
self.status = PilotStatus.Dead
@classmethod
def random(cls, faker: Faker) -> Pilot:
return Pilot(faker.name())
@dataclass
class Squadron:
name: str
nickname: Optional[str]
country: str
role: str
aircraft: AircraftType
livery: Optional[str]
mission_types: tuple[FlightType, ...]
#: The pool of pilots that have not yet been assigned to the squadron. This only
#: happens when a preset squadron defines more preset pilots than the squadron limit
#: allows. This pool will be consumed before random pilots are generated.
pilot_pool: list[Pilot]
current_roster: list[Pilot] = field(default_factory=list, init=False, hash=False)
available_pilots: list[Pilot] = field(
default_factory=list, init=False, hash=False, compare=False
)
auto_assignable_mission_types: set[FlightType] = field(
init=False, hash=False, compare=False
)
# We need a reference to the Game so that we can access the Faker without needing to
# persist it to the save game, or having to reconstruct it (it's not cheap) each
# time we create or load a squadron.
game: Game = field(hash=False, compare=False)
player: bool
def __post_init__(self) -> None:
if any(p.status is not PilotStatus.Active for p in self.pilot_pool):
raise ValueError("Squadrons can only be created with active pilots.")
self._recruit_pilots(self.game.settings.squadron_pilot_limit)
self.auto_assignable_mission_types = set(self.mission_types)
def __str__(self) -> str:
if self.nickname is None:
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 self.claim_new_pilot_if_allowed()
# For opfor, so player/AI option is irrelevant.
if not self.player:
return self.available_pilots.pop()
preference = self.game.settings.auto_ato_behavior
# No preference, so the first pilot is fine.
if preference is AutoAtoBehavior.Default:
return self.available_pilots.pop()
prefer_players = preference is AutoAtoBehavior.Prefer
for pilot in self.available_pilots:
if pilot.player == prefer_players:
self.available_pilots.remove(pilot)
return pilot
# No pilot was found that matched the user's preference.
#
# If they chose to *never* assign players and only players remain in the pool,
# we cannot fill the slot with the available pilots.
#
# If they only *prefer* players and we're out of players, just return an AI
# pilot.
if not prefer_players:
return self.claim_new_pilot_if_allowed()
return self.available_pilots.pop()
def claim_pilot(self, pilot: Pilot) -> None:
if pilot not in self.available_pilots:
raise ValueError(
f"Cannot assign {pilot} to {self} because they are not available"
)
self.available_pilots.remove(pilot)
def return_pilot(self, pilot: Pilot) -> None:
self.available_pilots.append(pilot)
def return_pilots(self, pilots: Sequence[Pilot]) -> None:
# Return in reverse so that returning two pilots and then getting two more
# results in the same ordering. This happens commonly when resetting rosters in
# the UI, when we clear the roster because the UI is updating, then end up
# repopulating the same size flight from the same squadron.
self.available_pilots.extend(reversed(pilots))
def _recruit_pilots(self, count: int) -> None:
new_pilots = self.pilot_pool[:count]
self.pilot_pool = self.pilot_pool[count:]
count -= len(new_pilots)
new_pilots.extend([Pilot(self.faker.name()) for _ in range(count)])
self.current_roster.extend(new_pilots)
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,
)
if replenish_count > 0:
self._recruit_pilots(replenish_count)
def return_all_pilots(self) -> None:
self.available_pilots = list(self.active_pilots)
@staticmethod
def send_on_leave(pilot: Pilot) -> None:
pilot.send_on_leave()
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"
)
pilot.return_from_leave()
@property
def faker(self) -> Faker:
return self.game.faker_for(self.player)
def _pilots_with_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.current_roster if p.status == status]
def _pilots_without_status(self, status: PilotStatus) -> list[Pilot]:
return [p for p in self.current_roster if p.status != status]
@property
def active_pilots(self) -> list[Pilot]:
return self._pilots_with_status(PilotStatus.Active)
@property
def pilots_on_leave(self) -> list[Pilot]:
return self._pilots_with_status(PilotStatus.OnLeave)
@property
def number_of_pilots_including_inactive(self) -> int:
return len(self.current_roster)
@property
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 not self.pilot_limits_enabled or bool(self.available_pilots)
@property
def has_unfilled_pilot_slots(self) -> bool:
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
def pilot_at_index(self, index: int) -> Pilot:
return self.current_roster[index]
@classmethod
def from_yaml(cls, path: Path, game: Game, player: bool) -> Squadron:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
from gen.flights.flight import FlightType
with path.open(encoding="utf8") as squadron_file:
data = yaml.safe_load(squadron_file)
name = data["aircraft"]
try:
unit_type = AircraftType.named(name)
except KeyError as ex:
raise KeyError(f"Could not find any aircraft named {name}") from ex
pilots = [Pilot(n, player=False) for n in data.get("pilots", [])]
pilots.extend([Pilot(n, player=True) for n in data.get("players", [])])
mission_types = [FlightType.from_name(n) for n in data["mission_types"]]
tasks = tasks_for_aircraft(unit_type)
for mission_type in list(mission_types):
if mission_type not in tasks:
logging.error(
f"Squadron has mission type {mission_type} but {unit_type} is not "
f"capable of that task: {path}"
)
mission_types.remove(mission_type)
return Squadron(
name=data["name"],
nickname=data.get("nickname"),
country=data["country"],
role=data["role"],
aircraft=unit_type,
livery=data.get("livery"),
mission_types=tuple(mission_types),
pilot_pool=pilots,
game=game,
player=player,
)
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"])
self.__dict__.update(state)
class SquadronLoader:
def __init__(self, game: Game, player: bool) -> None:
self.game = game
self.player = player
@staticmethod
def squadron_directories() -> Iterator[Path]:
from game import persistency
yield Path(persistency.base_path()) / "Liberation/Squadrons"
yield Path("resources/squadrons")
def load(self) -> dict[AircraftType, list[Squadron]]:
squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list)
country = self.game.country_for(self.player)
faction = self.game.faction_for(self.player)
any_country = country.startswith("Combined Joint Task Forces ")
for directory in self.squadron_directories():
for path, squadron in self.load_squadrons_from(directory):
if not any_country and squadron.country != country:
logging.debug(
"Not using squadron for non-matching country (is "
f"{squadron.country}, need {country}: {path}"
)
continue
if squadron.aircraft not in faction.aircrafts:
logging.debug(
f"Not using squadron because {faction.name} cannot use "
f"{squadron.aircraft}: {path}"
)
continue
logging.debug(
f"Found {squadron.name} {squadron.aircraft} {squadron.role} "
f"compatible with {faction.name}"
)
squadrons[squadron.aircraft].append(squadron)
# Convert away from defaultdict because defaultdict doesn't unpickle so we don't
# want it in the save state.
return dict(squadrons)
def load_squadrons_from(self, directory: Path) -> Iterator[Tuple[Path, Squadron]]:
logging.debug(f"Looking for factions in {directory}")
# First directory level is the aircraft type so that historical squadrons that
# have flown multiple airframes can be defined as many times as needed. The main
# load() method is responsible for filtering out squadrons that aren't
# compatible with the faction.
for squadron_path in directory.glob("*/*.yaml"):
try:
yield squadron_path, Squadron.from_yaml(
squadron_path, self.game, self.player
)
except Exception as ex:
raise RuntimeError(
f"Failed to load squadron defined by {squadron_path}"
) from ex
class AirWing:
def __init__(self, game: Game, player: bool) -> None:
from gen.flights.ai_flight_planner_db import tasks_for_aircraft
self.game = game
self.player = player
self.squadrons = SquadronLoader(game, player).load()
count = itertools.count(1)
for aircraft in game.faction_for(player).aircrafts:
if aircraft in self.squadrons:
continue
self.squadrons[aircraft] = [
Squadron(
name=f"Squadron {next(count):03}",
nickname=self.random_nickname(),
country=game.country_for(player),
role="Flying Squadron",
aircraft=aircraft,
livery=None,
mission_types=tuple(tasks_for_aircraft(aircraft)),
pilot_pool=[],
game=game,
player=player,
)
]
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):
yield squadron
def auto_assignable_for_task_with_type(
self, aircraft: AircraftType, task: FlightType
) -> Iterator[Squadron]:
for squadron in self.squadrons_for(aircraft):
if squadron.can_auto_assign(task) and squadron.has_available_pilots:
yield squadron
def squadron_for(self, aircraft: AircraftType) -> Squadron:
return self.squadrons_for(aircraft)[0]
def iter_squadrons(self) -> Iterator[Squadron]:
return itertools.chain.from_iterable(self.squadrons.values())
def squadron_at_index(self, index: int) -> Squadron:
return list(self.iter_squadrons())[index]
def replenish(self) -> None:
for squadron in self.iter_squadrons():
squadron.replenish_lost_pilots()
def reset(self) -> None:
for squadron in self.iter_squadrons():
squadron.return_all_pilots()
@property
def size(self) -> int:
return sum(len(s) for s in self.squadrons.values())
@staticmethod
def _make_random_nickname() -> str:
from gen.naming import ANIMALS
animal = random.choice(ANIMALS)
adjective = random.choice(
(
None,
"Red",
"Blue",
"Green",
"Golden",
"Black",
"Fighting",
"Flying",
)
)
if adjective is None:
return animal.title()
return f"{adjective} {animal}".title()
def random_nickname(self) -> str:
while True:
nickname = self._make_random_nickname()
for squadron in self.iter_squadrons():
if squadron.nickname == nickname:
break
else:
return nickname

View File

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

View File

@@ -1,33 +1,20 @@
import itertools
import logging
import math
import typing
from typing import Dict, Type
from typing import Any
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task
from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.unittype import UnitType
from game import db
from game.db import PRICES
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
STRENGTH_AA_ASSEMBLE_MIN = 0.2
PLANES_SCRAMBLE_MIN_BASE = 2
PLANES_SCRAMBLE_MAX_BASE = 8
PLANES_SCRAMBLE_FACTOR = 0.3
BASE_MAX_STRENGTH = 1
BASE_MIN_STRENGTH = 0
BASE_MAX_STRENGTH = 1.0
BASE_MIN_STRENGTH = 0.0
class Base:
def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {}
self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {}
self.strength = 1
def __init__(self) -> None:
self.aircraft: dict[AircraftType, int] = {}
self.armor: dict[GroundUnitType, int] = {}
self.strength = 1.0
@property
def total_aircraft(self) -> int:
@@ -41,150 +28,54 @@ class Base:
def total_armor_value(self) -> int:
total = 0
for unit_type, count in self.armor.items():
try:
total += PRICES[unit_type] * count
except KeyError:
logging.exception(f"No price found for {unit_type.id}")
total += unit_type.price * count
return total
@property
def total_frontline_aa(self) -> int:
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
@property
def total_aa(self) -> int:
return sum(self.aa.values())
def total_units(self, task: Task) -> int:
def total_units_of_type(self, unit_type: UnitType[Any]) -> int:
return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
if t in db.UNIT_BY_TASK[task]
]
)
def total_units_of_type(self, unit_type) -> int:
return sum(
[
c
for t, c in itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
for t, c in itertools.chain(self.aircraft.items(), self.armor.items())
if t == unit_type
]
)
@property
def all_units(self):
return itertools.chain(
self.aircraft.items(), self.armor.items(), self.aa.items()
)
def _find_best_unit(
self, available_units: Dict[UnitType, int], for_type: Task, count: int
) -> Dict[UnitType, int]:
if count <= 0:
logging.warning("{}: no units for {}".format(self, for_type))
return {}
sorted_units = [
key for key in available_units if key in db.UNIT_BY_TASK[for_type]
]
sorted_units.sort(key=lambda x: db.PRICES[x], reverse=True)
result: Dict[UnitType, int] = {}
for unit_type in sorted_units:
existing_count = available_units[unit_type] # type: int
if not existing_count:
continue
if count <= 0:
break
result_unit_count = min(count, existing_count)
count -= result_unit_count
assert result_unit_count > 0
result[unit_type] = result.get(unit_type, 0) + result_unit_count
logging.info("{} for {} ({}): {}".format(self, for_type, count, result))
return result
def _find_best_planes(
self, for_type: Task, count: int
) -> typing.Dict[FlyingType, int]:
return self._find_best_unit(self.aircraft, for_type, count)
def _find_best_armor(self, for_type: Task, count: int) -> typing.Dict[Armor, int]:
return self._find_best_unit(self.armor, for_type, count)
def append_commision_points(self, for_type, points: float) -> int:
self.commision_points[for_type] = (
self.commision_points.get(for_type, 0) + points
)
points = self.commision_points[for_type]
if points >= 1:
self.commision_points[for_type] = points - math.floor(points)
return int(math.floor(points))
return 0
def filter_units(self, applicable_units: typing.Collection):
self.aircraft = {
k: v for k, v in self.aircraft.items() if k in applicable_units
}
self.armor = {k: v for k, v in self.armor.items() if k in applicable_units}
def commision_units(self, units: typing.Dict[typing.Any, int]):
def commission_units(self, units: dict[Any, int]) -> None:
for unit_type, unit_count in units.items():
if unit_count <= 0:
continue
for_task = db.unit_task(unit_type)
target_dict = None
if (
for_task == AWACS
or for_task == CAS
or for_task == CAP
or for_task == Embarking
):
target_dict: dict[Any, int]
if isinstance(unit_type, AircraftType):
target_dict = self.aircraft
elif for_task == PinpointStrike:
elif isinstance(unit_type, GroundUnitType):
target_dict = self.armor
elif for_task == AirDefence:
target_dict = self.aa
if target_dict is not None:
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
else:
logging.error("Unable to determine target dict for " + str(unit_type))
logging.error(f"Unexpected unit type of {unit_type}")
return
def commit_losses(self, units_lost: typing.Dict[typing.Any, int]):
target_dict[unit_type] = target_dict.get(unit_type, 0) + unit_count
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:
target_array = self.aircraft
target_dict = self.aircraft
elif unit_type in self.armor:
target_array = self.armor
target_dict = self.armor
else:
print("Base didn't find event type {}".format(unit_type))
continue
if unit_type not in target_array:
if unit_type not in target_dict:
print("Base didn't find event type {}".format(unit_type))
continue
target_array[unit_type] = max(target_array[unit_type] - count, 0)
if target_array[unit_type] == 0:
del target_array[unit_type]
target_dict[unit_type] = max(target_dict[unit_type] - count, 0)
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
@@ -193,55 +84,3 @@ class Base:
def set_strength_to_minimum(self) -> None:
self.strength = BASE_MIN_STRENGTH
def scramble_count(self, multiplier: float, task: Task = None) -> int:
if task:
count = sum(
[v for k, v in self.aircraft.items() if db.unit_task(k) == task]
)
else:
count = self.total_aircraft
count = int(math.ceil(count * PLANES_SCRAMBLE_FACTOR * self.strength))
return min(
min(
max(count, PLANES_SCRAMBLE_MIN_BASE),
int(PLANES_SCRAMBLE_MAX_BASE * multiplier),
),
count,
)
def assemble_count(self):
return int(self.total_armor * 0.5)
def assemble_aa_count(self) -> int:
# previous logic removed because we always want the full air defense capabilities.
return self.total_aa
def scramble_sweep(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def scramble_last_defense(self):
# return as many CAP-capable aircraft as we can since this is the last defense of the base
# (but not more than 20 - that's just nuts)
return self._find_best_planes(CAP, min(self.total_aircraft, 20))
def scramble_cas(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAS, self.scramble_count(multiplier, CAS))
def scramble_interceptors(self, multiplier: float) -> typing.Dict[FlyingType, int]:
return self._find_best_planes(CAP, self.scramble_count(multiplier, CAP))
def assemble_attack(self) -> typing.Dict[Armor, int]:
return self._find_best_armor(PinpointStrike, self.assemble_count())
def assemble_defense(self) -> typing.Dict[Armor, int]:
count = int(self.total_armor * min(self.strength + 0.5, 1))
return self._find_best_armor(PinpointStrike, count)
def assemble_aa(self, count=None) -> typing.Dict[AirDefence, int]:
return self._find_best_unit(
self.aa,
AirDefence,
count and min(count, self.total_aa) or self.assemble_aa_count(),
)

26
game/theater/bullseye.py Normal file
View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import Dict, TYPE_CHECKING
from dcs import Point
from game.theater import LatLon
if TYPE_CHECKING:
from game.theater import ConflictTheater
@dataclass
class Bullseye:
position: Point
@classmethod
def from_pydcs(cls, bulls: Dict[str, float]) -> Bullseye:
return cls(Point(bulls["x"], bulls["y"]))
def to_pydcs(self) -> Dict[str, float]:
return {"x": self.position.x, "y": self.position.y}
def to_lat_lon(self, theater: ConflictTheater) -> LatLon:
return theater.point_to_ll(self.position)

8
game/theater/caucasus.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=33,
false_easting=-99516.9999999732,
false_northing=-4998114.999999984,
scale_factor=0.9996,
)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

183
game/theater/frontline.py Normal file
View File

@@ -0,0 +1,183 @@
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, List, Tuple, Any
from dcs.mapping import Point
from gen.flights.flight import FlightType
from .controlpoint import (
ControlPoint,
MissionTarget,
)
from ..utils import pairwise
FRONTLINE_MIN_CP_DISTANCE = 5000
@dataclass
class FrontLineSegment:
"""
Describes a line segment of a FrontLine
"""
point_a: Point
point_b: Point
@property
def attack_heading(self) -> float:
"""The heading of the frontline segment from player to enemy control point"""
return self.point_a.heading_between_point(self.point_b)
@property
def attack_distance(self) -> float:
"""Length of the segment"""
return self.point_a.distance_to_point(self.point_b)
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
Front lines are the area where ground combat happens.
Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation.
"""
def __init__(
self,
blue_point: ControlPoint,
red_point: ControlPoint,
) -> None:
self.blue_cp = blue_point
self.red_cp = red_point
try:
route = list(blue_point.convoy_route_to(red_point))
except KeyError:
# Some campaigns are air only and the mission generator currently relies on
# *some* "front line" being drawn between these two. In this case there will
# be no supply route to follow. Just create an arbitrary route between the
# two points.
route = [blue_point.position, red_point.position]
# Snap the beginning and end points to the CPs rather than the convoy waypoints,
# which are on roads.
route[0] = blue_point.position
route[-1] = red_point.position
self.segments: List[FrontLineSegment] = [
FrontLineSegment(a, b) for a, b in pairwise(route)
]
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:
return self.red_cp
return self.blue_cp
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.CAS,
FlightType.AEWC,
FlightType.REFUELING
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]
yield from super().mission_types(for_player)
@property
def points(self) -> Iterator[Point]:
yield self.segments[0].point_a
for segment in self.segments:
yield segment.point_b
@property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.blue_cp, self.red_cp
@property
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) -> float:
"""The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading
@property
def active_segment(self) -> FrontLineSegment:
"""The FrontLine segment where there can be an active conflict"""
if self._position_distance <= self.segments[0].attack_distance:
return self.segments[0]
remaining_dist = self._position_distance
for segment in self.segments:
if remaining_dist <= segment.attack_distance:
return segment
else:
remaining_dist -= segment.attack_distance
logging.error(
"Frontline attack distance is greater than the sum of its segments"
)
return self.segments[0]
def point_from_a(self, distance: float) -> Point:
"""
Returns a point {distance} away from control_point_a along the frontline segments.
"""
if distance < self.segments[0].attack_distance:
return self.blue_cp.position.point_from_heading(
self.segments[0].attack_heading, distance
)
remaining_dist = distance
for segment in self.segments:
if remaining_dist < segment.attack_distance:
return segment.point_a.point_from_heading(
segment.attack_heading, remaining_dist
)
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:
"""
The distance from point "a" where the conflict should occur
according to the current strength of each control point
"""
total_strength = self.blue_cp.base.strength + self.red_cp.base.strength
if self.blue_cp.base.strength == 0:
return self._adjust_for_min_dist(0)
if self.red_cp.base.strength == 0:
return self._adjust_for_min_dist(self.attack_distance)
strength_pct = self.blue_cp.base.strength / total_strength
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
def _adjust_for_min_dist(self, distance: float) -> float:
"""
Ensures the frontline conflict is never located within the minimum distance
constant of either end control point.
"""
if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE
):
distance = FRONTLINE_MIN_CP_DISTANCE
return distance

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)

34
game/theater/latlon.py Normal file
View File

@@ -0,0 +1,34 @@
from dataclasses import dataclass
from typing import List, Tuple
@dataclass(frozen=True)
class LatLon:
latitude: float
longitude: float
def as_list(self) -> List[float]:
return [self.latitude, self.longitude]
@staticmethod
def _components(dimension: float) -> Tuple[int, int, float]:
degrees = int(dimension)
minutes = int(dimension * 60 % 60)
seconds = dimension * 3600 % 60
return degrees, minutes, seconds
def _format_component(
self, dimension: float, hemispheres: Tuple[str, str], seconds_precision: int
) -> str:
hemisphere = hemispheres[0] if dimension >= 0 else hemispheres[1]
degrees, minutes, seconds = self._components(dimension)
return f"{degrees}°{minutes:02}'{seconds:02.{seconds_precision}f}\"{hemisphere}"
def format_dms(self, include_decimal_seconds: bool = False) -> str:
precision = 2 if include_decimal_seconds else 0
return " ".join(
[
self._format_component(self.latitude, ("N", "S"), precision),
self._format_component(self.longitude, ("E", "W"), precision),
]
)

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,8 +1,10 @@
from __future__ import annotations
from typing import Iterator, TYPE_CHECKING
from collections import Sequence
from typing import Iterator, TYPE_CHECKING, List, Union
from dcs.mapping import Point
from dcs.unit import Unit
if TYPE_CHECKING:
from gen.flights.flight import FlightType
@@ -19,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)
@@ -36,9 +38,13 @@ class MissionTarget:
yield from [
FlightType.ESCORT,
FlightType.TARCAP,
FlightType.SEAD,
FlightType.SEAD_ESCORT,
FlightType.SWEEP,
# TODO: FlightType.ELINT,
# TODO: FlightType.EWAR,
# TODO: FlightType.RECON,
]
@property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return []

8
game/theater/nevada.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=-117,
false_easting=-193996.80999964548,
false_northing=-4410028.063999966,
scale_factor=0.9996,
)

8
game/theater/normandy.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=-3,
false_easting=-195526.00000000204,
false_northing=-5484812.999999951,
scale_factor=0.9996,
)

View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=57,
false_easting=75755.99999999645,
false_northing=-2894933.0000000377,
scale_factor=0.9996,
)

View File

@@ -0,0 +1,31 @@
from dataclasses import dataclass
from pyproj import CRS
@dataclass(frozen=True)
class TransverseMercator:
central_meridian: int
false_easting: float
false_northing: float
scale_factor: float
def to_crs(self) -> CRS:
return CRS.from_proj4(
" ".join(
[
"+proj=tmerc",
"+lat_0=0",
f"+lon_0={self.central_meridian}",
f"+k_0={self.scale_factor}",
f"+x_0={self.false_easting}",
f"+y_0={self.false_northing}",
"+towgs84=0,0,0,0,0,0,0",
"+units=m",
"+vunits=m",
"+ellps=WGS84",
"+no_defs",
"+axis=neu",
]
)
)

View File

@@ -5,7 +5,7 @@ import pickle
import random
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Dict, Iterable, List, Optional, Set
from typing import Any, Dict, Iterable, List, Set
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike
@@ -13,15 +13,18 @@ from dcs.vehicles import AirDefence
from game import Game, db
from game.factions.faction import Faction
from game.theater import Carrier, Lha, LocationType, PointWithHeading
from game.scenery_group import SceneryGroup
from game.theater import Carrier, Lha, PointWithHeading
from game.theater.theatergroundobject import (
BuildingGroundObject,
CarrierGroundObject,
EwrGroundObject,
FactoryGroundObject,
LhaGroundObject,
MissileSiteGroundObject,
SamGroundObject,
ShipGroundObject,
SceneryGroundObject,
VehicleGroupGroundObject,
CoastalSiteGroundObject,
)
@@ -34,11 +37,10 @@ from gen.fleet.ship_group_generator import (
generate_lha_group,
generate_ship_group,
)
from gen.locations.preset_location_finder import MizDataLocationFinder
from gen.missiles.missiles_group_generator import generate_missile_group
from gen.sam.airdefensegroupgenerator import AirDefenseRange
from gen.sam.sam_group_generator import generate_anti_air_group
from gen.sam.ewr_group_generator import generate_ewr_group
from gen.sam.sam_group_generator import generate_anti_air_group
from . import (
ConflictTheater,
ControlPoint,
@@ -46,6 +48,7 @@ from . import (
Fob,
OffMapSpawn,
)
from ..profiling import logged_duration
from ..settings import Settings
GroundObjectTemplates = Dict[str, Dict[str, Any]]
@@ -75,37 +78,52 @@ 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:
# Reset name generator
namegen.reset()
self.prepare_theater()
game = Game(
player_name=self.player,
enemy_name=self.enemy,
theater=self.theater,
start_date=self.generator_settings.start_date,
settings=self.settings,
player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget,
)
with logged_duration("TGO population"):
# Reset name generator
namegen.reset()
self.prepare_theater()
game = Game(
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,
player_budget=self.generator_settings.player_budget,
enemy_budget=self.generator_settings.enemy_budget,
)
GroundObjectGenerator(game, self.generator_settings).generate()
GroundObjectGenerator(game, self.generator_settings).generate()
game.settings.version = VERSION
game.begin_turn_0()
return game
def prepare_theater(self) -> None:
@@ -140,175 +158,6 @@ class GameGenerator:
cp.captured = True
class LocationFinder:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.miz_data = MizDataLocationFinder.compute_possible_locations(
game.theater.terrain.name, control_point.full_name
)
def location_for(self, location_type: LocationType) -> Optional[PointWithHeading]:
position = self.control_point.preset_locations.random_for(location_type)
if position is not None:
return position
logging.warning(
f"No campaign location for %s Mat %s",
location_type.value,
self.control_point,
)
position = self.random_from_miz_data(
location_type == LocationType.OffshoreStrikeTarget
)
if position is not None:
return position
logging.debug(
f"No mizdata location for %s at %s", location_type.value, self.control_point
)
position = self.random_position(location_type)
if position is not None:
return position
logging.error(
f"Could not find position for %s at %s",
location_type.value,
self.control_point,
)
return None
def random_from_miz_data(self, offshore: bool) -> Optional[PointWithHeading]:
if offshore:
locations = self.miz_data.offshore_locations
else:
locations = self.miz_data.ashore_locations
if self.miz_data.offshore_locations:
preset = random.choice(locations)
locations.remove(preset)
return PointWithHeading.from_point(preset.position, preset.heading)
return None
def random_position(
self, location_type: LocationType
) -> Optional[PointWithHeading]:
# TODO: Flesh out preset locations so we never hit this case.
if location_type == LocationType.Coastal:
# No coastal locations generated randomly
return None
logging.warning(
"Falling back to random location for %s at %s",
location_type.value,
self.control_point,
)
is_base_defense = location_type in {
LocationType.BaseAirDefense,
LocationType.Garrison,
LocationType.Shorad,
}
on_land = location_type not in {
LocationType.OffshoreStrikeTarget,
LocationType.Ship,
}
avoid_others = location_type not in {
LocationType.Garrison,
LocationType.MissileSite,
LocationType.Sam,
LocationType.Ship,
LocationType.Shorad,
}
if is_base_defense:
min_range = 400
max_range = 3200
elif location_type == LocationType.Ship:
min_range = 5000
max_range = 40000
elif location_type == LocationType.MissileSite:
min_range = 2500
max_range = 40000
else:
min_range = 10000
max_range = 40000
position = self._find_random_position(
min_range, max_range, on_land, is_base_defense, avoid_others
)
# Retry once, searching a bit further (On some big airbases, 3200 is too
# short (Ex : Incirlik)), but searching farther on every base would be
# problematic, as some base defense units would end up very far away
# from small airfields.
if position is None and is_base_defense:
position = self._find_random_position(
3200, 4800, on_land, is_base_defense, avoid_others
)
return position
def _find_random_position(
self,
min_range: int,
max_range: int,
on_ground: bool,
is_base_defense: bool,
avoid_others: bool,
) -> Optional[PointWithHeading]:
"""
Find a valid ground object location
:param on_ground: Whether it should be on ground or on sea (True = on
ground)
:param min_range: Minimal range from point
:param max_range: Max range from point
:param is_base_defense: True if the location is for base defense.
:return:
"""
near = self.control_point.position
others = self.control_point.ground_objects
def is_valid(point: Optional[PointWithHeading]) -> bool:
if point is None:
return False
if on_ground and not self.game.theater.is_on_land(point):
return False
elif not on_ground and not self.game.theater.is_in_sea(point):
return False
if avoid_others:
for other in others:
if other.position.distance_to_point(point) < 10000:
return False
if is_base_defense:
# If it's a base defense we don't care how close it is to other
# points.
return True
# Else verify that it's not too close to another control point.
for control_point in self.game.theater.controlpoints:
if control_point != self.control_point:
if control_point.position.distance_to_point(point) < 30000:
return False
for ground_obj in control_point.ground_objects:
if ground_obj.position.distance_to_point(point) < 10000:
return False
return True
for _ in range(300):
# Check if on land or sea
p = PointWithHeading.from_point(
near.random_point_within(max_range, min_range), random.randint(0, 360)
)
if is_valid(p):
return p
return None
class ControlPointGroundObjectGenerator:
def __init__(
self,
@@ -319,14 +168,13 @@ class ControlPointGroundObjectGenerator:
self.game = game
self.generator_settings = generator_settings
self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@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:
@@ -335,10 +183,7 @@ class ControlPointGroundObjectGenerator:
def generate(self) -> bool:
self.control_point.connected_objectives = []
if self.faction.navy_generators:
# Even airbases can generate navies if they are close enough to the
# water. This is not controlled by the control point definition, but
# rather by whether or not the generator can find a valid position
# for the ship.
# Even airbases can generate navies if they are close enough to the water.
self.generate_navy()
return True
@@ -352,18 +197,14 @@ class ControlPointGroundObjectGenerator:
if not self.control_point.captured and skip_enemy_navy:
return
for _ in range(self.faction.navy_group_count):
self.generate_ship()
def generate_ship(self) -> None:
point = self.location_finder.location_for(LocationType.OffshoreStrikeTarget)
if point is None:
return
for position in self.control_point.preset_locations.ships:
self.generate_ship_at(position)
def generate_ship_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = ShipGroundObject(
namegen.random_objective_name(), group_id, point, self.control_point
namegen.random_objective_name(), group_id, position, self.control_point
)
group = generate_ship_group(self.game, g, self.faction_name)
@@ -432,156 +273,6 @@ class LhaGroundObjectGenerator(ControlPointGroundObjectGenerator):
return True
class BaseDefenseGenerator:
def __init__(self, game: Game, control_point: ControlPoint) -> None:
self.game = game
self.control_point = control_point
self.location_finder = LocationFinder(game, control_point)
@property
def faction_name(self) -> str:
if self.control_point.captured:
return self.game.player_name
else:
return self.game.enemy_name
@property
def faction(self) -> Faction:
return db.FACTIONS[self.faction_name]
def generate(self) -> None:
self.generate_ewr()
self.generate_garrison()
self.generate_base_defenses()
def generate_ewr(self) -> None:
position = self.location_finder.location_for(LocationType.BaseEwr)
if position is None:
return
group_id = self.game.next_group_id()
g = EwrGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
True,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
logging.error(f"Could not generate EWR at {self.control_point}")
return
g.groups = [group]
self.control_point.base_defenses.append(g)
def generate_base_defenses(self) -> None:
# First group has a 1/2 chance of being a SAM, 1/6 chance of SHORAD,
# and a 1/6 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_sam()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
def generate_garrison(self) -> None:
position = self.location_finder.location_for(LocationType.Garrison)
if position is None:
return
group_id = self.game.next_group_id()
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(f"Could not generate garrison at {self.control_point}")
return
g.groups.append(group)
self.control_point.base_defenses.append(g)
def generate_sam(self) -> None:
position = self.location_finder.location_for(LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(self.game, g, self.faction)
if not groups:
logging.error(f"Could not generate SAM at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
def generate_shorad(self) -> None:
position = self.location_finder.location_for(LocationType.BaseAirDefense)
if position is None:
return
group_id = self.game.next_group_id()
g = SamGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
for_airbase=True,
)
groups = generate_anti_air_group(
self.game, g, self.faction, ranges=[{AirDefenseRange.Short}]
)
if not groups:
logging.error(f"Could not generate SHORAD group at {self.control_point}")
return
g.groups = groups
self.control_point.base_defenses.append(g)
class FobDefenseGenerator(BaseDefenseGenerator):
def generate(self) -> None:
self.generate_garrison()
self.generate_fob_defenses()
def generate_fob_defenses(self):
# First group has a 1/2 chance of being a SHORAD,
# and a 1/2 chance of a garrison.
#
# Further groups have a 1/3 chance of being SHORAD and 2/3 chance of
# being a garrison.
for i in range(random.randint(2, 5)):
if i == 0 and random.randint(0, 1) == 0:
self.generate_shorad()
elif i == 0 and random.randint(0, 1) == 0:
self.generate_garrison()
elif random.randint(0, 2) == 1:
self.generate_shorad()
else:
self.generate_garrison()
class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
def __init__(
self,
@@ -597,8 +288,19 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if not super().generate():
return False
BaseDefenseGenerator(self.game, self.control_point).generate()
self.generate_ground_points()
return True
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
self.generate_armor_groups()
self.generate_aa()
self.generate_ewrs()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
self.generate_factories()
self.generate_ammunition_depots()
if self.faction.missiles:
self.generate_missile_sites()
@@ -606,96 +308,73 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True
def generate_armor_groups(self) -> None:
for position in self.control_point.preset_locations.armor_groups:
self.generate_armor_at(position)
def generate_ground_points(self) -> None:
"""Generate ground objects and AA sites for the control point."""
skip_sams = self.generate_required_aa()
skip_ewrs = self.generate_required_ewr()
def generate_armor_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
if self.control_point.is_global:
g = VehicleGroupGroundObject(
namegen.random_objective_name(),
group_id,
position,
self.control_point,
)
group = generate_armor_group(self.faction_name, self.game, g)
if group is None:
logging.error(
"Could not generate armor group for %s at %s",
g.name,
self.control_point,
)
return
g.groups = [group]
self.control_point.connected_objectives.append(g)
# Always generate at least one AA point.
self.generate_aa_site()
# And between 2 and 7 other objectives.
amount = random.randrange(2, 7)
for i in range(amount):
# 1 in 4 additional objectives are AA.
if random.randint(0, 3) == 0:
if skip_sams > 0:
skip_sams -= 1
else:
self.generate_aa_site()
# 1 in 4 additional objectives are EWR.
elif random.randint(0, 3) == 0:
if skip_ewrs > 0:
skip_ewrs -= 1
else:
self.generate_ewr_site()
else:
self.generate_ground_point()
def generate_required_aa(self) -> int:
"""Generates the AA sites that are required by the campaign.
Returns:
The number of AA sites that were generated.
"""
def generate_aa(self) -> None:
presets = self.control_point.preset_locations
for position in presets.required_long_range_sams:
for position in presets.long_range_sams:
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Long},
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
{AirDefenseRange.AAA},
],
)
for position in presets.required_medium_range_sams:
for position in presets.medium_range_sams:
self.generate_aa_at(
position,
ranges=[
{AirDefenseRange.Medium},
{AirDefenseRange.Short},
{AirDefenseRange.AAA},
],
)
return len(presets.required_long_range_sams) + len(
presets.required_medium_range_sams
)
for position in presets.short_range_sams:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.Short}, {AirDefenseRange.AAA}],
)
for position in presets.aaa:
self.generate_aa_at(
position,
ranges=[{AirDefenseRange.AAA}],
)
def generate_required_ewr(self) -> int:
"""Generates the EWR sites that are required by the campaign.
Returns:
The number of EWR sites that were generated.
"""
def generate_ewrs(self) -> None:
presets = self.control_point.preset_locations
for position in presets.required_ewrs:
for position in presets.ewrs:
self.generate_ewr_at(position)
return len(presets.required_ewrs)
def generate_ground_point(self) -> None:
try:
category = random.choice(self.faction.building_set)
except IndexError:
logging.exception("Faction has no buildings defined")
return
def generate_strike_target_at(self, category: str, position: Point) -> None:
obj_name = namegen.random_objective_name()
template = random.choice(list(self.templates[category].values()))
if category == "oil":
location_type = LocationType.OffshoreStrikeTarget
else:
location_type = LocationType.StrikeTarget
# Pick from preset locations
point = self.location_finder.location_for(location_type)
if point is None:
return
object_id = 0
group_id = self.game.next_group_id()
@@ -709,7 +388,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
category,
group_id,
object_id,
point + template_point,
position + template_point,
unit["heading"],
self.control_point,
unit["type"],
@@ -717,19 +396,28 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
def generate_aa_site(self) -> None:
position = self.location_finder.location_for(LocationType.Sam)
if position is None:
return
self.generate_aa_at(
position,
ranges=[
# Prefer to use proper SAMs, but fall back to SHORADs if needed.
{AirDefenseRange.Long, AirDefenseRange.Medium},
{AirDefenseRange.Short},
],
def generate_ammunition_depots(self) -> None:
for position in self.control_point.preset_locations.ammunition_depots:
self.generate_strike_target_at(category="ammo", position=position)
def generate_factories(self) -> None:
for position in self.control_point.preset_locations.factories:
self.generate_factory_at(position)
def generate_factory_at(self, point: PointWithHeading) -> None:
obj_name = namegen.random_objective_name()
group_id = self.game.next_group_id()
g = FactoryGroundObject(
obj_name,
group_id,
point,
point.heading,
self.control_point,
)
self.control_point.connected_objectives.append(g)
def generate_aa_at(
self, position: Point, ranges: Iterable[Set[AirDefenseRange]]
) -> None:
@@ -740,7 +428,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id,
position,
self.control_point,
for_airbase=False,
)
groups = generate_anti_air_group(self.game, g, self.faction, ranges)
if not groups:
@@ -753,13 +440,7 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = groups
self.control_point.connected_objectives.append(g)
def generate_ewr_site(self) -> None:
position = self.location_finder.location_for(LocationType.Ewr)
if position is None:
return
self.generate_ewr_at(position)
def generate_ewr_at(self, position: Point) -> None:
def generate_ewr_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = EwrGroundObject(
@@ -767,7 +448,6 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
group_id,
position,
self.control_point,
for_airbase=False,
)
group = generate_ewr_group(self.game, g, self.faction)
if group is None:
@@ -780,15 +460,45 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
g.groups = [group]
self.control_point.connected_objectives.append(g)
def generate_scenery_sites(self) -> None:
presets = self.control_point.preset_locations
for scenery_group in presets.scenery:
self.generate_tgo_for_scenery(scenery_group)
def generate_tgo_for_scenery(self, scenery: SceneryGroup) -> None:
obj_name = namegen.random_objective_name()
category = scenery.category
group_id = self.game.next_group_id()
object_id = 0
# Each nested trigger zone is a target/building/unit for an objective.
for zone in scenery.zones:
object_id += 1
local_position = zone.position
local_dcs_identifier = zone.name
g = SceneryGroundObject(
obj_name,
category,
group_id,
object_id,
local_position,
self.control_point,
local_dcs_identifier,
zone,
)
self.control_point.connected_objectives.append(g)
return
def generate_missile_sites(self) -> None:
for i in range(self.faction.missiles_group_count):
self.generate_missile_site()
def generate_missile_site(self) -> None:
position = self.location_finder.location_for(LocationType.MissileSite)
if position is None:
return
for position in self.control_point.preset_locations.missile_sites:
self.generate_missile_site_at(position)
def generate_missile_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = MissileSiteGroundObject(
@@ -802,14 +512,10 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
return
def generate_coastal_sites(self) -> None:
for i in range(self.faction.coastal_group_count):
self.generate_coastal_site()
def generate_coastal_site(self) -> None:
position = self.location_finder.location_for(LocationType.Coastal)
if position is None:
return
for position in self.control_point.preset_locations.coastal_defenses:
self.generate_coastal_site_at(position)
def generate_coastal_site_at(self, position: PointWithHeading) -> None:
group_id = self.game.next_group_id()
g = CoastalSiteGroundObject(
@@ -826,21 +532,45 @@ class AirbaseGroundObjectGenerator(ControlPointGroundObjectGenerator):
self.control_point.connected_objectives.append(g)
return
def generate_strike_targets(self) -> None:
building_set = list(set(self.faction.building_set) - {"oil"})
if not building_set:
logging.error("Faction has no buildings defined")
return
for position in self.control_point.preset_locations.strike_locations:
category = random.choice(building_set)
self.generate_strike_target_at(category, position)
def generate_offshore_strike_targets(self) -> None:
if "oil" not in self.faction.building_set:
logging.error("Faction does not support offshore strike targets")
return
for position in self.control_point.preset_locations.offshore_strike_locations:
self.generate_strike_target_at("oil", position)
class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
def generate(self) -> bool:
self.generate_fob()
FobDefenseGenerator(self.game, self.control_point).generate()
self.generate_required_aa()
self.generate_armor_groups()
self.generate_factories()
self.generate_ammunition_depots()
self.generate_aa()
self.generate_ewrs()
self.generate_scenery_sites()
self.generate_strike_targets()
self.generate_offshore_strike_targets()
if self.faction.missiles:
self.generate_missile_sites()
if self.faction.coastal_defenses:
self.generate_coastal_sites()
return True
def generate_fob(self) -> None:
try:
category = self.faction.building_set[self.faction.building_set.index("fob")]
except IndexError:
logging.exception("Faction has no fob buildings defined")
return
category = "fob"
obj_name = self.control_point.name
template = random.choice(list(self.templates[category].values()))
point = self.control_point.position
@@ -862,7 +592,7 @@ class FobGroundObjectGenerator(AirbaseGroundObjectGenerator):
unit["heading"],
self.control_point,
unit["type"],
airbase_group=True,
is_fob_structure=True,
)
self.control_point.connected_objectives.append(g)

8
game/theater/syria.py Normal file
View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=39,
false_easting=282801.00000003993,
false_northing=-3879865.9999999935,
scale_factor=0.9996,
)

View File

@@ -2,14 +2,20 @@ from __future__ import annotations
import itertools
import logging
from typing import Iterator, List, TYPE_CHECKING
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.unitgroup import ShipGroup, VehicleGroup
from .. import db
from ..data.radar_db import UNITS_WITH_RADAR
from ..data.radar_db import (
TRACK_RADARS,
TELARS,
LAUNCHER_TRACKER_PAIRS,
)
from ..utils import Distance, meters
if TYPE_CHECKING:
@@ -19,82 +25,32 @@ if TYPE_CHECKING:
from .missiontarget import MissionTarget
NAME_BY_CATEGORY = {
"power": "Power plant",
"ammo": "Ammo depot",
"fuel": "Fuel depot",
"ewr": "Early Warning Radar",
"aa": "AA Defense Site",
"ware": "Warehouse",
"farp": "FARP",
"fob": "FOB",
"factory": "Factory",
"comms": "Comms. tower",
"oil": "Oil platform",
"derrick": "Derrick",
"ww2bunker": "Bunker",
"village": "Village",
"allycamp": "Camp",
"EWR": "EWR",
}
ABBREV_NAME = {
"power": "PLANT",
"ammo": "AMMO",
"fuel": "FUEL",
"aa": "AA",
"ware": "WARE",
"ammo": "Ammo depot",
"armor": "Armor group",
"coastal": "Coastal defense",
"comms": "Communications tower",
"derrick": "Derrick",
"factory": "Factory",
"farp": "FARP",
"fob": "FOB",
"factory": "FACTORY",
"comms": "COMMST",
"oil": "OILP",
"derrick": "DERK",
"ww2bunker": "BUNK",
"village": "VLG",
"allycamp": "CMP",
}
CATEGORY_MAP = {
# Special cases
"CARRIER": ["CARRIER"],
"LHA": ["LHA"],
"aa": ["AA"],
# Buildings
"power": [
"Workshop A",
"Electric power box",
"Garage small A",
"Farm B",
"Repair workshop",
"Garage B",
],
"ware": ["Warehouse", "Hangar A"],
"fuel": ["Tank", "Tank 2", "Tank 3", "Fuel tank"],
"ammo": [".Ammunition depot", "Hangar B"],
"farp": [
"FARP Tent",
"FARP Ammo Dump Coating",
"FARP Fuel Depot",
"FARP Command Post",
"FARP CP Blindage",
],
"fob": ["Bunker 2", "Bunker 1", "Garage small B", ".Command Center", "Barracks 2"],
"factory": ["Tech combine", "Tech hangar A"],
"comms": ["TV tower", "Comms tower M"],
"oil": ["Oil platform"],
"derrick": ["Oil derrick", "Pump station", "Subsidiary structure 2"],
"ww2bunker": [
"Siegfried Line",
"Fire Control Bunker",
"SK_C_28_naval_gun",
"Concertina Wire",
"Czech hedgehogs 1",
],
"village": ["Small house 1B", "Small House 1A", "Small warehouse 1"],
"allycamp": [],
"fuel": "Fuel depot",
"missile": "Missile site",
"oil": "Oil platform",
"power": "Power plant",
"ship": "Ship",
"village": "Village",
"ware": "Warehouse",
"ww2bunker": "Bunker",
}
class TheaterGroundObject(MissionTarget):
GroupT = TypeVar("GroupT", ShipGroup, VehicleGroup)
class TheaterGroundObject(MissionTarget, Generic[GroupT]):
def __init__(
self,
name: str,
@@ -104,7 +60,6 @@ class TheaterGroundObject(MissionTarget):
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group: bool,
sea_object: bool,
) -> None:
super().__init__(name, position)
@@ -113,9 +68,8 @@ class TheaterGroundObject(MissionTarget):
self.heading = heading
self.control_point = control_point
self.dcs_identifier = dcs_identifier
self.airbase_group = airbase_group
self.sea_object = sea_object
self.groups: List[Group] = []
self.groups: List[GroupT] = []
@property
def is_dead(self) -> bool:
@@ -128,6 +82,17 @@ class TheaterGroundObject(MissionTarget):
"""
return list(itertools.chain.from_iterable([g.units for g in self.groups]))
@property
def dead_units(self) -> List[Unit]:
"""
:return: all the dead units at this location
"""
return list(
itertools.chain.from_iterable(
[getattr(g, "units_losts", []) for g in self.groups]
)
)
@property
def group_name(self) -> str:
"""The name of the unit group."""
@@ -178,15 +143,14 @@ class TheaterGroundObject(MissionTarget):
return False
@property
def has_radar(self) -> bool:
"""Returns True if the ground object contains a unit with radar."""
def has_live_radar_sam(self) -> bool:
"""Returns True if the ground object contains a unit with working radar SAM."""
for group in self.groups:
for unit in group.units:
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
return True
if self.threat_range(group, radar_only=True):
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)
@@ -204,21 +168,48 @@ class TheaterGroundObject(MissionTarget):
max_range = max(max_range, meters(unit_range))
return max_range
def detection_range(self, group: Group) -> Distance:
def max_detection_range(self) -> Distance:
return max(self.detection_range(g) for g in self.groups)
def detection_range(self, group: GroupT) -> Distance:
return self._max_range_of_type(group, "detection_range")
def threat_range(self, group: Group) -> Distance:
if not self.detection_range(group):
# For simple SAMs like shilkas, the unit has both a threat and
# detection range. For complex sites like SA-2s, the launcher has a
# threat range and the search/track radars have detection ranges. If
# the site has no detection range it has no radars and can't fire,
# so it's not actually a threat even if it still has launchers.
return meters(0)
def max_threat_range(self) -> Distance:
return max(self.threat_range(g) for g in self.groups)
def threat_range(self, group: GroupT, radar_only: bool = False) -> Distance:
return self._max_range_of_type(group, "threat_range")
@property
def is_factory(self) -> bool:
return self.category == "factory"
class BuildingGroundObject(TheaterGroundObject):
@property
def is_control_point(self) -> bool:
"""True if this TGO is the group for the control point itself (CVs and FOBs)."""
return False
@property
def strike_targets(self) -> Sequence[Union[MissionTarget, Unit]]:
return self.units
@property
def mark_locations(self) -> Iterator[Point]:
yield self.position
def clear(self) -> None:
self.groups = []
@property
def capturable(self) -> bool:
raise NotImplementedError
@property
def purchasable(self) -> bool:
raise NotImplementedError
class BuildingGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
@@ -229,7 +220,7 @@ class BuildingGroundObject(TheaterGroundObject):
heading: int,
control_point: ControlPoint,
dcs_identifier: str,
airbase_group=False,
is_fob_structure: bool = False,
) -> None:
super().__init__(
name=name,
@@ -239,9 +230,9 @@ class BuildingGroundObject(TheaterGroundObject):
heading=heading,
control_point=control_point,
dcs_identifier=dcs_identifier,
airbase_group=airbase_group,
sea_object=False,
)
self.is_fob_structure = is_fob_structure
self.object_id = object_id
# Other TGOs track deadness based on the number of alive units, but
# buildings don't have groups assigned to the TGO.
@@ -265,8 +256,92 @@ class BuildingGroundObject(TheaterGroundObject):
def kill(self) -> None:
self._dead = True
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
class NavalGroundObject(TheaterGroundObject):
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return list(self.iter_building_group())
@property
def mark_locations(self) -> Iterator[Point]:
for building in self.iter_building_group():
yield building.position
@property
def is_control_point(self) -> bool:
return self.is_fob_structure
@property
def capturable(self) -> bool:
return True
@property
def purchasable(self) -> bool:
return False
class SceneryGroundObject(BuildingGroundObject):
def __init__(
self,
name: str,
category: str,
group_id: int,
object_id: int,
position: Point,
control_point: ControlPoint,
dcs_identifier: str,
zone: TriggerZone,
) -> None:
super().__init__(
name=name,
category=category,
group_id=group_id,
object_id=object_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier=dcs_identifier,
is_fob_structure=False,
)
self.zone = zone
try:
# In the default TriggerZone using "assign as..." in the DCS Mission Editor,
# property three has the scenery's object ID as its value.
self.map_object_id = self.zone.properties[3]["value"]
except (IndexError, KeyError):
logging.exception(
"Invalid TriggerZone for Scenery definition. The third property must "
"be the map object ID."
)
raise
class FactoryGroundObject(BuildingGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
heading: int,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="factory",
group_id=group_id,
object_id=0,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="Workshop A",
is_fob_structure=False,
)
class NavalGroundObject(TheaterGroundObject[ShipGroup]):
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
@@ -278,9 +353,19 @@ class NavalGroundObject(TheaterGroundObject):
def might_have_aa(self) -> bool:
return True
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return False
class GenericCarrierGroundObject(NavalGroundObject):
pass
@property
def is_control_point(self) -> bool:
return True
# TODO: Why is this both a CP and a TGO?
@@ -294,7 +379,6 @@ class CarrierGroundObject(GenericCarrierGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="CARRIER",
airbase_group=True,
sea_object=True,
)
@@ -316,7 +400,6 @@ class LhaGroundObject(GenericCarrierGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="LHA",
airbase_group=True,
sea_object=True,
)
@@ -327,60 +410,69 @@ 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:
super().__init__(
name=name,
category="aa",
category="missile",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
class CoastalSiteGroundObject(TheaterGroundObject):
@property
def purchasable(self) -> bool:
return False
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,
category="aa",
category="coastal",
group_id=group_id,
position=position,
heading=heading,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
class BaseDefenseGroundObject(TheaterGroundObject):
"""Base type for all base defenses."""
@property
def purchasable(self) -> bool:
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(BaseDefenseGroundObject):
# 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,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
@@ -390,21 +482,106 @@ class SamGroundObject(BaseDefenseGroundObject):
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False,
)
# Set by the SAM unit generator if the generated group is compatible
# with Skynet.
self.skynet_capable = False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield FlightType.SEAD
yield from super().mission_types(for_player)
@property
def might_have_aa(self) -> bool:
return True
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.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(unit_type.threat_range))
elif unit_type in LAUNCHER_TRACKER_PAIRS:
launchers.add(unit_type)
else:
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(unit_type.threat_range))
if radar_only:
return max(max_tel_range, max_telar_range)
else:
return max(max_tel_range, max_telar_range, max_non_radar)
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return True
class VehicleGroupGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="armor",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
@property
def purchasable(self) -> bool:
return True
class EwrGroundObject(TheaterGroundObject[VehicleGroup]):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
) -> None:
super().__init__(
name=name,
category="ewr",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="EWR",
sea_object=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
# Prefix the group names with the side color so Skynet can find them.
# Use Group Id and uppercase EWR
return f"{self.faction_color}|EWR|{self.group_id}"
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
@@ -417,64 +594,12 @@ class SamGroundObject(BaseDefenseGroundObject):
def might_have_aa(self) -> bool:
return True
class VehicleGroupGroundObject(BaseDefenseGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
category="aa",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=for_airbase,
sea_object=False,
)
class EwrGroundObject(BaseDefenseGroundObject):
def __init__(
self,
name: str,
group_id: int,
position: Point,
control_point: ControlPoint,
for_airbase: bool,
) -> None:
super().__init__(
name=name,
category="EWR",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="EWR",
airbase_group=for_airbase,
sea_object=False,
)
@property
def capturable(self) -> bool:
return False
@property
def group_name(self) -> str:
# Prefix the group names with the side color so Skynet can find them.
return f"{self.faction_color}|{super().group_name}"
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
if not self.is_friendly(for_player):
yield FlightType.DEAD
yield from super().mission_types(for_player)
@property
def might_have_aa(self) -> bool:
def purchasable(self) -> bool:
return True
@@ -484,13 +609,12 @@ class ShipGroundObject(NavalGroundObject):
) -> None:
super().__init__(
name=name,
category="aa",
category="ship",
group_id=group_id,
position=position,
heading=0,
control_point=control_point,
dcs_identifier="AA",
airbase_group=False,
sea_object=True,
)

View File

@@ -0,0 +1,8 @@
from game.theater.projections import TransverseMercator
PARAMETERS = TransverseMercator(
central_meridian=3,
false_easting=99376.00000000288,
false_northing=-5636889.00000001,
scale_factor=0.9996,
)

View File

@@ -0,0 +1,196 @@
from __future__ import annotations
import heapq
import math
from collections import defaultdict
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Dict, Iterator, List, Optional, Set, Tuple
from game.theater import ConflictTheater
from game.theater.controlpoint import ControlPoint
class NoPathError(RuntimeError):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(f"Could not reconstruct path to {destination} from {origin}")
@dataclass(frozen=True, order=True)
class FrontierNode:
cost: float
point: ControlPoint = field(compare=False)
class Frontier:
def __init__(self) -> None:
self.nodes: List[FrontierNode] = []
def push(self, poly: ControlPoint, cost: float) -> None:
heapq.heappush(self.nodes, FrontierNode(cost, poly))
def pop(self) -> Optional[FrontierNode]:
try:
return heapq.heappop(self.nodes)
except IndexError:
return None
def __bool__(self) -> bool:
return bool(self.nodes)
class TransitConnection(Enum):
Road = auto()
Shipping = auto()
Airlift = auto()
class TransitNetwork:
def __init__(self) -> None:
self.nodes: Dict[
ControlPoint, Dict[ControlPoint, TransitConnection]
] = defaultdict(dict)
def has_destinations(self, control_point: ControlPoint) -> bool:
return bool(self.nodes[control_point])
def has_link(self, a: ControlPoint, b: ControlPoint) -> bool:
return b in self.nodes[a]
def link_type(self, a: ControlPoint, b: ControlPoint) -> TransitConnection:
return self.nodes[a][b]
def link_with(
self, a: ControlPoint, b: ControlPoint, link_type: TransitConnection
) -> None:
self.nodes[a][b] = link_type
self.nodes[b][a] = link_type
def link_road(self, a: ControlPoint, b: ControlPoint) -> None:
self.link_with(a, b, TransitConnection.Road)
def link_shipping(self, a: ControlPoint, b: ControlPoint) -> None:
self.link_with(a, b, TransitConnection.Shipping)
def link_airport(self, a: ControlPoint, b: ControlPoint) -> None:
self.link_with(a, b, TransitConnection.Airlift)
def connections_from(self, control_point: ControlPoint) -> Iterator[ControlPoint]:
yield from self.nodes[control_point]
def cost(self, a: ControlPoint, b: ControlPoint) -> float:
return {
TransitConnection.Road: 1,
TransitConnection.Shipping: 3,
# Set arbitrarily high so that other methods are preferred, but still scaled
# by distance so that when we do need it we still pick the closest airfield.
# The units of distance are meters so there's no risk of these
TransitConnection.Airlift: a.position.distance_to_point(b.position),
}[self.link_type(a, b)]
def has_path_between(
self,
origin: ControlPoint,
destination: ControlPoint,
seen: Optional[set[ControlPoint]] = None,
) -> bool:
if seen is None:
seen = set()
seen.add(origin)
for connection in self.connections_from(origin):
if connection in seen:
continue
if connection == destination:
return True
if self.has_path_between(connection, destination, seen):
return True
return False
def shortest_path_between(
self, origin: ControlPoint, destination: ControlPoint
) -> list[ControlPoint]:
return self.shortest_path_with_cost(origin, destination)[0]
def shortest_path_with_cost(
self, origin: ControlPoint, destination: ControlPoint
) -> Tuple[List[ControlPoint], float]:
if origin not in self.nodes:
raise ValueError(f"{origin} is not in the transit network.")
if destination not in self.nodes:
raise ValueError(f"{destination} is not in the transit network.")
frontier = Frontier()
frontier.push(origin, 0)
came_from: Dict[ControlPoint, Optional[ControlPoint]] = {origin: None}
best_known: Dict[ControlPoint, float] = defaultdict(lambda: math.inf)
best_known[origin] = 0.0
while (node := frontier.pop()) is not None:
cost = node.cost
current = node.point
if cost > best_known[current]:
continue
for neighbor in self.connections_from(current):
new_cost = cost + self.cost(node.point, neighbor)
if new_cost < best_known[neighbor]:
best_known[neighbor] = new_cost
frontier.push(neighbor, new_cost)
came_from[neighbor] = current
# Reconstruct and reverse the path.
current = destination
path: List[ControlPoint] = []
while current != origin:
path.append(current)
previous = came_from.get(current)
if previous is None:
raise NoPathError(origin, destination)
current = previous
path.reverse()
return path, best_known[destination]
class TransitNetworkBuilder:
def __init__(self, theater: ConflictTheater, for_player: bool) -> None:
self.control_points = list(theater.control_points_for(for_player))
self.network = TransitNetwork()
self.airports: Set[ControlPoint] = {
cp
for cp in self.control_points
if cp.is_friendly(for_player) and cp.runway_is_operational()
}
def build(self) -> TransitNetwork:
seen = set()
for control_point in self.control_points:
if control_point not in seen:
seen.add(control_point)
self.add_transit_links(control_point)
return self.network
def add_transit_links(self, control_point: ControlPoint) -> None:
# Prefer road connections.
for road_connection in control_point.connected_points:
if road_connection.is_friendly_to(control_point):
self.network.link_road(control_point, road_connection)
# Use sea connections if there's no road or rail connection.
for sea_connection in control_point.shipping_lanes:
if self.network.has_link(control_point, sea_connection):
continue
if sea_connection.is_friendly_to(control_point):
self.network.link_shipping(control_point, sea_connection)
# And use airports as a last resort.
if control_point in self.airports:
for airport in self.airports:
if control_point == airport:
continue
if self.network.has_link(control_point, airport):
continue
if not airport.is_friendly_to(control_point):
continue
self.network.link_airport(control_point, airport)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from functools import singledispatchmethod
from typing import Optional, TYPE_CHECKING, Union
from typing import Optional, TYPE_CHECKING, Union, Iterable
from dcs.mapping import Point as DcsPoint
from shapely.geometry import (
@@ -13,11 +13,10 @@ from shapely.geometry import (
from shapely.geometry.base import BaseGeometry
from shapely.ops import nearest_points, unary_union
from game.theater import ControlPoint
from game.theater import ControlPoint, MissionTarget
from game.utils import Distance, meters, nautical_miles
from gen import Conflict
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight
from gen.flights.flight import Flight, FlightWaypoint
if TYPE_CHECKING:
from game import Game
@@ -27,9 +26,15 @@ ThreatPoly = Union[MultiPolygon, Polygon]
class ThreatZones:
def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
def __init__(
self,
airbases: ThreatPoly,
air_defenses: ThreatPoly,
radar_sam_threats: ThreatPoly,
) -> None:
self.airbases = airbases
self.air_defenses = air_defenses
self.radar_sam_threats = radar_sam_threats
self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint:
@@ -38,8 +43,14 @@ class ThreatZones:
)
return DcsPoint(boundary.x, boundary.y)
def distance_to_threat(self, point: DcsPoint) -> Distance:
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
@@ -55,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
@@ -69,8 +82,17 @@ class ThreatZones:
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
def waypoints_threatened_by_aircraft(
self, waypoints: Iterable[FlightWaypoint]
) -> bool:
return self.threatened_by_aircraft(
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
@@ -83,12 +105,41 @@ class ThreatZones:
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
@threatened_by_air_defense.register
def _threatened_by_air_defense_mission_target(self, target: MissionTarget) -> bool:
return self.threatened_by_air_defense(
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: # type: ignore
raise NotImplementedError
@threatened_by_radar_sam.register
def _threatened_by_radar_sam_geom(self, position: BaseGeometry) -> bool:
return self.radar_sam_threats.intersects(position)
@threatened_by_radar_sam.register
def _threatened_by_radar_sam_flight(self, flight: Flight) -> bool:
return self.threatened_by_radar_sam(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
def waypoints_threatened_by_radar_sam(
self, waypoints: Iterable[FlightWaypoint]
) -> bool:
return self.threatened_by_radar_sam(
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
)
@classmethod
def closest_enemy_airbase(
cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in airfields.airfields_within(max_distance):
for airfield in airfields.all_airfields_within(max_distance):
if airfield.captured != location.captured:
return airfield
return None
@@ -134,6 +185,7 @@ class ThreatZones:
"""
air_threats = []
air_defenses = []
radar_sam_threats = []
for control_point in game.theater.controlpoints:
if control_point.captured != player:
continue
@@ -151,9 +203,16 @@ class ThreatZones:
point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters)
air_defenses.append(threat_zone)
radar_threat_range = tgo.threat_range(group, radar_only=True)
if radar_threat_range > nautical_miles(3):
point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters)
radar_sam_threats.append(threat_zone)
return cls(
airbases=unary_union(air_threats), air_defenses=unary_union(air_defenses)
airbases=unary_union(air_threats),
air_defenses=unary_union(air_defenses),
radar_sam_threats=unary_union(radar_sam_threats),
)
@staticmethod

763
game/transfers.py Normal file
View File

@@ -0,0 +1,763 @@
from __future__ import annotations
import logging
import math
from collections import defaultdict
from dataclasses import dataclass, field
from functools import singledispatchmethod
from typing import (
Generic,
Iterator,
List,
Optional,
TYPE_CHECKING,
TypeVar,
Sequence,
)
from dcs.mapping import Point
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.procurement import AircraftProcurementRequest
from game.squadrons import Squadron
from game.theater import ControlPoint, MissionTarget
from game.theater.transitnetwork import (
TransitConnection,
TransitNetwork,
)
from game.utils import meters, nautical_miles
from gen.ato import Package
from gen.flights.ai_flight_planner_db import TRANSPORT_CAPABLE, aircraft_for_task
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
from gen.naming import namegen
if TYPE_CHECKING:
from game import Game
from game.inventory import ControlPointAircraftInventory
class Transport:
def __init__(self, destination: ControlPoint):
self.destination = destination
def find_escape_route(self) -> Optional[ControlPoint]:
raise NotImplementedError
def description(self) -> str:
raise NotImplementedError
@dataclass
class TransferOrder:
"""The base type of all transfer orders.
A transfer order can transfer multiple units of multiple types.
"""
#: The location the units are transferring from.
origin: ControlPoint
#: The location the units are transferring to.
destination: ControlPoint
#: The current position of the group being transferred. Groups may make multiple
#: stops and can switch transport modes before reaching their destination.
position: ControlPoint = field(init=False)
#: True if the transfer order belongs to the player.
player: bool = field(init=False)
#: The units being transferred.
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)
@property
def description(self) -> str:
if self.transport is None:
return "No transports available"
return self.transport.description()
def kill_all(self) -> None:
self.units.clear()
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} 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(self.units.values())
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property
def completed(self) -> bool:
return self.destination == self.position or not self.size
def disband_at(self, location: ControlPoint) -> None:
logging.info(f"Units halting at {location}.")
location.base.commission_units(self.units)
self.units.clear()
@property
def next_stop(self) -> ControlPoint:
if self.transport is None:
raise RuntimeError(
"TransferOrder.next_stop called with no transport assigned"
)
return self.transport.destination
def find_escape_route(self) -> Optional[ControlPoint]:
if self.transport is not None:
return self.transport.find_escape_route()
return None
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"Current position ({self.position}) "
f"of the halting transfer was captured."
)
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
self.transport = None
if self.completed:
self.disband_at(self.position)
class Airlift(Transport):
"""A transfer order that moves units by cargo planes and helicopters."""
def __init__(
self, transfer: TransferOrder, flight: Flight, next_stop: ControlPoint
) -> None:
super().__init__(next_stop)
self.transfer = transfer
self.flight = flight
@property
def units(self) -> dict[GroundUnitType, int]:
return self.transfer.units
@property
def player_owned(self) -> bool:
return self.transfer.player
def find_escape_route(self) -> Optional[ControlPoint]:
# TODO: Move units to closest base.
return None
def description(self) -> str:
return (
f"Being airlifted from {self.transfer.position} to {self.destination} by "
f"{self.flight}"
)
class AirliftPlanner:
#: Maximum range from for any link in the route of takeoff, pickup, dropoff, and RTB
#: for a helicopter to be considered for airlift. Total route length is not
#: considered because the helicopter can refuel at each stop. Cargo planes have no
#: maximum range.
HELO_MAX_RANGE = nautical_miles(100)
def __init__(
self, game: Game, transfer: TransferOrder, next_stop: ControlPoint
) -> None:
self.game = game
self.transfer = transfer
self.next_stop = next_stop
self.for_player = transfer.destination.captured
self.package = Package(target=next_stop, auto_asap=True)
def compatible_with_mission(
self, unit_type: AircraftType, airfield: ControlPoint
) -> bool:
if unit_type not in aircraft_for_task(FlightType.TRANSPORT):
return False
if not self.transfer.origin.can_operate(unit_type):
return False
if not self.next_stop.can_operate(unit_type):
return False
# Cargo planes have no maximum range.
if not unit_type.dcs_unit_type.helicopter:
return True
# A helicopter that is transport capable and able to operate at both bases. Need
# to check that no leg of the journey exceeds the maximum range. This doesn't
# account for any routing around threats that might take place, but it's close
# enough.
home = airfield.position
pickup = self.transfer.position.position
drop_off = self.transfer.position.position
if meters(home.distance_to_point(pickup)) > self.HELO_MAX_RANGE:
return False
if meters(pickup.distance_to_point(drop_off)) > self.HELO_MAX_RANGE:
return False
if meters(drop_off.distance_to_point(home)) > self.HELO_MAX_RANGE:
return False
return True
def create_package_for_airlift(self) -> None:
distance_cache = ObjectiveDistanceCache.get_closest_airfields(
self.transfer.position
)
air_wing = self.game.air_wing_for(self.for_player)
for cp in distance_cache.closest_airfields:
if cp.captured != self.for_player:
continue
inventory = self.game.aircraft_inventory.for_control_point(cp)
for unit_type, available in inventory.all_aircraft:
squadrons = air_wing.auto_assignable_for_task_with_type(
unit_type, FlightType.TRANSPORT
)
for squadron in squadrons:
if self.compatible_with_mission(unit_type, cp):
while (
available
and squadron.has_available_pilots
and self.transfer.transport is None
):
flight_size = self.create_airlift_flight(
squadron, inventory
)
available -= flight_size
if self.package.flights:
self.game.ato_for(self.for_player).add_package(self.package)
def create_airlift_flight(
self, squadron: Squadron, inventory: ControlPointAircraftInventory
) -> int:
available_aircraft = inventory.available(squadron.aircraft)
capacity_each = 1 if squadron.aircraft.dcs_unit_type.helicopter else 2
required = math.ceil(self.transfer.size / capacity_each)
flight_size = min(
required,
available_aircraft,
squadron.aircraft.dcs_unit_type.group_size_max,
)
# 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:
transfer = self.game.transfers.split_transfer(self.transfer, capacity)
else:
transfer = self.transfer
player = inventory.control_point.captured
flight = Flight(
self.package,
self.game.country_for(player),
squadron,
flight_size,
FlightType.TRANSPORT,
self.game.settings.default_start_type,
departure=inventory.control_point,
arrival=inventory.control_point,
divert=None,
cargo=transfer,
)
transport = Airlift(transfer, flight, self.next_stop)
transfer.transport = transport
self.package.add_flight(flight)
planner = FlightPlanBuilder(self.game, self.package, self.for_player)
planner.populate_flight_plan(flight)
self.game.aircraft_inventory.claim_for_flight(flight)
return flight_size
class MultiGroupTransport(MissionTarget, Transport):
def __init__(
self, name: str, origin: ControlPoint, destination: ControlPoint
) -> None:
MissionTarget.__init__(self, name, origin.position)
Transport.__init__(self, destination)
self.origin = origin
self.transfers: List[TransferOrder] = []
def is_friendly(self, to_player: bool) -> bool:
return self.origin.captured
def add_units(self, transfer: TransferOrder) -> None:
self.transfers.append(transfer)
transfer.transport = self
def remove_units(self, transfer: TransferOrder) -> None:
transfer.transport = None
self.transfers.remove(transfer)
def kill_unit(self, unit_type: GroundUnitType) -> None:
for transfer in self.transfers:
try:
transfer.kill_unit(unit_type)
return
except KeyError:
pass
raise KeyError
def kill_all(self) -> None:
for transfer in self.transfers:
transfer.kill_all()
def disband(self) -> None:
for transfer in list(self.transfers):
self.remove_units(transfer)
self.transfers.clear()
@property
def size(self) -> int:
return sum(t.size for t in self.transfers)
@property
def units(self) -> dict[GroundUnitType, int]:
units: dict[GroundUnitType, int] = defaultdict(int)
for transfer in self.transfers:
for unit_type, count in transfer.units.items():
units[unit_type] += count
return units
def iter_units(self) -> Iterator[GroundUnitType]:
for unit_type, count in self.units.items():
for _ in range(count):
yield unit_type
@property
def player_owned(self) -> bool:
return self.origin.captured
def find_escape_route(self) -> Optional[ControlPoint]:
raise NotImplementedError
def description(self) -> str:
raise NotImplementedError
class Convoy(MultiGroupTransport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_convoy_name(), origin, destination)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.BAI
yield from super().mission_types(for_player)
@property
def route_start(self) -> Point:
return self.origin.convoy_origin_for(self.destination)
@property
def route_end(self) -> Point:
return self.destination.convoy_origin_for(self.origin)
def description(self) -> str:
return f"In a convoy from {self.origin} to {self.destination}"
def find_escape_route(self) -> Optional[ControlPoint]:
return None
class CargoShip(MultiGroupTransport):
def __init__(self, origin: ControlPoint, destination: ControlPoint) -> None:
super().__init__(namegen.next_cargo_ship_name(), origin, destination)
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
if self.is_friendly(for_player):
return
yield FlightType.ANTISHIP
yield from super().mission_types(for_player)
@property
def route(self) -> Sequence[Point]:
return self.origin.shipping_lanes[self.destination]
def description(self) -> str:
return f"On a ship from {self.origin} to {self.destination}"
def find_escape_route(self) -> Optional[ControlPoint]:
return None
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]
] = defaultdict(dict)
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> TransportType:
raise NotImplementedError
def transport_exists(self, origin: ControlPoint, destination: ControlPoint) -> bool:
return destination in self.transports[origin]
def find_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> Optional[TransportType]:
return self.transports[origin].get(destination)
def find_or_create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> TransportType:
transport = self.find_transport(origin, destination)
if transport is None:
transport = self.create_transport(origin, destination)
self.transports[origin][destination] = transport
return transport
def departing_from(self, origin: ControlPoint) -> Iterator[TransportType]:
yield from self.transports[origin].values()
def travelling_to(self, destination: ControlPoint) -> Iterator[TransportType]:
for destination_dict in self.transports.values():
if destination in destination_dict:
yield destination_dict[destination]
def disband_transport(self, transport: TransportType) -> None:
transport.disband()
del self.transports[transport.origin][transport.destination]
def add(self, transfer: TransferOrder, next_stop: ControlPoint) -> None:
self.find_or_create_transport(transfer.position, next_stop).add_units(transfer)
def remove(self, transport: TransportType, transfer: TransferOrder) -> None:
transport.remove_units(transfer)
if not transport.transfers:
self.disband_transport(transport)
def disband_all(self) -> None:
for transport in list(self):
self.disband_transport(transport)
def __iter__(self) -> Iterator[TransportType]:
for destination_dict in self.transports.values():
yield from destination_dict.values()
class ConvoyMap(TransportMap[Convoy]):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> Convoy:
return Convoy(origin, destination)
class CargoShipMap(TransportMap[CargoShip]):
def create_transport(
self, origin: ControlPoint, destination: ControlPoint
) -> CargoShip:
return CargoShip(origin, destination)
class PendingTransfers:
def __init__(self, game: Game) -> None:
self.game = game
self.convoys = ConvoyMap()
self.cargo_ships = CargoShipMap()
self.pending_transfers: List[TransferOrder] = []
def __iter__(self) -> Iterator[TransferOrder]:
yield from self.pending_transfers
@property
def pending_transfer_count(self) -> int:
return len(self.pending_transfers)
def transfer_at_index(self, index: int) -> TransferOrder:
return self.pending_transfers[index]
def index_of_transfer(self, transfer: TransferOrder) -> int:
return self.pending_transfers.index(transfer)
def network_for(self, control_point: ControlPoint) -> TransitNetwork:
return self.game.transit_network_for(control_point.captured)
def arrange_transport(self, transfer: TransferOrder) -> None:
network = self.network_for(transfer.position)
path = network.shortest_path_between(transfer.position, transfer.destination)
next_stop = path[0]
if network.link_type(transfer.position, next_stop) == TransitConnection.Road:
self.convoys.add(transfer, next_stop)
elif (
network.link_type(transfer.position, next_stop)
== TransitConnection.Shipping
):
self.cargo_ships.add(transfer, next_stop)
else:
AirliftPlanner(self.game, transfer, next_stop).create_package_for_airlift()
def new_transfer(self, transfer: TransferOrder) -> None:
transfer.origin.base.commit_losses(transfer.units)
self.pending_transfers.append(transfer)
self.arrange_transport(transfer)
def split_transfer(self, transfer: TransferOrder, size: int) -> TransferOrder:
"""Creates a smaller transfer that is a subset of the original."""
if transfer.size <= size:
raise ValueError
units = {}
for unit_type, remaining in transfer.units.items():
take = min(remaining, size)
size -= take
transfer.units[unit_type] -= take
units[unit_type] = take
if not size:
break
new_transfer = TransferOrder(transfer.origin, transfer.destination, units)
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( # type: ignore
self,
transport,
transfer: TransferOrder,
) -> None:
pass
@cancel_transport.register
def _cancel_transport_air(
self, transport: Airlift, _transfer: TransferOrder
) -> None:
flight = transport.flight
flight.package.remove_flight(flight)
if not flight.package.flights:
self.game.ato_for(transport.player_owned).remove_package(flight.package)
self.game.aircraft_inventory.return_from_flight(flight)
flight.clear_roster()
@cancel_transport.register
def _cancel_transport_convoy(
self, transport: Convoy, transfer: TransferOrder
) -> None:
self.convoys.remove(transport, transfer)
@cancel_transport.register
def _cancel_transport_cargo_ship(
self, transport: CargoShip, transfer: TransferOrder
) -> None:
self.cargo_ships.remove(transport, transfer)
def cancel_transfer(self, transfer: TransferOrder) -> None:
if transfer.transport is not None:
self.cancel_transport(transfer.transport, transfer)
self.pending_transfers.remove(transfer)
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()
if not transfer.completed:
incomplete.append(transfer)
self.pending_transfers = incomplete
self.convoys.disband_all()
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:
if self.game.air_wing_for(control_point.captured).can_auto_plan(
FlightType.TRANSPORT
):
self.order_airlift_assets_at(control_point)
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)
squadrons = self.game.air_wing_for(
control_point.captured
).auto_assignable_for_task(FlightType.TRANSPORT)
unit_types = {s.aircraft for s in squadrons}
return sum(
count
for unit_type, count in inventory.all_aircraft
if unit_type in unit_types
)
def order_airlift_assets_at(self, control_point: ControlPoint) -> None:
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
if gap % 2:
# Always buy in pairs since we're not trying to fill odd squadrons. Purely
# 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
)
)

170
game/unitdelivery.py Normal file
View File

@@ -0,0 +1,170 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Optional, TYPE_CHECKING, Any
from game.theater import ControlPoint
from .dcs.groundunittype import GroundUnitType
from .dcs.unittype import UnitType
from .theater.transitnetwork import (
NoPathError,
TransitNetwork,
)
from .transfers import TransferOrder
if TYPE_CHECKING:
from .game import Game
@dataclass(frozen=True)
class GroundUnitSource:
control_point: ControlPoint
class PendingUnitDeliveries:
def __init__(self, destination: ControlPoint) -> None:
self.destination = destination
# Maps unit type to order quantity.
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[Any], int]) -> None:
for k, v in units.items():
self.units[k] += v
def sell(self, units: dict[UnitType[Any], int]) -> None:
for k, v in units.items():
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_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[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[Any]) -> int:
current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units
def process(self, game: Game) -> None:
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
game.message(
f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price."
)
self.refund_ground_units(game)
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]
if (
isinstance(unit_type, GroundUnitType)
and self.destination != ground_unit_source
):
source = ground_unit_source
d = units_needing_transfer
else:
source = self.destination
d = bought_units
if count >= 0:
d[unit_type] = count
game.message(
f"{coalition} reinforcements: {unit_type} x {count} at {source}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {unit_type} x {-count} at {source}")
self.units = defaultdict(int)
self.destination.base.commission_units(bought_units)
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]
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the
# reaction to turn 0. On turn zero we allow units to be recruited anywhere for
# delivery on turn 1 so that turn 1 always starts with units on the front line.
if game.turn == 1:
return self.destination
# Fast path if the destination is a valid source.
if self.destination.can_recruit_ground_units(game):
return self.destination
try:
return self.find_ground_unit_source_in_network(
game.transit_network_for(self.destination.captured), game
)
except NoPathError:
return None
def find_ground_unit_source_in_network(
self, network: TransitNetwork, game: Game
) -> Optional[ControlPoint]:
sources = []
for control_point in game.theater.control_points_for(self.destination.captured):
if control_point.can_recruit_ground_units(
game
) and network.has_path_between(self.destination, control_point):
sources.append(control_point)
if not sources:
return None
# Fast path to skip the distance calculation if we have only one option.
if len(sources) == 1:
return sources[0]
closest = sources[0]
_, cost = network.shortest_path_with_cost(self.destination, closest)
for source in sources:
_, new_cost = network.shortest_path_with_cost(self.destination, source)
if new_cost < cost:
closest = source
cost = new_cost
return closest

View File

@@ -1,28 +1,52 @@
"""Maps generated units back to their Liberation types."""
import itertools
import math
from dataclasses import dataclass
from typing import Dict, Optional, Type
from typing import Dict, Optional, Any, Union, TypeVar, Generic
from dcs.unit import Unit
from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unittype import VehicleType
from dcs.unit import Vehicle, Ship
from dcs.unitgroup import FlyingGroup, VehicleGroup, StaticGroup, ShipGroup, MovingGroup
from game import db
from game.dcs.groundunittype import GroundUnitType
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject
from game.transfers import CargoShip, Convoy, TransferOrder
from gen.flights.flight import Flight
@dataclass(frozen=True)
class FrontLineUnit:
unit_type: Type[VehicleType]
origin: ControlPoint
class FlyingUnit:
flight: Flight
pilot: Optional[Pilot]
@dataclass(frozen=True)
class GroundObjectUnit:
ground_object: TheaterGroundObject
group: Group
unit: Unit
class FrontLineUnit:
unit_type: GroundUnitType
origin: ControlPoint
UnitT = TypeVar("UnitT", Ship, Vehicle)
@dataclass(frozen=True)
class GroundObjectUnit(Generic[UnitT]):
ground_object: TheaterGroundObject[Any]
group: MovingGroup[UnitT]
unit: UnitT
@dataclass(frozen=True)
class ConvoyUnit:
unit_type: GroundUnitType
convoy: Convoy
@dataclass(frozen=True)
class AirliftUnits:
cargo: tuple[GroundUnitType, ...]
transfer: TransferOrder
@dataclass(frozen=True)
@@ -32,22 +56,27 @@ class Building:
class UnitMap:
def __init__(self) -> None:
self.aircraft: Dict[str, Flight] = {}
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:
for unit in group.units:
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__.
name = str(unit.name)
if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}")
self.aircraft[name] = flight
self.aircraft[name] = FlyingUnit(flight, pilot)
if flight.cargo is not None:
self.add_airlift_units(group, flight.cargo)
def flight(self, unit_name: str) -> Optional[Flight]:
def flight(self, unit_name: str) -> Optional[FlyingUnit]:
return self.aircraft.get(unit_name, None)
def add_airfield(self, airfield: Airfield) -> None:
@@ -58,20 +87,15 @@ class UnitMap:
def airfield(self, name: str) -> Optional[Airfield]:
return self.airfields.get(name, None)
def add_front_line_units(self, group: Group, origin: ControlPoint) -> None:
def add_front_line_units(
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
# doesn't define __eq__.
name = str(unit.name)
if name in self.front_line_units:
raise RuntimeError(f"Duplicate front line unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType):
raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.front_line_units[name] = FrontLineUnit(unit_type, origin)
def front_line_unit(self, name: str) -> Optional[FrontLineUnit]:
@@ -79,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.
@@ -110,10 +134,66 @@ 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_building(self, ground_object: BuildingGroundObject, group: Group) -> 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__.
name = str(unit.name)
if name in self.convoys:
raise RuntimeError(f"Duplicate convoy unit: {name}")
self.convoys[name] = ConvoyUnit(unit_type, convoy)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
return self.convoys.get(name, 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
# a convoy of ships that logic needs to change.
raise ValueError("Expected cargo ship to be a single unit group.")
unit = group.units[0]
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.cargo_ships:
raise RuntimeError(f"Duplicate cargo ship: {name}")
self.cargo_ships[name] = ship
def cargo_ship(self, name: str) -> Optional[CargoShip]:
return self.cargo_ships.get(name, 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
# assigned arbitrarily to units in the order of the group. The last unit in
# the group will receive a partial load if there is not enough cargo to fill
# every transport.
base_idx = idx * capacity_each
cargo = tuple(
itertools.islice(
transfer.iter_units(), base_idx, base_idx + capacity_each
)
)
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(transport.name)
if name in self.airlifts:
raise RuntimeError(f"Duplicate airlift unit: {name}")
self.airlifts[name] = AirliftUnits(cargo, transfer)
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, 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"
@@ -136,5 +216,15 @@ class UnitMap:
raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object)
def add_scenery(self, ground_object: SceneryGroundObject) -> None:
name = str(ground_object.map_object_id)
if name in self.buildings:
raise RuntimeError(
f"Duplicate TGO unit: {name}. TriggerZone name: "
f"{ground_object.dcs_identifier}"
)
self.buildings[name] = Building(ground_object)
def building_or_fortification(self, name: str) -> Optional[Building]:
return self.buildings.get(name, None)

View File

@@ -1,8 +1,10 @@
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
@@ -15,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)
@@ -57,6 +54,10 @@ class Distance:
def from_nautical_miles(cls, value: float) -> Distance:
return cls(value * NM_TO_METERS)
@classmethod
def inf(cls) -> Distance:
return cls.from_meters(math.inf)
def __add__(self, other: Distance) -> Distance:
return meters(self.meters + other.meters)
@@ -178,3 +179,13 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
def pairwise(iterable: Iterable[Any]) -> Iterable[tuple[Any, Any]]:
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)

View File

@@ -1,8 +1,15 @@
from pathlib import Path
MAJOR_VERSION = 4
MINOR_VERSION = 1
MICRO_VERSION = 1
def _build_version_string() -> str:
components = ["2.5"]
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:
@@ -16,3 +23,91 @@ def _build_version_string() -> str:
#: Current version of Liberation.
VERSION = _build_version_string()
#: The latest version of the campaign format. Increment this version whenever all
#: existing campaigns should be flagged as incompatible in the UI. We will still attempt
#: to load old campaigns, but this provides a warning to the user that the campaign may
#: not work correctly.
#:
#: There is no verification that the campaign author updated their campaign correctly
#: this is just a UI hint.
#:
#: Version history:
#:
#: Version 0
#: * Unknown compatibility.
#:
#: Version 1
#: * Compatible with Liberation 2.5.
#:
#: Version 2
#: * Front line endpoints now define convoy origin/destination waypoints. They should be
#: placed on or near roads.
#: * Factories (Workshop_A) define factory objectives. Only control points with
#: factories will be able to recruit ground units, so they should exist in sufficient
#: number and be protected by IADS.
#:
#: Version 3
#: * Bulker Handy Winds define shipping lanes. They should be placed in port areas that
#: are navigable by ships and have a route to another port area. DCS ships *will not*
#: avoid driving into islands, so ensure that their waypoints plot a navigable route.
#:
#: Version 4
#: * TriggerZones define map based building targets. White TriggerZones created by right
#: clicking an object and using "assign as..." define the buildings within an objective.
#: Blue circular TriggerZones created normally must surround groups of one or more
#: white TriggerZones to define an objective. If a white TriggerZone is not surrounded
#: by a blue circular TriggerZone, campaign creation will fail. Blue circular
#: TriggerZones must also have their first property's value field define the type of
#: objective (a valid value for a building TGO category, from `game.db.PRICES`).
#:
#: Version 4.1
#: * All objective types may now be set as required generation (similar to the required
#: IADS generation). This includes:
#: * SHORADS
#: * Armor groups
#: * Strike targets
#: * Offshore strike targets
#: * Ships
#: * Missile sites
#: * Coastal defenses
#:
#: See the unit lists in MizCampaignLoader in conflicttheater.py for unit types.
#:
#: Version 4.2
#: * Adds support for AAA objectives. Place with any of the following units (either red
#: or blue):
#: * Flak18,
#: * Vulcan,
#: * ZSU_23_4_Shilka,
#:
#: Version 5.0
#: * Ammunition Depots objective locations are now predetermined using the "Ammunition
# Depot" Warehouse object, and through trigger zone based scenery objects.
#: * The number of alive Ammunition Depot objective buildings connected to a control
#: point directly influences how many ground units can be supported on the front
#: line.
#: * The number of supported ground units at any control point is artificially
#: capped at 50, even if the number of alive Ammunition Depot objectives can
#: support more.
#:
#: Version 6.0
#: * Random objective generation no is longer supported. Fixed objective locations were
#: added in 4.1.
#:
#: Version 6.1
#: * Support for new Syrian airfields in DCS 2.7.2.7910.1 (Cyprus update).
#:
#: 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

File diff suppressed because it is too large Load Diff

View File

@@ -451,7 +451,7 @@ AIRFIELD_DATA = {
"12": ("IMA", MHz(111, 750)),
},
),
"Al Minhad Intl": AirfieldData(
"Al Minhad AFB": AirfieldData(
theater="Persian Gulf",
icao="OMDM",
elevation=190,
@@ -620,9 +620,9 @@ AIRFIELD_DATA = {
tacan=TacanChannel(78, TacanBand.X),
tacan_callsign="BND",
vor=("BND", MHz(117, 200)),
atc=AtcData(MHz(4, 250), MHz(39, 401), MHz(118, 100), MHz(251, 0)),
atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(118, 100), MHz(251, 0)),
ils={
"21": ("IBND", MHz(333, 800)),
"21": ("IBND", MHz(109, 900)),
},
),
"Jiroft": AirfieldData(
@@ -674,7 +674,7 @@ AIRFIELD_DATA = {
vor=("DAN", MHz(108, 400)),
atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(122, 100), MHz(360, 100)),
ils={
"50": ("IDAN", MHz(109, 300)),
"05": ("IDAN", MHz(109, 300)),
"23": ("DANM", MHz(111, 700)),
},
),
@@ -966,7 +966,96 @@ AIRFIELD_DATA = {
runway_length=8871,
atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)),
ils={
"28": ("IGNP", MHz(109, 10)),
"28": ("IGNP", MHz(109, 100)),
},
),
"Gecitkale": AirfieldData(
theater="Syria",
icao="LCGK",
elevation=147,
runway_length=8156,
vor=("GKE", MHz(114, 300)),
atc=AtcData(MHz(3, 775), MHz(4, 800), MHz(40, 500), MHz(252, 50)),
),
"Kingsfield": AirfieldData(
theater="Syria",
icao="CY-0004",
elevation=270,
runway_length=3069,
atc=AtcData(MHz(4, 650), MHz(40, 200), MHz(121), MHz(251, 750)),
),
"Larnaca": AirfieldData(
theater="Syria",
icao="LCRE",
elevation=16,
runway_length=8009,
vor=("LCA", MHz(112, 80)),
atc=AtcData(MHz(4, 700), MHz(40, 300), MHz(121, 200), MHz(251, 850)),
ils={
"22": ("ILC", MHz(110, 300)),
},
),
"Ercan": AirfieldData(
theater="Syria",
icao="LCEN",
elevation=383,
runway_length=7559,
vor=("ECN", MHz(117)),
atc=AtcData(MHz(4, 750), MHz(40, 400), MHz(120, 200), MHz(251, 950)),
),
"Lakatamia": AirfieldData(
theater="Syria",
icao="CY-0001",
elevation=757,
runway_length=1230,
atc=AtcData(MHz(4, 725), MHz(40, 350), MHz(120, 200), MHz(251, 900)),
),
"Nicosia": AirfieldData(
theater="Syria",
icao="LCNC",
elevation=716,
runway_length=0,
),
"Pinarbashi": AirfieldData(
theater="Syria",
icao="CY-0003",
elevation=770,
runway_length=3364,
atc=AtcData(MHz(4, 825), MHz(40, 550), MHz(121), MHz(252, 100)),
),
"Akrotiri": AirfieldData(
theater="Syria",
icao="LCRA",
elevation=62,
runway_length=8276,
tacan=TacanChannel(107, TacanBand.X),
tacan_callsign="AKR",
vor=("AKR", MHz(116)),
atc=AtcData(MHz(4, 625), MHz(40, 150), MHz(128), MHz(251, 700)),
ils={
"28": ("IAK", MHz(109, 700)),
},
),
"Paphos": AirfieldData(
theater="Syria",
icao="LCPH",
elevation=40,
runway_length=8425,
vor=("PHA", MHz(117, 900)),
atc=AtcData(MHz(4, 675), MHz(40, 250), MHz(119, 900), MHz(251, 800)),
ils={
"29": ("IPA", MHz(108, 900)),
},
),
"Gazipasa": AirfieldData(
theater="Syria",
icao="LTFG",
elevation=36,
runway_length=6885,
vor=("GZP", MHz(114, 200)),
atc=AtcData(MHz(4, 600), MHz(40, 100), MHz(119, 250), MHz(251, 650)),
ils={
"8": ("IGZP", MHz(108, 500)),
},
),
# NTTR
@@ -1432,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,14 +15,17 @@ from dcs.task import (
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.unittype import UnitType
from game import db
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
@@ -35,23 +39,27 @@ AWACS_ALT = 13000
class AwacsInfo:
"""AWACS information for the kneeboard."""
dcsGroupName: str
group_name: str
callsign: str
freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
dcsGroupName: str
group_name: str
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass
@@ -65,7 +73,7 @@ class AirSupportConflictGenerator:
self,
mission: Mission,
conflict: Conflict,
game,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
) -> None:
@@ -90,119 +98,145 @@ class AirSupportConflictGenerator:
return (TANKER_ALT + 500, 596)
return (TANKER_ALT, 574)
def generate(self):
def generate(self) -> None:
player_cp = (
self.conflict.from_cp
if self.conflict.from_cp.captured
else self.conflict.to_cp
self.conflict.blue_cp
if self.conflict.blue_cp.captured
else self.conflict.red_cp
)
fallback_tanker_number = 0
if not self.game.settings.disable_legacy_tanker:
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(
db.find_unittype(Refueling, self.conflict.attackers_side)
):
alt, airspeed = self._get_tanker_params(tanker_unit_type)
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = (
self.conflict.to_cp.position.heading_between_point(
self.conflict.from_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
airport=None,
plane_type=tanker_unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
)
tanker_group.set_frequency(freq.mhz)
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
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
True,
tanker_group.units[0].id,
True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)
)
if not self.game.settings.disable_legacy_aewc:
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
if len(possible_awacs) > 0:
awacs_unit = possible_awacs[0]
# 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()
awacs_flight = self.mission.awacs_flight(
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = (
self.conflict.red_cp.position.heading_between_point(
self.conflict.blue_cp.position
)
+ TANKER_HEADING_OFFSET * i
)
tanker_position = player_cp.position.point_from_heading(
tanker_heading, TANKER_DISTANCE
)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name(
self.mission.country(self.game.player_country)
name=namegen.next_tanker_name(
self.mission.country(self.game.player_country), tanker_unit_type
),
plane_type=awacs_unit,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(
AWACS_DISTANCE, AWACS_DISTANCE
),
plane_type=unit_type,
position=tanker_position,
altitude=alt,
race_distance=58000,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=airspeed,
tacanchannel=str(tacan),
)
awacs_flight.set_frequency(freq.mhz)
tanker_group.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
self.air_support.awacs.append(
AwacsInfo(
dcsGroupName=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),
freq=freq,
depature_location=None,
if tanker_unit_type != IL_78M:
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(
ActivateBeaconCommand(
tacan.number,
tacan.band.value,
tacan_callsign,
True,
tanker_group.units[0].id,
True,
)
)
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(
TankerInfo(
str(tanker_group.name),
callsign,
tanker_unit_type.name,
freq,
tacan,
start_time=None,
end_time=None,
blue=True,
)
)
else:
if not self.game.settings.disable_legacy_aewc:
possible_awacs = [
a
for a in self.game.faction_for(player=True).aircrafts
if a in AEWC_CAPABLE
]
if not possible_awacs:
logging.warning("No AWACS for faction")
return
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=unit_type,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(
AWACS_DISTANCE, AWACS_DISTANCE
),
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(
AwacsInfo(
group_name=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),
freq=freq,
depature_location=None,
start_time=None,
end_time=None,
blue=True,
)
)

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
@@ -10,7 +11,6 @@ from dcs.action import AITaskPush
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction
from dcs.task import (
EPLRS,
@@ -24,20 +24,20 @@ 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 dcs.unittype import VehicleType
from game import db
from game.data.groundunitclass import GroundUnitClass
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap
from game.utils import heading_sum, opposite_heading
from game.theater.controlpoint import ControlPoint
from gen.ground_forces.ai_ground_planner import (
DISTANCE_FROM_FRONTLINE,
CombatGroup,
CombatGroupRole,
)
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
@@ -68,11 +68,12 @@ INFANTRY_GROUP_SIZE = 5
class JtacInfo:
"""JTAC information."""
dcsGroupName: str
group_name: str
unit_name: str
callsign: str
region: str
code: str
blue: bool
# TODO: Radio info? Type?
@@ -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,22 +123,13 @@ 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) -> None:
position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater
)
def generate(self):
position = Conflict.frontline_position(
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
)
frontline_vector = Conflict.frontline_vector(
self.conflict.from_cp, self.conflict.to_cp, self.game.theater
self.conflict.front_line, self.game.theater
)
# Create player groups at random position
@@ -150,37 +142,44 @@ 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,
player_groups,
enemy_groups,
self.conflict.heading + 90,
self.conflict.from_cp,
self.conflict.to_cp,
self.conflict.blue_cp,
self.conflict.red_cp,
)
self.plan_action_for_groups(
self.enemy_stance,
enemy_groups,
player_groups,
self.conflict.heading - 90,
self.conflict.to_cp,
self.conflict.from_cp,
self.conflict.red_cp,
self.conflict.blue_cp,
)
# Add JTAC
if self.game.player_faction.has_jtac:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id)
code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper
if self.game.player_faction.jtac_unit is not None:
utype = self.game.player_faction.jtac_unit
utype = self.game.player_faction.jtac_unit
if utype is None:
utype = AircraftType.named("MQ-9 Reaper")
jtac = self.mission.flight_group(
country=self.mission.country(self.game.player_country),
name=n,
aircraft_type=utype,
aircraft_type=utype.dcs_unit_type,
position=position[0],
airport=None,
altitude=5000,
@@ -191,12 +190,19 @@ class GroundConflictGenerator:
OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
)
frontline = (
f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
f"Frontline {self.conflict.blue_cp.name}/{self.conflict.red_cp.name}"
)
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(
JtacInfo(str(jtac.name), n, callsign, frontline, str(code))
JtacInfo(
str(jtac.name),
n,
callsign,
frontline,
str(code),
blue=True,
)
)
def gen_infantry_group_for_group(
@@ -213,27 +219,26 @@ class GroundConflictGenerator:
logging.warning("Could not find infantry position")
return
if side == self.conflict.attackers_country:
cp = self.conflict.from_cp
cp = self.conflict.blue_cp
else:
cp = self.conflict.to_cp
cp = self.conflict.red_cp
if is_player:
faction = self.game.player_name
else:
faction = self.game.enemy_name
faction = self.game.faction_for(is_player)
# Disable infantry unit gen if disabled
if not self.game.settings.perf_infantry:
if self.game.settings.manpads:
# 50% of armored units protected by manpad
if random.choice([True, False]):
manpads = db.find_manpad(faction)
if len(manpads) > 0:
u = random.choice(manpads)
manpads = list(faction.infantry_with_class(GroundUnitClass.Manpads))
if manpads:
u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads]
)[0]
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp.id, u),
u,
u.dcs_unit_type,
position=infantry_position,
group_size=1,
heading=forward_heading,
@@ -241,30 +246,38 @@ class GroundConflictGenerator:
)
return
possible_infantry_units = db.find_infantry(
faction, allow_manpad=self.game.settings.manpads
possible_infantry_units = set(
faction.infantry_with_class(GroundUnitClass.Infantry)
)
if len(possible_infantry_units) == 0:
if self.game.settings.manpads:
possible_infantry_units |= set(
faction.infantry_with_class(GroundUnitClass.Manpads)
)
if not possible_infantry_units:
return
u = random.choice(possible_infantry_units)
infantry_choices = list(possible_infantry_units)
units = random.choices(
infantry_choices,
weights=[u.spawn_weight for u in infantry_choices],
k=INFANTRY_GROUP_SIZE,
)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp.id, u),
u,
namegen.next_infantry_name(side, cp.id, units[0]),
units[0].dcs_unit_type,
position=infantry_position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad,
)
for i in range(INFANTRY_GROUP_SIZE):
u = random.choice(possible_infantry_units)
for unit in units[1:]:
position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp.id, u),
u,
namegen.next_infantry_name(side, cp.id, unit),
unit.dcs_unit_type,
position=position,
group_size=1,
heading=forward_heading,
@@ -304,7 +317,7 @@ class GroundConflictGenerator:
)
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45) * 60))
# TODO: Update to fire at group instead of point
fire_task = FireAtPoint(target, len(gen_group.units) * 10, 100)
fire_task = FireAtPoint(target, gen_group.size * 10, 100)
fire_task.number = 2 if stance != CombatStance.RETREAT else 1
dcs_group.add_trigger_action(fire_task)
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
@@ -347,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
@@ -494,7 +506,7 @@ class GroundConflictGenerator:
return
for dcs_group, group in ally_groups:
if hasattr(group.units[0], "eplrs") and group.units[0].eplrs:
if group.unit_type.eplrs_capable:
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY:
@@ -556,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))
@@ -620,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(
@@ -644,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(
@@ -665,7 +677,7 @@ class GroundConflictGenerator:
Search the enemy groups for a potential target suitable to an artillery unit
"""
# TODO: Update to return a list of groups instead of a single point
rng = group.units[0].threat_range
rng = getattr(group.unit_type.dcs_unit_type, "threat_range", 0)
if not enemy_groups:
return None
for _ in range(10):
@@ -682,7 +694,7 @@ class GroundConflictGenerator:
"""
For artilery group, decide the distance from frontline with the range of the unit
"""
rg = group.units[0].threat_range - 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],
@@ -702,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)
)
@@ -715,7 +727,7 @@ class GroundConflictGenerator:
def _generate_groups(
self,
groups: List[CombatGroup],
groups: list[CombatGroup],
frontline_vector: Tuple[Point, int, int],
is_player: bool,
) -> List[Tuple[VehicleGroup, CombatGroup]]:
@@ -746,16 +758,15 @@ class GroundConflictGenerator:
if final_position is not None:
g = self._generate_group(
self.mission.country(country),
group.units[0],
len(group.units),
group.unit_type,
group.size,
final_position,
distance_from_frontline,
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]:
@@ -773,31 +784,29 @@ class GroundConflictGenerator:
def _generate_group(
self,
side: Country,
unit: VehicleType,
unit_type: GroundUnitType,
count: int,
at: Point,
distance_from_frontline,
move_formation: PointAction = PointAction.OffRoad,
heading=0,
heading: int = 0,
) -> VehicleGroup:
if side == self.conflict.attackers_country:
cp = self.conflict.from_cp
cp = self.conflict.blue_cp
else:
cp = self.conflict.to_cp
cp = self.conflict.red_cp
logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group(
side,
namegen.next_unit_name(side, cp.id, unit),
unit,
namegen.next_unit_name(side, cp.id, unit_type),
unit_type.dcs_unit_type,
position=at,
group_size=count,
heading=heading,
move_formation=move_formation,
)
self.unit_map.add_front_line_units(group, cp)
self.unit_map.add_front_line_units(group, cp, unit_type)
for c in range(count):
vehicle: Vehicle = group.units[c]

View File

@@ -8,6 +8,8 @@ example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight.
"""
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass, field
@@ -65,6 +67,10 @@ class Package:
waypoints: Optional[PackageWaypoints] = field(default=None)
@property
def has_players(self) -> bool:
return any(flight.client_count for flight in self.flights)
@property
def formation_speed(self) -> Optional[Speed]:
"""The speed of the package when in formation.
@@ -162,9 +168,10 @@ class Package:
# likely to be the main task than others. For example, a package with
# only CAP flights is a CAP package, a flight with CAP and strike is a
# strike package, a flight with CAP and DEAD is a DEAD package, and a
# flight with strike and SEAD is an OCA/Strike package. The type of
# package is determined by the highest priority flight in the package.
task_priorities = [
# flight with strike and SEAD is an OCA/Strike package. This list defines the
# priority order for package task names. The package's primary task will be the
# first task in this list that matches a flight in the package.
tasks_by_priority = [
FlightType.CAS,
FlightType.STRIKE,
FlightType.ANTISHIP,
@@ -172,14 +179,16 @@ class Package:
FlightType.OCA_RUNWAY,
FlightType.BAI,
FlightType.DEAD,
FlightType.TRANSPORT,
FlightType.SEAD,
FlightType.TARCAP,
FlightType.BARCAP,
FlightType.AEWC,
FlightType.REFUELING,
FlightType.SWEEP,
FlightType.ESCORT,
]
for task in task_priorities:
for task in tasks_by_priority:
if flight_counts[task]:
return task

View File

@@ -36,8 +36,8 @@ class CommInfo:
class FrontLineInfo:
def __init__(self, front_line: FrontLine):
self.front_line: FrontLine = front_line
self.player_base: ControlPoint = front_line.control_point_a
self.enemy_base: ControlPoint = front_line.control_point_b
self.player_base: ControlPoint = front_line.blue_cp
self.enemy_base: ControlPoint = front_line.red_cp
self.player_zero: bool = self.player_base.base.total_armor == 0
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
self.advantage: bool = (
@@ -164,7 +164,7 @@ class BriefingGenerator(MissionInfoGenerator):
def _generate_frontline_info(self) -> None:
"""Build FrontLineInfo objects from FrontLine type and append to briefing."""
for front_line in self.game.theater.conflicts(from_player=True):
for front_line in self.game.theater.conflicts():
self.add_frontline(FrontLineInfo(front_line))
# TODO: This should determine if runway is friendly through a method more robust than the existing string match

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]

47
gen/cargoshipgen.py Normal file
View File

@@ -0,0 +1,47 @@
from __future__ import annotations
import itertools
from typing import TYPE_CHECKING
from dcs import Mission
from dcs.ships import HandyWind
from dcs.unitgroup import ShipGroup
from game.transfers import CargoShip
from game.unitmap import UnitMap
from game.utils import knots
if TYPE_CHECKING:
from game import Game
class CargoShipGenerator:
def __init__(self, mission: Mission, game: Game, unit_map: UnitMap) -> None:
self.mission = mission
self.game = game
self.unit_map = unit_map
self.count = itertools.count()
def generate(self) -> None:
# Reset the count to make generation deterministic.
for ship in self.game.transfers.cargo_ships:
self.generate_cargo_ship(ship)
def generate_cargo_ship(self, ship: CargoShip) -> ShipGroup:
country = self.mission.country(
self.game.player_country if ship.player_owned else self.game.enemy_country
)
waypoints = ship.route
group = self.mission.ship_group(
country,
ship.name,
HandyWind,
position=waypoints[0],
group_size=1,
)
for waypoint in waypoints[1:]:
# 12 knots is very slow but it's also nearly the max allowed by DCS for this
# type of ship.
group.add_waypoint(waypoint, speed=knots(12).kph)
self.unit_map.add_cargo_ship(group, ship)
return group

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,19 +1,24 @@
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)
self.add_unit(
MissilesSS.AShM_Silkworm_SR,
MissilesSS.Silkworm_SR,
"SR#0",
self.position.x,
self.position.y,
@@ -23,7 +28,7 @@ class SilkwormGenerator(GroupGenerator):
# Launchers
for i, p in enumerate(positions):
self.add_unit(
MissilesSS.AShM_SS_N_2_Silkworm,
MissilesSS.Hy_launcher,
"Missile#" + str(i),
p[0],
p[1],
@@ -32,7 +37,7 @@ class SilkwormGenerator(GroupGenerator):
# Commander
self.add_unit(
Unarmed.Truck_KAMAZ_43101,
Unarmed.KAMAZ_Truck,
"KAMAZ#0",
self.position.x - 35,
self.position.y - 20,
@@ -41,7 +46,7 @@ class SilkwormGenerator(GroupGenerator):
# Shorad
self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.ZSU_23_4_Shilka,
"SHILKA#0",
self.position.x - 55,
self.position.y - 38,
@@ -50,7 +55,7 @@ class SilkwormGenerator(GroupGenerator):
# Shorad 2
self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
AirDefence.Strela_1_9P31,
"STRELA#0",
self.position.x + 200,
self.position.y + 15,

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import logging
from typing import Tuple, Optional
@@ -17,8 +19,7 @@ class Conflict:
def __init__(
self,
theater: ConflictTheater,
from_cp: ControlPoint,
to_cp: ControlPoint,
front_line: FrontLine,
attackers_side: str,
defenders_side: str,
attackers_country: Country,
@@ -33,39 +34,47 @@ class Conflict:
self.attackers_country = attackers_country
self.defenders_country = defenders_country
self.from_cp = from_cp
self.to_cp = to_cp
self.front_line = front_line
self.theater = theater
self.position = position
self.heading = heading
self.size = size
@property
def blue_cp(self) -> ControlPoint:
return self.front_line.blue_cp
@property
def red_cp(self) -> ControlPoint:
return self.front_line.red_cp
@classmethod
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline
@classmethod
def frontline_position(
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
cls, frontline: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int]:
frontline = FrontLine(from_cp, to_cp, theater)
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
def frontline_vector(
cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
cls, front_line: FrontLine, theater: ConflictTheater
) -> Tuple[Point, int, int]:
"""
Returns a vector for a valid frontline location avoiding exclusion zones.
"""
center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
center_position, heading = cls.frontline_position(front_line, theater)
left_heading = heading_sum(heading, -90)
right_heading = heading_sum(heading, 90)
left_position = cls.extend_ground_position(
@@ -84,18 +93,16 @@ class Conflict:
defender_name: str,
attacker: Country,
defender: Country,
from_cp: ControlPoint,
to_cp: ControlPoint,
front_line: FrontLine,
theater: ConflictTheater,
):
assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
) -> 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(
position=position,
heading=heading,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
front_line=front_line,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
@@ -135,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.
@@ -150,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

92
gen/convoygen.py Normal file
View File

@@ -0,0 +1,92 @@
from __future__ import annotations
import itertools
from typing import TYPE_CHECKING
from dcs import Mission
from dcs.mapping import Point
from dcs.point import PointAction
from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from game.dcs.groundunittype import GroundUnitType
from game.transfers import Convoy
from game.unitmap import UnitMap
from game.utils import kph
if TYPE_CHECKING:
from game import Game
class ConvoyGenerator:
def __init__(self, mission: Mission, game: Game, unit_map: UnitMap) -> None:
self.mission = mission
self.game = game
self.unit_map = unit_map
self.count = itertools.count()
def generate(self) -> None:
# Reset the count to make generation deterministic.
for convoy in self.game.transfers.convoys:
self.generate_convoy(convoy)
def generate_convoy(self, convoy: Convoy) -> VehicleGroup:
group = self._create_mixed_unit_group(
convoy.name,
convoy.route_start,
convoy.units,
convoy.player_owned,
)
group.add_waypoint(
convoy.route_end,
speed=kph(40).kph,
move_formation=PointAction.OnRoad,
)
self.make_drivable(group)
self.unit_map.add_convoy_units(group, convoy)
return group
def _create_mixed_unit_group(
self,
name: str,
position: Point,
units: dict[GroundUnitType, int],
for_player: bool,
) -> VehicleGroup:
country = self.mission.country(
self.game.player_country if for_player else self.game.enemy_country
)
unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0]
group = self.mission.vehicle_group(
country,
name,
main_unit_type.dcs_unit_type,
position=position,
group_size=main_unit_count,
move_formation=PointAction.OnRoad,
)
unit_name_counter = itertools.count(main_unit_count + 1)
# pydcs spreads units out by 20 in the Y axis by default. Pick up where it left
# off.
y = itertools.count(position.y + main_unit_count * 20, 20)
for unit_type, count in unit_types[1:]:
for i in range(count):
v = self.mission.vehicle(
f"{name} Unit #{next(unit_name_counter)}", unit_type.dcs_unit_type
)
v.position.x = position.x
v.position.y = next(y)
v.heading = 0
group.add_unit(v)
return group
@staticmethod
def make_drivable(group: VehicleGroup) -> None:
for v in group.units:
if isinstance(v, Vehicle):
v.player_can_drive = True

View File

@@ -1,21 +1,33 @@
import random
from typing import Optional
from dcs.vehicles import Armor
from dcs.unitgroup import VehicleGroup
from game import db
from game import db, Game
from game.data.groundunitclass import GroundUnitClass
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.defenses.armored_group_generator import (
ArmoredGroupGenerator,
FixedSizeArmorGroupGenerator,
)
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
"""
armor_types = (
GroundUnitClass.Apc,
GroundUnitClass.Atgm,
GroundUnitClass.Ifv,
GroundUnitClass.Tank,
)
possible_unit = [
u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()
u for u in db.FACTIONS[faction].frontline_units if u.unit_class in armor_types
]
if len(possible_unit) > 0:
unit_type = random.choice(possible_unit)
@@ -23,7 +35,9 @@ def generate_armor_group(faction: str, game, ground_object):
return None
def generate_armor_group_of_type(game, ground_object, unit_type):
def generate_armor_group_of_type(
game: Game, ground_object: VehicleGroupGroundObject, unit_type: GroundUnitType
) -> VehicleGroup:
"""
This generate a group of ground units of given type
:return: Generated group
@@ -33,7 +47,12 @@ def generate_armor_group_of_type(game, ground_object, unit_type):
return generator.get_generated_group()
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
def generate_armor_group_of_type_and_size(
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
size: int,
) -> VehicleGroup:
"""
This generate a group of ground units of given type and size
:return: Generated group

View File

@@ -1,15 +1,22 @@
import random
from gen.sam.group_generator import GroupGenerator
from game import Game
from game.dcs.groundunittype import GroundUnitType
from game.theater.theatergroundobject import VehicleGroupGroundObject
from gen.sam.group_generator import VehicleGroupGenerator
class ArmoredGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type):
super(ArmoredGroupGenerator, self).__init__(game, ground_object)
class ArmoredGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
def __init__(
self,
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
) -> None:
super().__init__(game, ground_object)
self.unit_type = unit_type
def generate(self):
def generate(self) -> None:
grid_x = random.randint(2, 3)
grid_y = random.randint(1, 2)
@@ -20,7 +27,7 @@ class ArmoredGroupGenerator(GroupGenerator):
for j in range(grid_y):
index = index + 1
self.add_unit(
self.unit_type,
self.unit_type.dcs_unit_type,
"Armor#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j,
@@ -28,20 +35,26 @@ class ArmoredGroupGenerator(GroupGenerator):
)
class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size):
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
class FixedSizeArmorGroupGenerator(VehicleGroupGenerator[VehicleGroupGroundObject]):
def __init__(
self,
game: Game,
ground_object: VehicleGroupGroundObject,
unit_type: GroundUnitType,
size: int,
) -> None:
super().__init__(game, ground_object)
self.unit_type = unit_type
self.size = size
def generate(self):
def generate(self) -> None:
spacing = random.randint(20, 70)
index = 0
for i in range(self.size):
index = index + 1
self.add_unit(
self.unit_type,
self.unit_type.dcs_unit_type,
"Armor#" + str(index),
self.position.x + spacing * i,
self.position.y,

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

@@ -2,11 +2,11 @@ import random
from gen.sam.group_generator import ShipGroupGenerator
from dcs.ships import DDG_Arleigh_Burke_IIa, CG_Ticonderoga
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":
@@ -22,7 +22,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
# Add Arleigh Burke escort
self.add_unit(
DDG_Arleigh_Burke_IIa,
USS_Arleigh_Burke_IIa,
"USS Ramage",
self.position.x + 6482,
self.position.y + 6667,
@@ -30,7 +30,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
USS_Arleigh_Burke_IIa,
"USS Mitscher",
self.position.x - 7963,
self.position.y + 7037,
@@ -38,7 +38,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
USS_Arleigh_Burke_IIa,
"USS Forrest Sherman",
self.position.x - 7408,
self.position.y - 7408,
@@ -46,7 +46,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
USS_Arleigh_Burke_IIa,
"USS Lassen",
self.position.x + 8704,
self.position.y - 6296,
@@ -56,7 +56,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
# Add Ticonderoga escort
if self.heading >= 180:
self.add_unit(
CG_Ticonderoga,
TICONDEROG,
"USS Hué City",
self.position.x + 2222,
self.position.y - 3333,
@@ -64,7 +64,7 @@ class CarrierGroupGenerator(ShipGroupGenerator):
)
else:
self.add_unit(
CG_Ticonderoga,
TICONDEROG,
"USS Hué City",
self.position.x - 3333,
self.position.y + 2222,

View File

@@ -3,24 +3,23 @@ from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
Type_052C_Destroyer,
Type_052B_Destroyer,
Type_054A_Frigate,
Type_052C,
Type_052B,
Type_054A,
)
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])
@@ -30,14 +29,14 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
if include_frigate:
self.add_unit(
Type_054A_Frigate,
Type_054A,
"FF1",
self.position.x + 1200,
self.position.y + 900,
self.heading,
)
self.add_unit(
Type_054A_Frigate,
Type_054A,
"FF2",
self.position.x + 1200,
self.position.y - 900,
@@ -45,7 +44,7 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
)
if include_dd:
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
dd_type = random.choice([Type_052C, Type_052B])
self.add_unit(
dd_type,
"DD1",
@@ -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_Frigate
game, ground_object, faction, Type_054A
)

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Type
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 TheaterGroundObject
from game.theater.theatergroundobject import ShipGroundObject
from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType
from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa
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: ShipType,
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, FFG_Oliver_Hazzard_Perry
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, DDG_Arleigh_Burke_IIa
game, ground_object, faction, USS_Arleigh_Burke_IIa
)

View File

@@ -0,0 +1,13 @@
from dcs.ships import La_Combattante_II
from game import Game
from game.factions.faction import Faction
from game.theater.theatergroundobject import ShipGroundObject
from gen.fleet.dd_group import DDGroupGenerator
class LaCombattanteIIGroupGenerator(DDGroupGenerator):
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,29 +1,29 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
Corvette_1124_4_Grisha,
Corvette_1241_1_Molniya,
Frigate_11540_Neustrashimy,
Frigate_1135M_Rezky,
Cruiser_1164_Moskva,
SSK_877V_Kilo,
SSK_641B_Tango,
ALBATROS,
MOLNIYA,
NEUSTRASH,
REZKY,
MOSCOW,
KILO,
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])
@@ -37,9 +37,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True
if include_frigate:
frigate_type = random.choice(
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
)
frigate_type = random.choice([ALBATROS, MOLNIYA])
self.add_unit(
frigate_type,
"FF1",
@@ -56,7 +54,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
)
if include_dd:
dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky])
dd_type = random.choice([NEUSTRASH, REZKY])
self.add_unit(
dd_type,
"DD1",
@@ -74,9 +72,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
if include_cc:
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
# See https://github.com/Khopa/dcs_liberation/issues/567
# See https://github.com/dcs-liberation/dcs_liberation/issues/567
self.add_unit(
Cruiser_1164_Moskva,
MOSCOW,
"CC1",
self.position.x,
self.position.y,
@@ -87,36 +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, Corvette_1124_4_Grisha
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, Corvette_1241_1_Molniya
game, ground_object, faction, MOLNIYA
)
class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(
self, game: Game, ground_object: TheaterGroundObject, faction: Faction
):
super(KiloSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_877V_Kilo
)
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
):
super(TangoSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_641B_Tango
)
def __init__(self, game: Game, ground_object: ShipGroundObject, faction: Faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SOM)

View File

@@ -1,16 +1,16 @@
import random
from dcs.ships import Boat_Schnellboot_type_S130
from dcs.ships import Schnellboot_type_S130
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(
Boat_Schnellboot_type_S130,
Schnellboot_type_S130,
"Schnellboot" + str(i),
self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100),

View File

@@ -1,13 +1,24 @@
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 (
ArleighBurkeGroupGenerator,
OliverHazardPerryGroupGenerator,
)
from gen.fleet.lacombattanteII import LaCombattanteIIGroupGenerator
from gen.fleet.lha_group import LHAGroupGenerator
from gen.fleet.ru_dd_group import (
RussianNavyGroupGenerator,
@@ -20,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,
@@ -34,13 +48,16 @@ SHIP_MAP = {
"KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator,
"LaCombattanteIIGroupGenerator": LaCombattanteIIGroupGenerator,
}
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:
@@ -59,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

@@ -1,16 +1,16 @@
import random
from dcs.ships import U_boat_VIIC_U_flak
from dcs.ships import Uboat_VIIC
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(
U_boat_VIIC_U_flak,
Uboat_VIIC,
"Uboat" + str(i),
self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100),

View File

@@ -1,16 +1,16 @@
import random
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
from dcs.ships import USS_Samuel_Chase, LST_Mk2
from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self):
def generate(self) -> None:
# Add LS Samuel Chase
self.add_unit(
LS_Samuel_Chase,
USS_Samuel_Chase,
"SamuelChase",
self.position.x,
self.position.y,
@@ -19,7 +19,7 @@ class WW2LSTGroupGenerator(ShipGroupGenerator):
for i in range(1, random.randint(3, 4)):
self.add_unit(
LST_Mk_II,
LST_Mk2,
"LST" + str(i),
self.position.x + i * random.randint(800, 1200),
self.position.y,

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