136 Commits

Author SHA1 Message Date
Raffson
0a32d13201 Avoid appending triggers without actions
Resolves #67

The problem turned out to be split-triggers with no actions, which occurs when a STRIKE flight has no escorts in its package. Added a guard for this so the trigger isn't pushed to the mission in such a case.
2023-01-05 18:46:59 +01:00
holyorangejuice
782759bd48 WRL mission yaml updates (#47)
- Removed/changed some units from WRL missions.
- Minor changes across all missions.
- Added frontline length recommendations to each mission yaml.
2022-12-23 12:54:56 +01:00
Raffson
653e507e89 Set "RTB on bingo" false for ferry flights 2022-12-20 20:56:30 +01:00
Raffson
4f9c7a7370 Adjust issue templates 2022-12-20 20:46:33 +01:00
Raffson
c114256b82 Renaming Liberation
Resolves #42
2022-12-20 20:46:32 +01:00
MetalStormGhost
cdd72820b4 A-4E-C mod support v2.1.0 (#43)
* Updated a4ec.py to Community A-4E-C mod version 2.1.0

* Updated changelog.

Updated A-4E mod version in QNewGameWizard.py (#44)

Co-Authored-By: Raffson <Raffson@users.noreply.github.com>
2022-12-19 23:07:35 +01:00
Raffson
4b8e7e972e Relocate layouts.p 2022-12-19 19:08:25 +01:00
Raffson
68b04b0e46 Fix broken factions
Due to the F-16A MLU having a wrong filename, all factions that included it got broken. Also fixed an excessive comma in USN 1985
2022-12-19 19:06:58 +01:00
holyorangejuice
d00b7a176e WRL Factions Update (#41)
Changed WRL blue,red and added red insurgent faction.
2022-12-19 18:48:04 +01:00
Dan Albert
3c3abd484f Fix livery for VF-33 F-14A squadron.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2610.
2022-12-18 22:12:23 +01:00
Dan Albert
9d5eece5a5 Fix encoding issues with the Peru faction.
Not really sure what's going on here, but presumably it's UTF-16 and
UTF-8 fighting.
2022-12-18 22:12:23 +01:00
Dan Albert
f08df08c7e Add changelog notes that were missed in PRs. 2022-12-18 22:12:23 +01:00
DillieKoe
44e5bc646d Add Peru faction.
A new Peru faction with a mirage squadon
2022-12-18 22:08:20 +01:00
ColonelAkirNakesh
74eb8f38f9 Override liveries for Russian aircraft in bluefor faction. 2022-12-18 22:08:20 +01:00
Raffson
5ff8fb170d Only check for ground units in capture triggers 2022-12-18 16:42:40 +01:00
Raffson
39a5b486d3 Start using own pydcs fork 2022-12-18 16:42:39 +01:00
Raffson
6664a139d8 Support for Ka-50 Black Shark 3
Resolves #38
2022-12-18 16:42:39 +01:00
Raffson
d5de5b3a78 Stack aircraft when spawning right above CP
Should prevent mid-air collisions in most cases, though I'm still worried about "off map" spawns that can possibly collide, though an easy fix would be to manually use time-spacing.
Alternatively we need to treat it as a special case, assigning different altitudes to avoid collisions during the first leg of the flight if that turns out to be the case...
2022-12-18 16:42:38 +01:00
Raffson
c3024e38a6 Update WRL factions 2022-12-17 16:03:32 +01:00
Raffson
cfd05400d2 Update changelog 2022-12-17 16:03:31 +01:00
Raffson
64992a2949 Wrap target description for strike waypoints on kneeboard
Resolve #39
2022-12-17 16:02:29 +01:00
Raffson
8ea7656362 Fix exception when trying to add extra waypoints 2022-12-17 16:02:28 +01:00
Raffson
cd760c45a8 Updating WRL campaigns 2022-12-17 16:02:28 +01:00
MetalStormGhost
15f6c8dd85 Add new performance option: Disable idle aircraft at airfields (#37) 2022-12-16 18:59:21 +01:00
MetalStormGhost
7d2dd0bcbe Add option to only generate night missions (#36)
Replaces the existing checkbox to disable night missions with a selector that allows you to choose between Day/Night/Day&Night
2022-12-13 19:08:56 +01:00
Raffson
608525d5b4 Add game master slot & more observer/commander slots 2022-12-11 21:36:41 +01:00
Raffson
49d4d508d4 Add F-15E payload for DEAD 2022-12-11 21:35:43 +01:00
Raffson
0e286ab7b5 Include B-1B payload for DEAD 2022-12-11 19:36:01 +01:00
dependabot[bot]
4e27591afa Bump certifi from 2022.9.24 to 2022.12.7 (#34)
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.9.24 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.09.24...2022.12.07)

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

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-11 12:21:48 +01:00
Raffson
01c5f3e2a9 Rename F-16A icon
Fixes crash trying to access AIRCRAFT_ICONS["F-16A"]
2022-12-10 19:06:17 +01:00
Raffson
436d2f87da Add F-16A MLU
Resolves #10
2022-12-10 17:28:02 +01:00
Raffson
2e2a397312 Increase Mirage F1's priority for RUNWAY_ATTACK 2022-12-10 17:28:02 +01:00
Raffson
54a5461ecb Remove duplicate campaign 2022-12-10 17:28:01 +01:00
Raffson
49ddd5ce96 Add links in descriptions of WRL's factions and campaigns 2022-12-10 17:28:01 +01:00
Raffson
e9c1e7b770 Allow external links in Campaign/Faction description 2022-12-10 17:28:01 +01:00
Raffson
c298e5bc24 Add campaign: Kutaisi to Vaziani by WRL
Resolves #31

Co-Authored-By: holyorangejuice <4298744+holyorangejuice@users.noreply.github.com>
2022-12-10 17:28:00 +01:00
MetalStormGhost
48e5e7ca95 F-100 Super Sabre mod version 2.7.18.30765 patch 20.10.22 support (#32)
Also makes F-105G "SEAD Escort" capabale
2022-12-10 17:24:22 +01:00
MetalStormGhost
55fb5befb9 F-104C is now correctly removed from the faction when the mod is not enabled. (#33) 2022-12-10 17:17:35 +01:00
Raffson
22c7e21a66 Add battle4area51 by Oscar Juliet
Still WIP as the IADS network is not fully connected

Co-Authored-By: holyorangejuice <4298744+holyorangejuice@users.noreply.github.com>
2022-12-04 21:32:23 +01:00
Raffson
a88f48f3ce Bump campaign version 2022-12-04 21:27:43 +01:00
Raffson
6c673697f0 Fix livery overrides 2022-12-04 13:40:11 +01:00
MetalStormGhost
4af7255917 Cull FlightType.TRANSPORT, FlightType.AEWC & FlightType.REFUELING surroundings
Don't create culling exclusion zones around FlightType.TRANSPORT, FlightType.AEWC & FlightType.REFUELING mission targets. (#30)
2022-12-03 22:12:35 +01:00
MetalStormGhost
dfa33724e9 B1-B bomber strike ingress now has expend=Expend.All and WeaponType.Guided strike mission. 2022-12-03 22:43:20 +02:00
Raffson
568fd35ddc Fix CAS' landing WPT 2022-12-03 16:31:03 +01:00
MetalStormGhost
bc674edb3a Updated the Caucasus landmap (#28)
It is now also suitable for campaigns set in the forested regions of Caucasus. There are corridors for ground forces from Apsheronsk to Tuapse and from Maykop to Sochi. Also, the hilly area around Ambrolauri can now be used in campaigns as well.
2022-12-03 16:25:29 +01:00
MetalStormGhost
f5c7935993 GroundForcePainter for applying custom liveries to ground units and ships (#26)
See #26 for more info
2022-12-03 16:11:22 +01:00
MetalStormGhost
0c30ce3bb3 Revert "Revert "Add support for the AI-only F-14A."" (#27)
This reverts commit 920c9b15ff.
2022-12-03 14:31:40 +01:00
Raffson
e38a6cd907 Support defining max frontline length in campaign
Resolve #25
2022-12-02 23:08:47 +01:00
Raffson
dd98a1d078 Update operation_vectrons_claw.yaml 2022-12-02 23:08:46 +01:00
Raffson
c9611cca99 Update F-14B payloads 2022-12-02 23:08:46 +01:00
RndName
8a17184ccc Fix manual debrief submit not ending mission 2022-12-02 23:08:46 +01:00
Dan Albert
e5b193f21a Changelog section for 6.1. 2022-12-02 23:08:45 +01:00
Raffson
b9c62bc44a primary flight dead => escort RTB
Make sure trigger isn't activated when split flag was already set to true
2022-12-02 23:08:45 +01:00
Raffson
243ded448d Improve bomber tasking 2022-12-02 23:08:44 +01:00
Raffson
ae561e5564 Remove 'initial' wpt from STRIKE 2022-12-02 21:37:08 +01:00
ColonelAkirNakesh
5ffb8cbd96 Update uk_1990.json (#24)
Replaced USS Tarawa with HMS Invincible.
2022-11-29 21:58:17 +01:00
Raffson
07d0e555e5 Limit spacing 2022-11-27 21:09:51 +01:00
Raffson
e5c80ef3af Move Bronco down in CAS list, lowering priority 2022-11-27 21:09:13 +01:00
Raffson
11cc62d9a9 Remove py
Attempt to remove this as it seems to be unused and according to dependabot contains a ReDoS vulnerability
2022-11-26 21:45:25 +01:00
MetalStormGhost
8c9f07643f Add some South Atlantic naval assets (#23)
- Unit yamls for several ships and added some new ones to uk_1990.json
- 2 new layouts: Aircraft Carrier / LHA with Frigate escorts
- Add "HMS Invincible" as Control Point
2022-11-26 21:01:16 +01:00
Raffson
85607b97d5 Update ai_flight_planner_db.py 2022-11-26 17:50:01 +01:00
Raffson
0bc67adea8 Remove references to liberation in issue templates 2022-11-26 17:38:00 +01:00
Starfire13
1cfab07b8b Updates Khopa's Normandy and Channel campaign to 10.0
Note that Operation Dynamo only requires a yaml file update. The .miz file is fine and is not included here.
2022-11-26 17:38:00 +01:00
Dan Albert
499e63e2c3 Unfilter the custom waypoint targets.
There doesn't appear to be any reason for us to be poking at
implementation details here aside from changing the name from "unit" to
"building" for that case. Just iterate over the known strike targets.

Making this change uncovered some latent type errors.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2564.
2022-11-26 17:38:00 +01:00
Dan Albert
bc8624d605 Remove dead code. 2022-11-26 17:37:59 +01:00
Raffson
11a3c49464 Correct radios for the MB-339A.
Cherry picked from e208df16b2ed9ea50e93b16d2eb8b8f4ffb22e82

Co-Authored-By: Dan Albert <danalbert@google.com>
2022-11-26 17:37:59 +01:00
Dan Albert
23becb49b2 Update RoleplayingPleb's campaigns.
https://github.com/dcs-liberation/dcs_liberation/issues/2558
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2561.
2022-11-26 17:37:59 +01:00
Dan Albert
72b594934e Make the casualty report scrollable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2567.
2022-11-26 17:37:58 +01:00
Raffson
d535d312a5 Update requirements.txt 2022-11-26 17:37:58 +01:00
RndName
4b0215c16f Remove dcs capture event from state json
With the latest change we added capture zones and corresponding trigger rules for all Airfields as well so we do not need to rely on the dcs capture event S_EVENT_BASE_CAPTURED anymore.
2022-11-26 16:26:12 +01:00
RndName
8292da414d Add Airfield to list of capture zone types
This will create capture zones and the trigger rules to check for a base capture. Will fix an issue where the dcs capture event is not fired and therefore the capture not recognized by liberation
2022-11-26 16:26:12 +01:00
SnappyComebacks
b2cce52b34 Update pydcs.
Added ice halo generation.
2022-11-26 15:21:52 +01:00
Dan Albert
6ec248dfbf Move develop forward to 7.0. 2022-11-26 15:21:24 +01:00
Dan Albert
634d48e3e7 Attribution compliance for OSM data.
https://github.com/dcs-liberation/dcs_liberation/issues/2434
2022-11-26 15:20:23 +01:00
Dan Albert
391efb1b2b Fuzzle's campaign updates.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2425.
2022-11-26 15:18:51 +01:00
Dan Albert
07e64cfa02 Add Gran Polvorin campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2424.
2022-11-26 15:18:51 +01:00
Dan Albert
594a8dd3cc Add Up the Coast campaign.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2223.
2022-11-26 15:18:50 +01:00
Dan Albert
f5ef509af6 Fix drop zone display for air assault.
Troops must be dropped inside this zone or they won't attack the target.
The zone needs to be drawn in the map so players don't break the flight
plan by accidentally moving the drop waypoint outside the DZ.

I've move the API for doing this out of `PatrollingFlightPlan` in favor
of a mixin so this is no longer presented as `engagement_distance` by
the flight plan. I don't love that it's still the `commit-boundary`
endpoint, but it's fine for now.

I don't know why mypy wasn't able to catch this. pycharm is also
struggling to understand this class.
2022-11-26 15:18:41 +01:00
Raffson
e541111a27 Remove obsolete files 2022-11-20 16:29:19 +01:00
Raffson
c0c310862a Increase default AI skill to Veteran 2022-11-20 16:20:09 +01:00
Dan Albert
65f09c46ca Fix typo. 2022-11-20 16:07:03 +01:00
Dan Albert
f199dd5b9f Note the start time feature in the changelog.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2400.
2022-11-20 16:07:03 +01:00
Dan Albert
56e53219c2 Changelog 6 cleanup.
Typo fixes, sort, recategorize.
2022-11-20 16:07:02 +01:00
Dan Albert
cee00d8315 Update pydcs.
Includes all the new airfields in the South Atlantic.
2022-11-20 16:07:02 +01:00
Dan Albert
cbc124eff6 Hide sim speed controls behind a flag.
Flag is only controlled from the command-line because redoing Qt layout
usually breaks things. Off by default in 6 since this feature is nowhere
near done enough to even be used experimentally (most changes to the ATO
made after the sim begins will break the game).

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2535.
2022-11-20 16:07:02 +01:00
Dan Albert
219b52fc12 Fix adding and removing waypoints in the UI.
This moves more of the logic out of the UI so mypy can actually spot
these mistakes more often.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2495.
2022-11-20 16:07:01 +01:00
Dan Albert
98e8fc88bd Add bug link for refuelzonegeometry. 2022-11-20 16:07:01 +01:00
Dan Albert
9a73a10a09 Remove refueling from sweep flights.
The timing for these doesn't work. Sweep RTBs at the same time the
package reaches its TOT. The tanker won't be on station until 1m30s
before the package reaches the refueling point.
2022-11-20 16:07:01 +01:00
Dan Albert
6ff3a34c14 Fix layout for TARCAP, Escort, and strike-like.
If there's a refuel point, nav from that, not from the patrol end/split.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1749.
2022-11-20 16:07:01 +01:00
Dan Albert
0499ded1b3 Clarify AirAssault flight plan method name.
`engagement_distance` is used elsewhere to mean commit distance, so this
looked like a bug when I stumbled across it. Rename it to be more
explicit.
2022-11-20 16:07:00 +01:00
SnappyComebacks
9fa04702af Wind & Turbulence updates from Liberation
Tune turbulence values.

Modify the range of values used to choose a wind speed.

Wind speed at high elevation IRL can range from 20 to 160 knots around the globe.
You may see wind speed generated here up to 100+ knots, but generally around 40 or so.
IRL wind speed appears to depend on the latitude of the sun, not in this implementation.

Note increased wind speeds in the changelog.

Limit wind speed to 97 knots.

Made minor adjustments to wind speed calculation.

Calculate turbulance.

Turbulance is based off time of day, and day of year.
Each theatre may adjust their turbulance parameters.

Spell turbulence correctly.
2022-11-20 16:07:00 +01:00
Raffson
89c4cc9d79 Advanced IADS: Reconnect from config
Co-Authored-By: RndName <mail@rndname.de>
2022-11-20 16:07:00 +01:00
RndName
b49562f4bc Airlift & AirAssault updates from Liberation
Cleanup and reword refuel waypoints

- rename Stopover back to CargoStop
- precise some waypoint naming

Cleanup and refine airlift and airassault waypoints

- Drop Off and Pickup now correctly worded
- Helo waypoints now represent LandingZones for pickup and dropoff

Forbid planes from air assault.

Make air assault drop-off non-optional.

There is always a drop-off location for troops.

Add docs explaining what the assault area is.

Add error handling for cargo drop waypoints.

Document some airlift waypoint behavior.

Remove unnecessary refuel waypoint in airlifts.

Remove CTLD logic from Airlift flightplan for AI

Add Ingress Point to AirAssault FlightPlan

Add simulation halt at AirAssault ingress

Remove AirAssault completly from AutoPlanner

Remove unneeded LandRefuel from Airlift

Clarify cargo waypoints for AirLift and AirAssault
2022-11-20 16:06:59 +01:00
Nosajthedevil
5f15ddc52c Add OV-10A Bronco Mod 2022-11-20 16:06:59 +01:00
Starfire13
5bde9804d6 Starfire's campaign updates for 10.5. 2022-11-20 16:06:59 +01:00
Raffson
547f1aed75 Use flag to indicate end of escort task 2022-11-19 22:47:33 +01:00
Raffson
5aed3fbb87 Add migrator
Attempts to fix incompatibilities in save files across builds. The idea is to try and handle the incompatibility as graceful as possible, and adding more "incompatibility fixes" in the future.
2022-11-19 22:47:33 +01:00
dependabot[bot]
13cb4d321a Bump loader-utils from 1.4.1 to 1.4.2 in /client (#20)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-19 16:39:49 +01:00
Raffson
004a99e080 Cleanup 2022-11-13 22:38:24 +01:00
Raffson
2aee878a40 Adjust ai flight planner for (sead) escort 2022-11-13 22:38:24 +01:00
Raffson
ca042631b7 Always propose SEAD Escort 2022-11-13 22:38:24 +01:00
Raffson
7e51061582 Formatting 2022-11-13 22:38:23 +01:00
Raffson
e71ff17a23 Fix mypy errors 2022-11-13 22:38:23 +01:00
Raffson
2270300177 Adjust flightplans for escorts 2022-11-13 22:38:23 +01:00
Raffson
3f79fa5b9d Lock speed during strike ingress 2022-11-13 22:38:22 +01:00
Raffson
322a6a700a Reconfigure escort task 2022-11-13 22:38:22 +01:00
Raffson
ba66c18f28 Allow AI to jettison empty tanks 2022-11-13 22:38:22 +01:00
Raffson
5f10154000 Add triggered waypoint switch to strike escorts 2022-11-13 22:38:21 +01:00
Raffson
66290fe1ce Lock speed towards split & set flag upon arrival 2022-11-13 22:38:21 +01:00
Raffson
0c3340b543 Set refueling probability to 10% 2022-11-13 22:38:21 +01:00
Raffson
c7ba4e8db6 Update requirements.txt 2022-11-13 22:38:20 +01:00
Raffson
65ec41c6ce Bump pydcs
Introduces option for jettisoning empty tanks
2022-11-13 22:38:20 +01:00
Raffson
395969a371 Initial attempt at fixing escorts 2022-11-13 22:38:20 +01:00
Raffson
f2f818acb3 Update changelog.md 2022-11-13 22:38:01 +01:00
Raffson
5668c37127 Fine-tune briefing formatting 2022-11-10 17:43:05 +01:00
Raffson
5f67f02cde Add tanker info to briefing 2022-11-10 17:42:47 +01:00
dependabot[bot]
f0fcd7155c Bump loader-utils from 1.4.0 to 1.4.1 in /client (#18)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-10 16:07:57 +01:00
MetalStormGhost
f96348cf59 Add banner for the F-100 & add the VSN Century Series logo for F-4B (#16)
Added a banner for the F-100 Super Sabre (by datoneslav, used with permission) and added the VSN Century Series logo for the F-4B Phantom.
2022-11-10 13:14:48 +01:00
Raffson
f7f4803fdc Update Skynet to 3.0.1 2022-11-07 21:08:27 +01:00
Raffson
810c202f1b Add "save before quit" in closing dialog 2022-11-07 18:46:46 +01:00
Raffson
2d770ae05e Support for MB-339A 2022-11-07 18:46:16 +01:00
Raffson
5dfeb60a13 Tweak weather
Hopefully this prevents the generation of thunderstorms in clear skies
2022-11-05 16:03:19 +01:00
Raffson
5896bdccc1 Fix IADS exception
Exception was caused by `skynet_nodes` trying to generate node for dead units, which would eventually be thrown by `dcs_name_for_group`
2022-11-04 19:30:11 +01:00
Raffson
206c046382 Update build.yml 2022-11-01 21:56:49 +01:00
Raffson
3ce66894f0 Stop loiter 60 seconds earlier 2022-11-01 21:56:49 +01:00
Raffson
cfcdcd0838 Update README.md 2022-10-31 14:58:33 +01:00
Raffson
474980060e Change STRIKE payloads of F-15E to 4xGBU-31 2022-10-31 14:39:12 +01:00
Raffson
e816b4e58b Update requirements.txt 2022-10-30 20:48:35 +01:00
MetalStormGhost
3f4460ad6f Squadron pilot limits enabled by default (#13) 2022-10-30 19:49:33 +01:00
Raffson
92e2d45f0d Refactor mod labels/checkboxes in NewGameWizard (#12) 2022-10-30 17:09:22 +01:00
Raffson
7e7c920816 Fix for orbit's broken stop condition 2022-10-29 23:54:01 +02:00
Raffson
6606f0f58b Also look for Liberation payloads 2022-10-29 21:08:22 +02:00
MetalStormGhost
b64a0f17eb F-16I mod v2.2 support (by IDF Mods Project) (#9) 2022-10-29 17:12:50 +02:00
Raffson
7a5e878733 Corrected Polish faction Leopard 2 variants
Co-Authored-By: MetalStormGhost <89945461+MetalStormGhost@users.noreply.github.com>
2022-10-29 12:59:08 +02:00
Raffson
5edec0c0cb Update changelog.md 2022-10-29 12:49:06 +02:00
Raffson
8912b1de62 Reduce price of A-6A to 12M 2022-10-28 19:21:35 +02:00
245 changed files with 12343 additions and 1882 deletions

View File

@@ -8,13 +8,13 @@ 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
distributed with Retribution. If you are _requesting_ an update to a campaign, see
https://github.com/dcs-retribution/dcs-retribution/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
https://github.com/dcs-retribution/dcs-retribution/wiki/Campaign-maintenance, please start
there.
Otherwise, delete everything above the line below and fill out the following form. Note:

View File

@@ -0,0 +1,17 @@
---
name: Code Architecture
description: >
Suggest a refactor, or other issues related to the codebase.
labels: ["refactor", "python"]
body:
- type: markdown
attributes:
value: >
Before filing, please search the issues to see if the problem has already been reported.
- type: textarea
attributes:
label: Describe your proposal.
description: >
Indicate where the issue lies and briefly explain how you plan to tackle it.
validations:
required: true

View File

@@ -1,12 +1,12 @@
blank_issues_enabled: false
contact_links:
- name: FAQ
url: https://discord.gg/PXHA6AXw
url: https://discord.gg/b4x34Bg4We
about: Check to see if your issue is in the FAQ.
- name: Manual
url: https://github.com/dcs-liberation/dcs_liberation/wiki/
url: https://github.com/dcs-retribution/dcs-retribution/wiki/
- name: Feature blocking DCS AI bugs
url: https://github.com/dcs-liberation/dcs_liberation#dcs-bugs
url: https://github.com/dcs-retribution/dcs-retribution#dcs-bugs
about: >
A list of known DCS bugs that prevent us from improving AI behavior. Check
the list before filing AI bugs here to see if it's something we know about

View File

@@ -1,21 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
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 [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,31 @@
---
name: Feature Request
description: >
Suggest an idea for this project.
labels: enhancement
body:
- type: markdown
attributes:
value: >
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-retribution/dcs-retribution#dcs-bugs.
- type: textarea
attributes:
label: Is your feature request related to a problem? Please describe.
description: >
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: >
A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
attributes:
label: Additional context
description: >
Add any other context or screenshots about the feature request here.

View File

@@ -1,6 +1,6 @@
---
name: Mod support request
about: Request Liberation support for new mods, or updates to existing mods
about: Request Retribution support for new mods, or updates to existing mods
title: Add/update <mod name>
labels: mod support
assignees: ''

View File

@@ -45,8 +45,7 @@ jobs:
with:
webhook_url: ${{ secrets.discord_webhook_betas }}
title: "Build #${{ github.run_number }} available!"
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n**
https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
message: "**Branch:** ${{ github.ref_name }}\n**Download link:\n** https://github.com/dcs-retribution/dcs-retribution/actions/runs/${{ github.run_id }}"
include_image: true
avatar_url: https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png
username: GitHub Build Notifier

View File

@@ -39,7 +39,7 @@ Currently, Retribution supports the following new features:
Latest release is available here : https://github.com/dcs-retribution/dcs-retribution/releases
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds.
To download preview builds of the next version of DCS Retribution, see https://github.com/dcs-retribution/dcs-retribution/wiki/Betas.
## DCS bugs
@@ -62,7 +62,7 @@ Our plans for future releases can be found on our
[Projects page](https://github.com/dcs-retribution/dcs-retribution/projects).
Each planned release has a Project, and the page for that project has columns for to do,
in progress, and done. Items in the Done column are in the
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Preview-builds)
[preview build](https://github.com/dcs-retribution/dcs-retribution/wiki/Betas)
for that release. Items in the To do column are planned to be added to that release.
## Resources

View File

@@ -1,78 +1,122 @@
# Retribution 1.0.1 (hotfix)
* **[Mission Generation]** Fix serialization issue when STRIKE flight has no escorts
# Retribution 1.0.0
## Features/Improvements
* **[Engine]** Support for DCS v2.8.1.34437.
* **[Briefing]** Add tanker info to mission briefing
* **[Campaign]** Add 5 new campaigns by Oscar Juliet from WRL
* **[Campaign]** Add ability to define livery overrides also for ground/naval units
* **[Data]** Added data to support C-47 Skytrain.
* **[Data]** Added data to support F-16A MLU.
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
* **[Mission Generation]** Add option to configure the maximum front-line length in settings
* **[Mission Generation]** Use Escort & SEAD tasks for Escort & SEAD Escort flights
* **[Mission Generation]** Variable flight-size (2/3/4-ship) for
BAI/ANTISHIP/DEAD/STRIKE/BARCAP/CAS/OCA/AIR-ASSAULT (main) missions
* **[Mission Generation]** Add option to only generate night missions
* **[Modding]** Support for F-15D 'Baz' mod version 1.0
* **[Modding]** Support for Su-30 mod version 2.01B
* **[Modding]** Support for A-6A Intruder version 2.7.5.01
* **[Modding]** Support for F-4B Phantom II mod version v2.7.10.02, patch 2022.10.02
* **[Modding]** Support for F-100 Super Sabre mod versions v2.7.18.01 & 2.7.18.30765 and patches 30.09.22 & 09.10.22
* **[Modding]** Support for F-100 Super Sabre mod versions v2.7.18.01 & 2.7.18.30765 and patch 20.10.22
* **[Modding]** Support for F-105 mod version 2.7.12.23x
* **[Modding]** Support IDF Mod Project F-16I Sufa & F-16D v2.2 mod
* **[Modding]** Support for F-84G mod version 2.5.7.01
* **[Modding]** Updated F-104 mod version support to 2.7.11.222.01
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
* **[UI]** Add livery selector to Air Wing Configurator's squadrons.
* **[Performance]** Added performance option: Maximum front-line unit supply per control point.
* **[Performance]** Added performance option: Disable convoys.
* **[Performance]** Added performance option: Front-line troops prefer roads.
* **[Data]** Added data to support C-47 Skytrain.
* **[Data]** Added data to support KS-19 & SON-9, including support for "AAA Site" layout.
* **[Performance]** Added performance option: Disable idle aircraft at airfields.
* **[Squadrons]** Squadron pilot limits enabled by default.
* **[UI]** Add livery selector to Air Wing Configurator's squadrons.
## Fixes
* **[Mission Generation]** Fixed issue where aircraft carriers would return after being killed.
* **[Mission Generation]** Kneeboard STRIKE coordinates would sometimes get clipped when not fitting.
* **[UI]** Fix exception when trying to add a waypoints to a flightplan.
# Liberation:
# 7.0.0
Saves from 6.x are not compatible with 7.0.
## Features/Improvements
## Fixes
# 6.1.0
Saves from 6.0.0 are compatible with 6.1.0
## Features/Improvements
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
* **[Factions]** Added Peru.
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
## Fixes
* **[Flight Planning]** Fixes CAS flights not having landing waypoints.
* **[Squadrons]** Fixed the livery for the VF-33 F-14A squadron.
* **[UI]** Fixed an issue where manual submit of mission results did not end the mission correctly.
# 6.0.0
Saves from 5.x are not compatible with 6.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.7.18.30348.
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
* **[Mission Generation]** Added performance option to not cull IADS when culling would effect how mission is played at target area.
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
* **[Engine]** Support for DCS 2.8.0.33006.
* **[Factions]** Updated the Faction file structure. Older custom faction files will not work correctly and have to be updated to the new structure.
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. AI does not plan.
* **[Flight Planning]** Added preset formations for different flight types at hold, join, ingress, and split waypoints. Air to Air flights will tend toward line-abreast and spread-four formations. Air to ground flights will tend towards trail formation.
* **[Flight Planning]** Added the ability to plan tankers for recovery on package flights. This mission type will not be planned automatically.
* **[Flight Planning]** Air to Ground flights now have ECM enabled on lock at the join point, and SEAD/DEAD also have ECM enabled on detection and lock at ingress.
* **[Flight Planning]** AWACS flightplan changed from orbit to a racetrack to reduce data link disconnects which were caused by blind spots as a result of the bank angle.
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
* **[Flight Planning]** Added a new helo mission type: AirAssault which can be used to load and transport infantry troops from a pickup zone or a carrier to an enemy CP to capture it.
* **[Flight Planning]** Improved the Airlift mission type so that it now can be enforced within the unit transfer dialog and implemented CTLD support. This allows user to spawn sling loadable crates at the pickup location and fly transport flights.
* **[Mission Generation]** Added an option to fast-forward mission generation until the point of first contact (WIP).
* **[Mission Generation]** Added performance option to not cull IADS when culling would affect how mission is played at target area.
* **[Mission Generation]** Reworked the ground object generation which now uses a new layout system
* **[Mission Generation]** Added information about the modulation (AM/FM) of the assigned frequencies to the kneeboard and assign AM modulation instead of FM for JTAC.
* **[Mission Generation]** Added ice halos.
* **[Mission Generation]** Adjusted wind speeds. Wind speeds at high altitude are generally higher now.
* **[Mission Generation]** Added turbulence. Higher in Summer and Winter, also higher at day time than at nighttime.
* **[Modding]** Updated UH-60L mod version support to 1.3.1
* **[Modding]** Updated the High Digit SAMs implementation and added the HQ-2 as well as the upgraded SA-2 and SA-3 Launchers from the mod. Threat range circles will now also be displayed correctly.
* **[Modding]** Theater information such as climate properties is now moddable.
* **[Modding]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
* **[Modding]** Campaigns can now optionally define their start time by including a time in the `recommended_start_date` field. There is not currently a way to override the start time in the UI.
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
* **[UI]** Added options to the loadout editor for setting properties such as HMD choice.
* **[UI]** Added separate images for the different carrier types.
* **[UI]** Add Accept/Reset buttons to Air Wing Configurator screen.
* **[Campaign]** Allow campaign designers to define default values for the economy settings (starting budget and multiplier).
* **[Plugins]** Allow full support of the SkynetIADS plugin with all advanced features (connection nodes, power sources, command centers) if campaign supports it.
* **[Plugins]** Added support for the CTLD script by ciribob with many possible customization options and updated the JTAC Autolase to the CTLD included script.
## Fixes
* **[Engine]** Fixed issue that prevented some weapon types like torpedoes from being recognized.
* **[Flight Planning]** Fixed a miscalculation of waypoint TOTs that would require time travel.
* **[Loadouts]** Improved the range of the F-16 CAS loadout by adding bags.
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
* **[Mission Generation]** Fixed SA-13 incorrectly created as SA-8 Loading Unit which will not be spawned in the generated mission.
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
* **[Mission Generation]** Fixed an issue which generated the helipads at FARPs incorrectly and placed the helicopters within each other.
* **[Mission Generation]** Fixed an issue with SEAD missions flown by the AI when using the Skynet Plugin and anti-radiation missiles (ARM). The AI now correctly engages the SAM when it comes alive instead of diving into it.
* **[Mission Generation]** Fixed an issue where SEAD/DEAD/BAI flights fired all missiles / bombs against a single unit in a group instead of targeting the whole group.
* **[Mission Generation]** Fixed adding additional mission types for a squadron causing error messages when the mission type is not supported by the aircraft type by default
* **[Mission Generation]** AAA ground units now spawn correctly at the frontline
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
* **[Mission Generation]** Fixed generation issue that would cause AI helicopters to get stuck after taking off from a FARP.
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
* **[Mission Generation]** Fixed mission scripting error caused by control points with apostrophes in their names, such as Tha'lah.
* **[Modding]** Campaigns that used quad zones for scenery targets will no longer load. Only circular zones were ever supported, but an implementation quirk allowed them to load in a way that would misbehave. A "No white triggerzones found" message during campaign generation is the sign of a broken campaign.
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
* **[Modding]** Loadouts with invalid weapons (typically new DCS weapons not yet available in Liberation) will be ignored rather than causing an error.
* **[Squadrons]** Fixed issue in air wing configuration that would allow squadrons to be created with no home base if no base was available.
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
* **[UI]** Disable player slots for non-flyable aircraft.
* **[Squadrons]** Helicopter squadrons can no longer be assigned to FOBs that are not FARPs.
* **[UI]** Add vanilla theme weather and time of day icons
* **[UI]** Disable player slots for non-flyable aircraft.
* **[UI]** Fixed and issue where the liberation main exe was still running after application close.
# 5.2.0

View File

@@ -6424,9 +6424,9 @@
}
},
"node_modules/babel-loader/node_modules/loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -13959,9 +13959,9 @@
}
},
"node_modules/loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"dependencies": {
"big.js": "^5.2.2",
@@ -17234,9 +17234,9 @@
}
},
"node_modules/react-dev-utils/node_modules/loader-utils": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
"integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
"integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
"dev": true,
"engines": {
"node": ">= 12.13.0"
@@ -25872,9 +25872,9 @@
}
},
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -31521,9 +31521,9 @@
"dev": true
},
"loader-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz",
"integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
"integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
"dev": true,
"requires": {
"big.js": "^5.2.2",
@@ -33869,9 +33869,9 @@
"dev": true
},
"loader-utils": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz",
"integrity": "sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ==",
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz",
"integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==",
"dev": true
},
"supports-color": {

View File

@@ -1,2 +1,3 @@
from .game import Game
from .version import VERSION
from .migrator import Migrator

View File

@@ -10,6 +10,7 @@ from dcs.helicopters import (
CH_47D,
CH_53E,
Ka_50,
Ka_50_3,
Mi_24P,
Mi_24V,
Mi_26,
@@ -45,11 +46,13 @@ from dcs.planes import (
FW_190A8,
FW_190D9,
F_117A,
F_14A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
F_16A,
F_16A_MLU,
F_16C_50,
F_4E,
F_5E_3,
@@ -116,6 +119,7 @@ from dcs.planes import (
Tu_95MS,
WingLoong_I,
Yak_40,
MB_339A,
)
from dcs.unittype import FlyingType
@@ -126,6 +130,14 @@ from pydcs_extensions.f100.f100 import VSN_F100
from pydcs_extensions.f104.f104 import VSN_F104C, VSN_F104G, VSN_F104S, VSN_F104S_AG
from pydcs_extensions.f105.f105 import VSN_F105D, VSN_F105G
from pydcs_extensions.f15d.f15d import F_15D
from pydcs_extensions.f16i_idf.f16i_idf import (
F_16D_50,
F_16D_52,
F_16D_50_NS,
F_16D_52_NS,
F_16D_Barak,
F_16I,
)
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.f4b.f4b import VSN_F4B
from pydcs_extensions.f84g.f84g import VSN_F84G
@@ -133,6 +145,7 @@ from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.su30.su30 import Su_30MKA, Su_30MKI, Su_30MKM, Su_30SM
from pydcs_extensions.su57.su57 import Su_57
from pydcs_extensions.ov10a.ov10a import Bronco_OV_10A
from pydcs_extensions.uh60l.uh60l import KC130J, UH_60L
from .flighttype import FlightType
@@ -143,13 +156,14 @@ from .flighttype import FlightType
# factions that also have F-4s should not.
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
CAP_CAPABLE = [
ESCORT_CAPABLE = [
Su_57,
F_22A,
F_15C,
F_15D,
F_14B,
F_14A_135_GR,
F_14A,
Su_33,
J_11A,
Su_30,
@@ -160,9 +174,16 @@ CAP_CAPABLE = [
Su_27,
MiG_29S,
F_16C_50,
F_16I,
F_16D_Barak,
F_16D_50,
F_16D_50_NS,
F_16D_52,
F_16D_52_NS,
FA_18C_hornet,
JF_17,
JAS39Gripen,
F_16A_MLU,
F_16A,
F_4E,
VSN_F4B,
@@ -194,7 +215,6 @@ CAP_CAPABLE = [
F_86F_Sabre,
MiG_15bis,
C_101CC,
L_39ZA,
VSN_F84G,
P_51D_30_NA,
P_51D,
@@ -210,6 +230,11 @@ CAP_CAPABLE = [
I_16,
]
# Types to be skipped for escorts
CAP_CAPABLE = ESCORT_CAPABLE + [
L_39ZA,
]
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
@@ -222,8 +247,13 @@ CAS_CAPABLE = [
Su_25,
F_15D,
F_15E,
F_16A,
F_16C_50,
F_16I,
F_16D_Barak,
F_16D_50,
F_16D_50_NS,
F_16D_52,
F_16D_52_NS,
FA_18C_hornet,
Tornado_GR4,
Tornado_IDS,
@@ -231,6 +261,8 @@ CAS_CAPABLE = [
JF_17,
AV8BNA,
A_10A,
F_16A_MLU,
F_16A,
B_1B,
A_4E_C,
F_14B,
@@ -258,6 +290,7 @@ CAS_CAPABLE = [
SA342M,
SA342L,
Ka_50,
Ka_50_3,
Mi_28N,
Mi_24P,
Mi_24V,
@@ -278,6 +311,8 @@ CAS_CAPABLE = [
F_86F_Sabre,
C_101CC,
L_39ZA,
MB_339A,
Bronco_OV_10A,
UH_1H,
A_20G,
Ju_88A4,
@@ -309,17 +344,21 @@ CAS_CAPABLE = [
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
SEAD_CAPABLE = [
SEAD_ESCORT_CAPABLE = [
JF_17,
F_16C_50,
F_16I,
F_16D_Barak,
F_16D_50,
F_16D_50_NS,
F_16D_52,
F_16D_52_NS,
FA_18C_hornet,
Tornado_IDS,
Su_25T,
Su_25TM,
F_4E,
A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA,
Su_24M,
@@ -333,15 +372,21 @@ SEAD_CAPABLE = [
MiG_27K,
Tornado_GR4,
VSN_F105G,
VSN_F100,
]
SEAD_CAPABLE = SEAD_ESCORT_CAPABLE + [
F_16A_MLU,
F_14B,
F_14A_135_GR,
]
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
DEAD_CAPABLE = SEAD_CAPABLE + [
AJS37,
F_14B,
F_14A_135_GR,
F_16A,
F_15E,
JAS39Gripen_AG,
B_1B,
B_52H,
@@ -355,6 +400,7 @@ DEAD_CAPABLE = SEAD_CAPABLE + [
P_47D_30,
P_51D_30_NA,
P_51D,
Bronco_OV_10A,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
@@ -385,9 +431,16 @@ STRIKE_CAPABLE = [
AJS37,
Tornado_GR4,
F_16C_50,
F_16I,
F_16D_Barak,
F_16D_50,
F_16D_50_NS,
F_16D_52,
F_16D_52_NS,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A_MLU,
F_16A,
F_14B,
F_14A_135_GR,
@@ -414,6 +467,7 @@ STRIKE_CAPABLE = [
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
@@ -430,6 +484,7 @@ STRIKE_CAPABLE = [
F_86F_Sabre,
C_101CC,
L_39ZA,
MB_339A,
B_17G,
A_20G,
Ju_88A4,
@@ -461,9 +516,10 @@ ANTISHIP_CAPABLE = [
Tu_142,
Tu_22M3,
H_6J,
F_16A,
FA_18C_hornet,
JAS39Gripen_AG,
F_16A_MLU,
F_16A,
Su_24M,
Su_17M4,
JF_17,
@@ -492,6 +548,14 @@ RUNWAY_ATTACK_CAPABLE = [
JF_17,
Tornado_IDS,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
H_6J,
B_1B,
B_52H,
@@ -500,9 +564,16 @@ RUNWAY_ATTACK_CAPABLE = [
F_15E,
AJS37,
F_16C_50,
F_16I,
F_16D_Barak,
F_16D_50,
F_16D_50_NS,
F_16D_52,
F_16D_52_NS,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A_MLU,
F_16A,
F_14B,
F_14A_135_GR,
@@ -529,15 +600,8 @@ RUNWAY_ATTACK_CAPABLE = [
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
MiG_27K,
MiG_21Bis,
MiG_15bis,
@@ -545,6 +609,7 @@ RUNWAY_ATTACK_CAPABLE = [
F_86F_Sabre,
C_101CC,
L_39ZA,
MB_339A,
B_17G,
A_20G,
Ju_88A4,
@@ -643,7 +708,7 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
elif task == FlightType.SEAD:
return SEAD_CAPABLE
elif task == FlightType.SEAD_ESCORT:
return SEAD_CAPABLE
return SEAD_ESCORT_CAPABLE
elif task == FlightType.DEAD:
return DEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT:
@@ -653,7 +718,7 @@ def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
return ESCORT_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.REFUELING:

View File

@@ -63,6 +63,7 @@ class Flight(SidcDescribable):
self.start_type = start_type
self.use_custom_loadout = False
self.custom_name = custom_name
self.group_id: int = 0
# Only used by transport missions.
self.cargo = cargo
@@ -90,6 +91,12 @@ class Flight(SidcDescribable):
def flight_plan(self) -> FlightPlan[Any]:
return self._flight_plan_builder.get_or_build()
def degrade_to_custom_flight_plan(self) -> None:
from .flightplans.custom import Builder as CustomBuilder
self._flight_plan_builder = CustomBuilder(self, self.flight_plan.waypoints[1:])
self.recreate_flight_plan()
def __getstate__(self) -> dict[str, Any]:
state = self.__dict__.copy()
# Avoid persisting the flight state since that's not (currently) used outside

View File

@@ -4,28 +4,39 @@ from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.airlift import AirliftLayout
from game.ato.flightplans.standard import StandardFlightPlan
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.theater.controlpoint import ControlPointType
from game.theater.missiontarget import MissionTarget
from game.utils import Distance, feet, meters
from .ibuilder import IBuilder
from .planningerror import PlanningError
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
from ..flightwaypoint import FlightWaypointType
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
@dataclass(frozen=True)
class AirAssaultLayout(AirliftLayout):
class AirAssaultLayout(StandardLayout):
# The pickup point is optional because we don't always need to load the cargo. When
# departing from a carrier, LHA, or off-map spawn, the cargo is pre-loaded.
pickup: FlightWaypoint | None
nav_to_ingress: list[FlightWaypoint]
ingress: FlightWaypoint
drop_off: FlightWaypoint
# This is an implementation detail used by CTLD. The aircraft will not go to this
# waypoint. It is used by CTLD as the destination for unloaded troops.
target: FlightWaypoint
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
yield from self.nav_to_pickup
if self.pickup:
if self.pickup is not None:
yield self.pickup
yield from self.nav_to_drop_off
yield from self.nav_to_ingress
yield self.ingress
yield self.drop_off
yield self.target
yield from self.nav_to_home
@@ -35,7 +46,7 @@ class AirAssaultLayout(AirliftLayout):
yield self.bullseye
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -53,35 +64,48 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout]):
return None
@property
def engagement_distance(self) -> Distance:
# The radius of the WaypointZone created at the target location
def ctld_target_zone_radius(self) -> Distance:
return meters(2500)
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
def ui_zone(self) -> UiZone:
return UiZone(
[self.layout.target.position],
self.ctld_target_zone_radius,
)
class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
def layout(self) -> AirAssaultLayout:
if not self.flight.is_helo:
raise PlanningError("Air assault is only usable by helicopters")
assert self.package.waypoints is not None
altitude = feet(1500) if self.flight.is_helo else self.doctrine.ingress_altitude
altitude_is_agl = self.flight.is_helo
builder = WaypointBuilder(self.flight, self.coalition)
if not self.flight.is_helo or self.flight.departure.cptype in [
if self.flight.departure.cptype in [
ControlPointType.AIRCRAFT_CARRIER_GROUP,
ControlPointType.LHA_GROUP,
ControlPointType.OFF_MAP,
]:
# Non-Helo flights or Off_Map will be preloaded
# Off_Map spawns will be preloaded
# Carrier operations load the logistics directly from the carrier
pickup = None
pickup_position = self.flight.departure.position
else:
# TODO The calculation of the Pickup LZ is currently randomized. This
# leads to the problem that we can not gurantee that the LZ is clear of
# obstacles. This has to be improved in the future so that the Mission can
# be autoplanned. In the current state the User has to check the created
# Waypoints for the Pickup and Dropoff LZs are free of obstacles.
# Create a special pickup zone for Helos from Airbase / FOB
pickup = builder.pickup(
pickup = builder.pickup_zone(
MissionTarget(
"Pickup Zone",
self.flight.departure.position.random_point_within(1200, 600),
@@ -90,6 +114,8 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
pickup_position = pickup.position
assault_area = builder.assault_area(self.package.target)
heading = self.package.target.position.heading_between_point(pickup_position)
# TODO we can not gurantee a safe LZ for DropOff. See comment above.
drop_off_zone = MissionTarget(
"Dropoff zone",
self.package.target.position.point_from_heading(heading, 1200),
@@ -97,21 +123,19 @@ class Builder(IBuilder[AirAssaultFlightPlan, AirAssaultLayout]):
return AirAssaultLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_pickup=builder.nav_path(
self.flight.departure.position,
pickup_position,
altitude,
altitude_is_agl,
),
pickup=pickup,
nav_to_drop_off=builder.nav_path(
nav_to_ingress=builder.nav_path(
pickup_position,
drop_off_zone.position,
self.package.waypoints.ingress,
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(drop_off_zone),
stopover=None,
ingress=builder.ingress(
FlightWaypointType.INGRESS_AIR_ASSAULT,
self.package.waypoints.ingress,
self.package.target,
),
drop_off=builder.dropoff_zone(drop_off_zone),
target=assault_area,
nav_to_home=builder.nav_path(
drop_off_zone.position,

View File

@@ -19,10 +19,20 @@ if TYPE_CHECKING:
@dataclass(frozen=True)
class AirliftLayout(StandardLayout):
nav_to_pickup: list[FlightWaypoint]
# There will not be a pickup waypoint when the pickup airfield is the departure
# airfield for cargo planes, as the cargo is pre-loaded. Helicopters will still pick
# up the cargo near the airfield.
pickup: FlightWaypoint | None
# pickup_zone will be used for player flights to create the CTLD stuff
ctld_pickup_zone: FlightWaypoint | None
nav_to_drop_off: list[FlightWaypoint]
drop_off: FlightWaypoint
stopover: FlightWaypoint | None
# There will not be a drop-off waypoint when the drop-off airfield and the arrival
# airfield is the same for a cargo plane, as planes will land to unload and we don't
# want a double landing. Helicopters will still drop their cargo near the airfield
# before landing.
drop_off: FlightWaypoint | None
# drop_off_zone will be used for player flights to create the CTLD stuff
ctld_drop_off_zone: FlightWaypoint | None
nav_to_home: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -30,10 +40,13 @@ class AirliftLayout(StandardLayout):
yield from self.nav_to_pickup
if self.pickup is not None:
yield self.pickup
if self.ctld_pickup_zone is not None:
yield self.ctld_pickup_zone
yield from self.nav_to_drop_off
yield self.drop_off
if self.stopover is not None:
yield self.stopover
if self.drop_off is not None:
yield self.drop_off
if self.ctld_drop_off_zone is not None:
yield self.ctld_drop_off_zone
yield from self.nav_to_home
yield self.arrival
if self.divert is not None:
@@ -48,7 +61,11 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
@property
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.drop_off
# The TOT is the time that the cargo will be dropped off. If the drop-off
# location is the arrival airfield and this is not a helicopter flight, there
# will not be a separate drop-off waypoint; the arrival landing waypoint is the
# drop-off waypoint.
return self.layout.drop_off or self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
# TOT planning isn't really useful for transports. They're behind the front
@@ -77,31 +94,31 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
builder = WaypointBuilder(self.flight, self.coalition)
pickup = None
stopover = None
drop_off = None
pickup_zone = None
drop_off_zone = None
if cargo.origin != self.flight.departure:
pickup = builder.cargo_stop(cargo.origin)
if cargo.next_stop != self.flight.arrival:
drop_off = builder.cargo_stop(cargo.next_stop)
if self.flight.is_helo:
# Create a pickupzone where the cargo will be spawned
pickup_zone = MissionTarget(
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
# Create CTLD Zones for Helo flights
pickup_zone = builder.pickup_zone(
MissionTarget(
"Pickup Zone", cargo.origin.position.random_point_within(1000, 200)
)
)
pickup = builder.pickup(pickup_zone)
# If The cargo is at the departure controlpoint, the pickup waypoint should
# only be created for client flights
pickup.only_for_player = cargo.origin == self.flight.departure
# Create a dropoff zone where the cargo should be dropped
drop_off_zone = MissionTarget(
"Dropoff zone",
cargo.next_stop.position.random_point_within(1000, 200),
drop_off_zone = builder.dropoff_zone(
MissionTarget(
"Dropoff zone",
cargo.next_stop.position.random_point_within(1000, 200),
)
)
drop_off = builder.drop_off(drop_off_zone)
# Add an additional stopover point so that the flight can refuel
stopover = builder.stopover(cargo.next_stop)
else:
# Fixed Wing will get stopover points for pickup and dropoff
if cargo.origin != self.flight.departure:
pickup = builder.stopover(cargo.origin, "PICKUP")
drop_off = builder.stopover(cargo.next_stop, "DROP OFF")
# Show the zone waypoints only to the player
pickup_zone.only_for_player = True
drop_off_zone.only_for_player = True
nav_to_pickup = builder.nav_path(
self.flight.departure.position,
@@ -110,20 +127,11 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
altitude_is_agl,
)
if self.flight.client_count > 0:
# Normal Landing Waypoint
arrival = builder.land(self.flight.arrival)
else:
# The AI Needs another Stopover point to actually fly back to the original
# base. Otherwise the Cargo drop will be the new Landing Waypoint and the
# AI will end its mission there instead of flying back.
# https://forum.dcs.world/topic/211775-landing-to-refuel-and-rearm-the-landingrefuar-waypoint/
arrival = builder.stopover(self.flight.arrival, "LANDING")
return AirliftLayout(
departure=builder.takeoff(self.flight.departure),
nav_to_pickup=nav_to_pickup,
pickup=pickup,
ctld_pickup_zone=pickup_zone,
nav_to_drop_off=builder.nav_path(
cargo.origin.position,
cargo.next_stop.position,
@@ -131,14 +139,14 @@ class Builder(IBuilder[AirliftFlightPlan, AirliftLayout]):
altitude_is_agl,
),
drop_off=drop_off,
stopover=stopover,
ctld_drop_off_zone=drop_off_zone,
nav_to_home=builder.nav_path(
cargo.origin.position,
self.flight.arrival.position,
altitude,
altitude_is_agl,
),
arrival=arrival,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),
)

View File

@@ -10,6 +10,7 @@ from game.utils import Distance, Speed, kph, meters
from .ibuilder import IBuilder
from .invalidobjectivelocation import InvalidObjectiveLocation
from .patrolling import PatrollingFlightPlan, PatrollingLayout
from .uizonedisplay import UiZone, UiZoneDisplay
from .waypointbuilder import WaypointBuilder
from ..flightwaypointtype import FlightWaypointType
@@ -28,13 +29,13 @@ class CasLayout(PatrollingLayout):
yield self.target
yield self.patrol_end
yield from self.nav_from
yield self.departure
yield self.arrival
if self.divert is not None:
yield self.divert
yield self.bullseye
class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
class CasFlightPlan(PatrollingFlightPlan[CasLayout], UiZoneDisplay):
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -65,6 +66,12 @@ class CasFlightPlan(PatrollingFlightPlan[CasLayout]):
def dismiss_escort_at(self) -> FlightWaypoint | None:
return self.layout.patrol_end
def ui_zone(self) -> UiZone:
return UiZone(
[self.layout.target.position],
self.engagement_distance,
)
class Builder(IBuilder[CasFlightPlan, CasLayout]):
def layout(self) -> CasLayout:

View File

@@ -8,6 +8,7 @@ from typing import TYPE_CHECKING, Type
from .flightplan import FlightPlan, Layout
from .ibuilder import IBuilder
from .waypointbuilder import WaypointBuilder
from .. import Flight
from ..flightwaypointtype import FlightWaypointType
if TYPE_CHECKING:
@@ -55,9 +56,17 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
class Builder(IBuilder[CustomFlightPlan, CustomLayout]):
def __init__(
self, flight: Flight, waypoints: list[FlightWaypoint] | None = None
) -> None:
super().__init__(flight)
if waypoints is None:
waypoints = []
self.waypoints = waypoints
def layout(self) -> CustomLayout:
builder = WaypointBuilder(self.flight, self.coalition)
return CustomLayout(builder.takeoff(self.flight.departure), [])
return CustomLayout(builder.takeoff(self.flight.departure), self.waypoints)
def build(self) -> CustomFlightPlan:
return CustomFlightPlan(self.flight, self.layout())

View File

@@ -8,6 +8,8 @@ from .formationattack import (
FormationAttackLayout,
)
from .waypointbuilder import WaypointBuilder
from .. import FlightType
from ...utils import Distance
class EscortFlightPlan(FormationAttackFlightPlan):
@@ -24,12 +26,17 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
ingress, target = builder.escort(
self.package.waypoints.ingress, self.package.target
)
ingress.only_for_player = True
target.only_for_player = True
hold = builder.hold(self._hold_point())
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
refuel = None
if self.package.waypoints.refuel is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
refuel = builder.refuel(self.package.waypoints.refuel)
initial = None
if self.package.primary_task == FlightType.STRIKE:
initial = builder.escort_hold(
self.package.waypoints.initial, Distance.from_feet(20000)
)
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
@@ -39,11 +46,12 @@ class Builder(FormationAttackBuilder[EscortFlightPlan, FormationAttackLayout]):
),
join=join,
ingress=ingress,
initial=initial,
targets=[target],
split=split,
refuel=refuel,
nav_from=builder.nav_path(
split.position,
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),

View File

@@ -4,13 +4,13 @@ from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, TypeVar
from typing import TYPE_CHECKING, TypeVar, Optional
from dcs import Point
from game.flightplan import HoldZoneGeometry
from game.theater import MissionTarget
from game.utils import Speed, meters
from game.utils import Speed, meters, Distance
from .flightplan import FlightPlan
from .formation import FormationFlightPlan, FormationLayout
from .ibuilder import IBuilder
@@ -134,6 +134,7 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
class FormationAttackLayout(FormationLayout):
ingress: FlightWaypoint
targets: list[FlightWaypoint]
initial: Optional[FlightWaypoint] = None
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.departure
@@ -141,6 +142,8 @@ class FormationAttackLayout(FormationLayout):
yield from self.nav_to
yield self.join
yield self.ingress
if self.initial is not None:
yield self.initial
yield from self.targets
yield self.split
if self.refuel is not None:
@@ -181,9 +184,11 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
hold = builder.hold(self._hold_point())
join = builder.join(self.package.waypoints.join)
split = builder.split(self.package.waypoints.split)
refuel = None
if self.package.waypoints.refuel is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
refuel = builder.refuel(self.package.waypoints.refuel)
ingress = builder.ingress(
ingress_type, self.package.waypoints.ingress, self.package.target
)
return FormationAttackLayout(
departure=builder.takeoff(self.flight.departure),
@@ -192,14 +197,12 @@ class FormationAttackBuilder(IBuilder[FlightPlanT, LayoutT], ABC):
hold.position, join.position, self.doctrine.ingress_altitude
),
join=join,
ingress=builder.ingress(
ingress_type, self.package.waypoints.ingress, self.package.target
),
ingress=ingress,
targets=target_waypoints,
split=split,
refuel=refuel,
nav_from=builder.nav_path(
split.position,
refuel.position,
self.flight.arrival.position,
self.doctrine.ingress_altitude,
),

View File

@@ -9,6 +9,7 @@ from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.typeguard import self_type_guard
from game.utils import Distance, Speed
from .uizonedisplay import UiZone, UiZoneDisplay
if TYPE_CHECKING:
from ..flightwaypoint import FlightWaypoint
@@ -37,7 +38,7 @@ class PatrollingLayout(StandardLayout):
LayoutT = TypeVar("LayoutT", bound=PatrollingLayout)
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
@property
@abstractmethod
def patrol_duration(self) -> timedelta:
@@ -97,3 +98,9 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], ABC):
self, flight_plan: FlightPlan[Any]
) -> TypeGuard[PatrollingFlightPlan[Any]]:
return True
def ui_zone(self) -> UiZone:
return UiZone(
[self.layout.patrol_start.position, self.layout.patrol_end.position],
self.engagement_distance,
)

View File

@@ -22,7 +22,6 @@ class SweepLayout(LoiterLayout):
nav_to: list[FlightWaypoint]
sweep_start: FlightWaypoint
sweep_end: FlightWaypoint
refuel: FlightWaypoint | None
nav_from: list[FlightWaypoint]
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -31,8 +30,6 @@ class SweepLayout(LoiterLayout):
yield from self.nav_to
yield self.sweep_start
yield self.sweep_end
if self.refuel is not None:
yield self.refuel
yield from self.nav_from
yield self.arrival
if self.divert is not None:
@@ -92,6 +89,7 @@ class SweepFlightPlan(LoiterFlightPlan):
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
@property
def mission_departure_time(self) -> timedelta:
return self.sweep_end_time
@@ -112,11 +110,6 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
hold = builder.hold(self._hold_point())
refuel = None
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
return SweepLayout(
departure=builder.takeoff(self.flight.departure),
hold=hold,
@@ -130,7 +123,6 @@ class Builder(IBuilder[SweepFlightPlan, SweepLayout]):
),
sweep_start=start,
sweep_end=end,
refuel=refuel,
arrival=builder.land(self.flight.arrival),
divert=builder.divert(self.flight.divert),
bullseye=builder.bullseye(),

View File

@@ -105,9 +105,11 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
start, end = builder.race_track(orbit0p, orbit1p, patrol_alt)
refuel = None
nav_from_origin = orbit1p
if self.package.waypoints is not None:
refuel = builder.refuel(self.package.waypoints.refuel)
nav_from_origin = refuel.position
return TarCapLayout(
departure=builder.takeoff(self.flight.departure),
@@ -115,7 +117,7 @@ class Builder(CapBuilder[TarCapFlightPlan, TarCapLayout]):
self.flight.departure.position, orbit0p, patrol_alt
),
nav_from=builder.nav_path(
orbit1p, self.flight.arrival.position, patrol_alt
nav_from_origin, self.flight.arrival.position, patrol_alt
),
patrol_start=start,
patrol_end=end,

View File

@@ -0,0 +1,18 @@
import abc
from dataclasses import dataclass
from dcs import Point
from game.utils import Distance
@dataclass(frozen=True)
class UiZone:
points: list[Point]
radius: Distance
class UiZoneDisplay(abc.ABC):
@abc.abstractmethod
def ui_zone(self) -> UiZone:
...

View File

@@ -304,6 +304,13 @@ class WaypointBuilder:
return self._target_area(f"ATTACK {target.name}", target, flyover=True)
def assault_area(self, target: MissionTarget) -> FlightWaypoint:
"""A destination waypoint used by air-assault ground troops.
This waypoint is an implementation detail for CTLD and should not be followed by
aircraft.
"""
# TODO: Add a property that can hide this waypoint from the player's flight
# plan.
return self._target_area(f"ASSAULT {target.name}", target)
@staticmethod
@@ -415,6 +422,24 @@ class WaypointBuilder:
pretty_name="Orbit",
)
@staticmethod
def escort_hold(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates custom waypoint for escort flights that need to hold.
Args:
start: Position of the waypoint.
altitude: Altitude of the holding pattern.
"""
return FlightWaypoint(
"ESCORT HOLD",
FlightWaypointType.CUSTOM,
start,
altitude,
description="Anchor and hold at this point",
pretty_name="Escort Hold",
)
@staticmethod
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
"""Creates a sweep start waypoint.
@@ -494,58 +519,50 @@ class WaypointBuilder:
)
@staticmethod
def stopover(stopover: ControlPoint, name: str = "STOPOVER") -> FlightWaypoint:
"""Creates a stopover waypoint.
Args:
control_point: Pick up location.
def pickup_zone(pick_up: MissionTarget) -> FlightWaypoint:
"""Creates a pickup landing zone waypoint
This waypoint is used to generate the Trigger Zone used for AirAssault and
AirLift using the CTLD plugin (see LogisticsGenerator)
"""
return FlightWaypoint(
name,
FlightWaypointType.STOPOVER,
stopover.position,
meters(0),
"RADIO",
description=f"Stopover at {stopover}",
pretty_name="Stopover location",
control_point=stopover,
)
@staticmethod
def pickup(pick_up: MissionTarget) -> FlightWaypoint:
"""Creates a cargo pickup waypoint.
Args:
control_point: Pick up location.
"""
control_point = pick_up if isinstance(pick_up, ControlPoint) else None
return FlightWaypoint(
"PICKUP",
FlightWaypointType.PICKUP,
"PICKUPZONE",
FlightWaypointType.PICKUP_ZONE,
pick_up.position,
meters(0),
"RADIO",
description=f"Pick up cargo from {pick_up.name}",
pretty_name="Pick up location",
control_point=control_point,
pretty_name="Pick-up zone",
)
@staticmethod
def drop_off(drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a cargo drop-off waypoint.
Args:
control_point: Drop-off location.
def dropoff_zone(drop_off: MissionTarget) -> FlightWaypoint:
"""Creates a dropoff landing zone waypoint
This waypoint is used to generate the Trigger Zone used for AirAssault and
AirLift using the CTLD plugin (see LogisticsGenerator)
"""
control_point = drop_off if isinstance(drop_off, ControlPoint) else None
return FlightWaypoint(
"DROP OFF",
FlightWaypointType.DROP_OFF,
"DROPOFFZONE",
FlightWaypointType.DROPOFF_ZONE,
drop_off.position,
meters(0),
"RADIO",
description=f"Drop off cargo at {drop_off.name}",
pretty_name="Drop off location",
pretty_name="Drop-off zone",
)
@staticmethod
def cargo_stop(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo stop waypoint.
This waypoint is used by AirLift as a landing and stopover waypoint
"""
return FlightWaypoint(
"CARGOSTOP",
FlightWaypointType.CARGO_STOP,
control_point.position,
meters(0),
"RADIO",
description=f"Stop for cargo at {control_point.name}",
pretty_name="Cargo stop",
control_point=control_point,
)

View File

@@ -137,6 +137,7 @@ class InFlight(FlightState, ABC):
FlightWaypointType.INGRESS_OCA_RUNWAY,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.INGRESS_AIR_ASSAULT,
}
return self.current_waypoint.waypoint_type in contact_types

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
from collections.abc import Sequence
from dataclasses import dataclass, field
from datetime import timedelta
from typing import Literal, TYPE_CHECKING
@@ -12,8 +11,7 @@ from game.theater.theatergroup import TheaterUnit
from game.utils import Distance, meters
if TYPE_CHECKING:
from game.theater import ControlPoint, MissionTarget
from game.theater import ControlPoint
AltitudeReference = Literal["BARO", "RADIO"]
@@ -32,7 +30,7 @@ class FlightWaypoint:
# having three names. A short and long form is enough.
description: str = ""
targets: Sequence[MissionTarget | TheaterUnit] = field(default_factory=list)
targets: list[TheaterUnit] = field(default_factory=list)
obj_name: str = ""
pretty_name: str = ""
only_for_player: bool = False

View File

@@ -43,8 +43,9 @@ class FlightWaypointType(IntEnum):
DIVERT = 23
INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25
PICKUP = 26
DROP_OFF = 27
PICKUP_ZONE = 26 # Pickup Zone for cargo or troops
DROPOFF_ZONE = 27 # Dropoff Zone for cargo or troops
BULLSEYE = 28
REFUEL = 29 # Should look for nearby tanker to refuel from.
STOPOVER = 30 # Stopover landing point using the LandingReFuAr waypoint type
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
INGRESS_AIR_ASSAULT = 31

View File

@@ -151,10 +151,22 @@ class Loadout:
# names, so those have been included here too. The priority goes from first to
# last - the first element in the tuple will be tried first, then the second,
# etc.
loadout_names = {t: [f"Liberation {t.value}"] for t in FlightType}
loadout_names = {
t: [f"Retribution {t.value}", f"Liberation {t.value}"] for t in FlightType
}
legacy_names = {
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
FlightType.TARCAP: (
"CAP HEAVY",
"CAP",
"Retribution BARCAP",
"Liberation BARCAP",
),
FlightType.BARCAP: (
"CAP HEAVY",
"CAP",
"Retribution TARCAP",
"Liberation TARCAP",
),
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
FlightType.STRIKE: ("STRIKE",),
FlightType.ANTISHIP: ("ANTISHIP",),

View File

@@ -129,6 +129,16 @@ class Package:
if not self.flights:
self.waypoints = None
@property
def primary_flight(self) -> Optional[Flight]:
task = self.primary_task
if not task:
return None
primaries = [x for x in self.flights if x.flight_type == task]
if len(primaries) > 0:
return primaries[0]
return None
@property
def primary_task(self) -> Optional[FlightType]:
if not self.flights:

View File

@@ -1,13 +1,15 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from dcs import Point
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.flightplan import IpZoneGeometry, JoinZoneGeometry
from game.flightplan.refuelzonegeometry import RefuelZoneGeometry
from game.utils import nautical_miles
if TYPE_CHECKING:
from game.ato import Package
@@ -18,6 +20,7 @@ if TYPE_CHECKING:
class PackageWaypoints:
join: Point
ingress: Point
initial: Point
split: Point
refuel: Point
@@ -32,6 +35,11 @@ class PackageWaypoints:
coalition,
).find_best_ip()
hdg = package.target.position.heading_between_point(ingress_point)
# Generate a waypoint randomly between 7 & 9 NM
dist = nautical_miles(random.random() * 2 + 7).meters
initial_point = package.target.position.point_from_heading(hdg, dist)
join_point = JoinZoneGeometry(
package.target.position,
origin.position,
@@ -51,6 +59,7 @@ class PackageWaypoints:
return PackageWaypoints(
WaypointBuilder.perturb(join_point),
ingress_point,
initial_point,
WaypointBuilder.perturb(join_point),
refuel_point,
)

View File

@@ -139,14 +139,6 @@ class ObjectiveFinder:
"""Iterates over all active front lines in the theater."""
yield from self.game.theater.conflicts()
def air_assault_targets(self) -> Iterator[ControlPoint]:
"""Iterates over all capturable controlpoints for all active front lines"""
if not self.game.settings.plugin_option("ctld"):
# Air Assault should only be tasked with CTLD enabled
return
for front_line in self.front_lines():
yield front_line.control_point_hostile_to(self.is_player)
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs.

View File

@@ -7,7 +7,6 @@ from game.commander.tasks.compound.destroyenemygroundunits import (
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
ReduceEnemyFrontLineCapacity,
)
from game.commander.tasks.primitive.airassault import PlanAirAssault
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
from game.commander.theaterstate import TheaterState
from game.htn import CompoundTask, Method
@@ -19,7 +18,6 @@ class CaptureBase(CompoundTask[TheaterState]):
front_line: FrontLine
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
yield [PlanAirAssault(self.enemy_cp(state))]
yield [BreakthroughAttack(self.front_line, state.context.coalition.player)]
yield [DestroyEnemyGroundUnits(self.front_line)]
if self.worth_destroying_ammo_depots(state):

View File

@@ -1,35 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass
from random import randint
from game.ato.flighttype import FlightType
from game.commander.tasks.packageplanningtask import PackagePlanningTask
from game.commander.theaterstate import TheaterState
from game.theater import ControlPoint
@dataclass
class PlanAirAssault(PackagePlanningTask[ControlPoint]):
def preconditions_met(self, state: TheaterState) -> bool:
if self.target not in state.air_assault_targets:
return False
if self.capture_blocked(state):
# Do not task if there are enemy battle_positions blocking the capture
return False
if not self.target_area_preconditions_met(state):
# Do not task if air defense is present in the target area
return False
return super().preconditions_met(state)
def capture_blocked(self, state: TheaterState) -> bool:
battle_positions = state.enemy_battle_positions[self.target]
return len(battle_positions.blocking_capture) > 0
def apply_effects(self, state: TheaterState) -> None:
state.air_assault_targets.remove(self.target)
def propose_flights(self) -> None:
self.propose_flight(FlightType.AIR_ASSAULT, randint(2, 4))
# TODO Validate this.. / is Heli escort possible?
self.propose_flight(FlightType.TARCAP, 2)

View File

@@ -42,6 +42,5 @@ class PlanDead(PackagePlanningTask[IadsGroundObject]):
# needs a decent refactor to the escort planning to do so.
if self.target.has_live_radar_sam:
self.propose_flight(FlightType.SEAD, 2)
else:
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)

View File

@@ -45,7 +45,6 @@ class TheaterState(WorldState["TheaterState"]):
context: PersistentContext
barcaps_needed: dict[ControlPoint, int]
active_front_lines: list[FrontLine]
air_assault_targets: list[ControlPoint]
front_line_stances: dict[FrontLine, Optional[CombatStance]]
vulnerable_front_lines: list[FrontLine]
aewc_targets: list[MissionTarget]
@@ -110,7 +109,6 @@ class TheaterState(WorldState["TheaterState"]):
context=self.context,
barcaps_needed=dict(self.barcaps_needed),
active_front_lines=list(self.active_front_lines),
air_assault_targets=list(self.air_assault_targets),
front_line_stances=dict(self.front_line_stances),
vulnerable_front_lines=list(self.vulnerable_front_lines),
aewc_targets=list(self.aewc_targets),
@@ -161,7 +159,6 @@ class TheaterState(WorldState["TheaterState"]):
cp: barcap_rounds for cp in finder.vulnerable_control_points()
},
active_front_lines=list(finder.front_lines()),
air_assault_targets=list(finder.air_assault_targets()),
front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()],

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from datetime import timedelta
from game.data.units import UnitClass
from game.utils import Distance, feet, nautical_miles
from game.utils import Distance, feet, nautical_miles, Speed, knots
@dataclass
@@ -77,6 +77,16 @@ class Doctrine:
ground_unit_procurement_ratios: GroundUnitProcurementRatios
rtb_speed: Speed
sead_escort_spacing: Distance
escort_spacing: Distance
sead_escort_engagement_range: Distance
escort_engagement_range: Distance
MODERN_DOCTRINE = Doctrine(
cap=True,
@@ -113,6 +123,11 @@ MODERN_DOCTRINE = Doctrine(
UnitClass.RECON: 1,
}
),
rtb_speed=knots(500),
sead_escort_spacing=feet(1000),
escort_spacing=feet(2000),
sead_escort_engagement_range=nautical_miles(40),
escort_engagement_range=nautical_miles(30),
)
COLDWAR_DOCTRINE = Doctrine(
@@ -150,6 +165,11 @@ COLDWAR_DOCTRINE = Doctrine(
UnitClass.RECON: 1,
}
),
rtb_speed=knots(450),
sead_escort_spacing=feet(500),
escort_spacing=feet(1000),
sead_escort_engagement_range=nautical_miles(25),
escort_engagement_range=nautical_miles(20),
)
WWII_DOCTRINE = Doctrine(
@@ -186,4 +206,9 @@ WWII_DOCTRINE = Doctrine(
UnitClass.RECON: 1,
}
),
rtb_speed=knots(300),
sead_escort_spacing=feet(100),
escort_spacing=feet(200),
sead_escort_engagement_range=nautical_miles(10),
escort_engagement_range=nautical_miles(5),
)

View File

@@ -338,9 +338,7 @@ class Debriefing:
seen = set()
captures = []
for capture in reversed(self.state_data.base_capture_events):
# The ID string in the JSON file will be an airport ID for airport captures
# but will be a UUID for all other types, since DCS doesn't know the UUIDs
# for the captured FOBs.
# The ID string in the JSON file will be the UUID generated from liberation
cp_id, new_owner_id_str, _name = capture.split("||")
# Only the most recent capture event matters.
@@ -349,13 +347,8 @@ class Debriefing:
seen.add(cp_id)
try:
control_point = self.game.theater.find_control_point_by_airport_id(
int(cp_id)
)
except ValueError:
# The CP ID could not be converted to an int, so it's a UUID.
control_point = self.game.theater.find_control_point_by_id(UUID(cp_id))
except KeyError:
except (KeyError, ValueError):
# Captured base is not a part of the campaign. This happens when neutral
# bases are near the conflict. Nothing to do.
continue

View File

@@ -32,6 +32,7 @@ from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.dcs.unittype import UnitType
from pydcs_extensions.f16i_idf.f16i_idf import inject_F16I
if TYPE_CHECKING:
from game.theater.start_generator import ModSettings
@@ -115,6 +116,9 @@ class Faction:
# List of default livery overrides
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
# List of default livery overrides for ground vehicles
liveries_overrides_ground_forces: Dict[str, 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
#: of the time period or operator. For example, the CJTF "countries" don't
@@ -124,6 +128,10 @@ class Faction:
#: both will use it.
unrestricted_satnav: bool = False
# Store mod settings so mod properties can be injected again on game load,
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
mod_settings: Optional[ModSettings] = field(default=None)
def has_access_to_dcs_type(self, unit_type: Type[DcsUnitType]) -> bool:
# Vehicle and Ship Units
if any(unit_type == u.dcs_unit_type for u in self.accessible_units):
@@ -279,6 +287,16 @@ class Faction:
aircraft = AircraftType.named(name)
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
# Load liveries override for ground forces
faction.liveries_overrides_ground_forces = {}
liveries_overrides_ground_forces = json.get(
"liveries_overrides_ground_forces", {}
)
for vehicle_type, livery in liveries_overrides_ground_forces.items():
faction.liveries_overrides_ground_forces[vehicle_type] = [
s.lower() for s in livery
]
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
return faction
@@ -294,7 +312,21 @@ class Faction:
if unit.unit_class is unit_class:
yield unit
def apply_mod_settings(self, mod_settings: ModSettings) -> None:
def apply_mod_settings(self, mod_settings: Optional[ModSettings] = None) -> None:
if mod_settings is None:
if self.mod_settings is None:
# No mod settings were provided and none were saved for this faction
# so stop here
return
elif self.mod_settings is not None:
# Saved mod settings were found for this faction,
# so load them for use
mod_settings = self.mod_settings
else:
# Update the mod settings of this faction
# so the settings can be applied again on load, if needed
self.mod_settings = mod_settings
# aircraft
if not mod_settings.a4_skyhawk:
self.remove_aircraft("A-4E-C")
@@ -307,6 +339,16 @@ class Faction:
self.remove_aircraft("VSN_F4B")
if not mod_settings.f15d_baz:
self.remove_aircraft("F-15D")
if not mod_settings.f_16_idf:
self.remove_aircraft("F-16I")
self.remove_aircraft("F_16D_52")
self.remove_aircraft("F_16D_50")
self.remove_aircraft("F_16D_50_NS")
self.remove_aircraft("F_16D_52_NS")
else:
inject_F16I()
# Remove the stock Viper because that DCS ID is now used by the Sufa
self.remove_aircraft_by_name("F-16CM Fighting Falcon (Block 50)")
if not mod_settings.f22_raptor:
self.remove_aircraft("F-22A")
if not mod_settings.f84g_thunderjet:
@@ -314,6 +356,7 @@ class Faction:
if not mod_settings.f100_supersabre:
self.remove_aircraft("VSN_F100")
if not mod_settings.f104_starfighter:
self.remove_aircraft("VSN_F104C")
self.remove_aircraft("VSN_F104G")
self.remove_aircraft("VSN_F104S")
self.remove_aircraft("VSN_F104S_AG")
@@ -332,6 +375,8 @@ class Faction:
self.remove_aircraft("Su-30SM")
if not mod_settings.su57_felon:
self.remove_aircraft("Su-57")
if not mod_settings.ov10a_bronco:
self.remove_aircraft("Bronco-OV-10A")
# frenchpack
if not mod_settings.frenchpack:
self.remove_vehicle("AMX10RCR")
@@ -391,6 +436,11 @@ class Faction:
if i.dcs_unit_type.id == name:
self.aircrafts.remove(i)
def remove_aircraft_by_name(self, name: str) -> None:
for i in self.aircrafts:
if i.name == name:
self.aircrafts.remove(i)
def remove_preset(self, name: str) -> None:
for pg in self.preset_groups:
if pg.name == name:

View File

@@ -21,7 +21,8 @@ class RefuelZoneGeometry:
def find_best_refuel_point(self) -> Point:
# Do simple at first.
# TODO: Consider threats.
# TODO: Avoid threatened areas, minimize backtracking.
# https://github.com/dcs-liberation/dcs_liberation/issues/2542
distance = 0.75 * self.package_home.distance_to_point(self.join)
heading = self.package_home.heading_between_point(self.join)
return self.package_home.point_from_heading(heading, distance)

View File

@@ -232,6 +232,10 @@ class Game:
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_unculled_zones(GameUpdateEvents())
# Apply mod settings again so mod properties get injected again,
# in case mods like CJS F/A-18E/F/G or IDF F-16I are selected by the player
self.blue.faction.apply_mod_settings()
self.red.faction.apply_mod_settings()
if not game_still_initializing:
# We don't need to push events that happen during load. The UI will fully
# reset when we're done.
@@ -524,12 +528,20 @@ class Game:
packages = itertools.chain(self.blue.ato.packages, self.red.ato.packages)
for package in packages:
if package.primary_task is FlightType.BARCAP:
if package.primary_task in [
FlightType.BARCAP,
FlightType.TRANSPORT,
FlightType.AEWC,
FlightType.REFUELING,
]:
# BARCAPs will be planned at most locations on smaller theaters,
# rendering culling fairly useless. BARCAP packages don't really
# need the ground detail since they're defensive. SAMs nearby
# are only interesting if there are enemies in the area, and if
# there are they won't be culled because of the enemy's mission.
# Don't create culling exclusion zones around FlightType.TRANSPORT,
# FlightType.AEWC & FlightType.REFUELING mission targets.
continue
zones.append(package.target.position)

View File

@@ -31,7 +31,7 @@ from game.profiling import logged_duration
from game.version import VERSION
LAYOUT_DIR = "resources/layouts/"
LAYOUT_DUMP = "Liberation/layouts.p"
LAYOUT_DUMP = "Retribution/layouts.p"
LAYOUT_TYPES = {
GroupRole.AIR_DEFENSE: AntiAirLayout,

44
game/migrator.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from game.ato.packagewaypoints import PackageWaypoints
from game.data.doctrine import MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
from game.utils import knots, feet, nautical_miles
if TYPE_CHECKING:
from game import Game
class Migrator:
def __init__(self, game: Game):
self.game = game
self._migrate_game()
def _migrate_game(self) -> None:
self._update_doctrine()
self._update_packagewaypoints()
def _update_doctrine(self) -> None:
doctrines = [
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
]
for c in self.game.coalitions:
if c.faction.doctrine.__dict__ in [d.__dict__ for d in doctrines]:
continue
found = False
for d in doctrines:
if c.faction.doctrine.rendezvous_altitude == d.rendezvous_altitude:
c.faction.doctrine = d
found = True
break
if not found:
c.faction.doctrine = MODERN_DOCTRINE
def _update_packagewaypoints(self) -> None:
for c in self.game.coalitions:
for p in c.ato.packages:
if not hasattr(p.waypoints, "initial"):
p.waypoints = PackageWaypoints.create(p, c)

View File

@@ -8,6 +8,7 @@ from dcs.task import (
CAP,
CAS,
EPLRS,
Escort,
FighterSweep,
GroundAttack,
Nothing,
@@ -19,6 +20,9 @@ from dcs.task import (
Refueling,
RunwayAttack,
Transport,
SEAD,
SwitchWaypoint,
OptJettisonEmptyTanks,
)
from dcs.unitgroup import FlyingGroup
@@ -83,6 +87,7 @@ class AircraftBehavior:
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
restrict_jettison: Optional[bool] = None,
mission_uses_gun: bool = True,
rtb_on_bingo: bool = True,
) -> None:
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(react_on_threat))
@@ -104,7 +109,8 @@ class AircraftBehavior:
for unit in group.units:
unit.gun = 0
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
group.points[0].tasks.append(OptRTBOnBingoFuel(rtb_on_bingo))
group.points[0].tasks.append(OptJettisonEmptyTanks())
# Do not restrict afterburner.
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
@@ -265,7 +271,9 @@ class AircraftBehavior:
# Escort groups are actually given the CAP task so they can perform the
# Search Then Engage task, which we have to use instead of the Escort
# task for the reasons explained in JoinPointBuilder.
group.task = CAP.name
group.task = Escort.name
if flight.package.primary_task == FlightType.STRIKE:
group.add_trigger_action(SwitchWaypoint(None, 5))
self.configure_behavior(
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True
)
@@ -274,7 +282,9 @@ class AircraftBehavior:
# CAS is able to perform all the same tasks as SEAD using a superset of the
# available aircraft, and F-14s are not able to be SEAD despite having TALDs.
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
group.task = CAS.name
group.task = SEAD.name
if flight.package.primary_task == FlightType.STRIKE:
group.add_trigger_action(SwitchWaypoint(None, 5))
self.configure_behavior(
flight,
group,
@@ -304,6 +314,7 @@ class AircraftBehavior:
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
rtb_on_bingo=False,
)
def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None:

View File

@@ -5,9 +5,12 @@ from datetime import datetime
from functools import cached_property
from typing import Any, Dict, List, TYPE_CHECKING
from dcs.action import AITaskPush
from dcs.condition import FlagIsTrue, GroupDead, Or, FlagIsFalse
from dcs.country import Country
from dcs.mission import Mission
from dcs.terrain.terrain import NoParkingSlotError
from dcs.triggers import TriggerOnce, Event
from dcs.unitgroup import FlyingGroup, StaticGroup
from game.ato.airtaaskingorder import AirTaskingOrder
@@ -111,6 +114,23 @@ class AircraftGenerator:
flight, country, dynamic_runways
)
self.unit_map.add_aircraft(group, flight)
if (
package.primary_task == FlightType.STRIKE
and package.primary_flight is not None
):
splittrigger = TriggerOnce(Event.NoEvent, f"Split-{id(package)}")
splittrigger.add_condition(FlagIsTrue(flag=f"split-{id(package)}"))
splittrigger.add_condition(Or())
splittrigger.add_condition(FlagIsFalse(flag=f"split-{id(package)}"))
splittrigger.add_condition(GroupDead(package.primary_flight.group_id))
for flight in package.flights:
if flight.flight_type in [
FlightType.ESCORT,
FlightType.SEAD_ESCORT,
]:
splittrigger.add_action(AITaskPush(flight.group_id, 1))
if len(splittrigger.actions) > 0:
self.mission.triggerrules.triggers.append(splittrigger)
def spawn_unused_aircraft(
self, player_country: Country, enemy_country: Country
@@ -135,6 +155,9 @@ class AircraftGenerator:
def _spawn_unused_for(
self, squadron: Squadron, country: Country, faction: Faction
) -> None:
if self.game.settings.perf_disable_idle_aircraft:
return
assert isinstance(squadron.location, Airfield)
for _ in range(squadron.untasked_aircraft):
# Creating a flight even those this isn't a fragged mission lets us
@@ -152,7 +175,7 @@ class AircraftGenerator:
flight.state = Completed(flight, self.game.settings)
group = FlightGroupSpawner(
flight, country, self.mission, self.helipads
flight, country, self.mission, self.helipads, self.mission_data
).create_idle_aircraft()
AircraftPainter(flight, group).apply_livery()
self.unit_map.add_aircraft(group, flight)
@@ -162,7 +185,7 @@ class AircraftGenerator:
) -> FlyingGroup[Any]:
"""Creates and configures the flight group in the mission."""
group = FlightGroupSpawner(
flight, country, self.mission, self.helipads
flight, country, self.mission, self.helipads, self.mission_data
).create_flight_group()
self.flights.append(
FlightGroupConfigurator(

View File

@@ -6,7 +6,7 @@ from dcs import Mission
from dcs.country import Country
from dcs.mapping import Vector2
from dcs.mission import StartType as DcsStartType
from dcs.planes import Su_33
from dcs.planes import F_14A, Su_33
from dcs.ships import KUZNECOW
from dcs.terrain import Airport, NoParkingSlotError
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
@@ -15,6 +15,7 @@ from game.ato import Flight
from game.ato.flightstate import InFlight
from game.ato.starttype import StartType
from game.ato.traveltime import GroundSpeed
from game.missiongenerator.missiondata import MissionData
from game.naming import namegen
from game.theater import Airfield, ControlPoint, Fob, NavalControlPoint, OffMapSpawn
from game.utils import feet, meters
@@ -32,6 +33,8 @@ WARM_START_ALTITUDE = meters(3000)
MINIMUM_MID_MISSION_SPAWN_ALTITUDE_MSL = feet(6000)
MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL = feet(500)
STACK_SEPARATION = feet(200)
RTB_ALTITUDE = meters(800)
RTB_DISTANCE = 5000
HELI_ALT = 500
@@ -44,11 +47,13 @@ class FlightGroupSpawner:
country: Country,
mission: Mission,
helipads: dict[ControlPoint, StaticGroup],
mission_data: MissionData,
) -> None:
self.flight = flight
self.country = country
self.mission = mission
self.helipads = helipads
self.mission_data = mission_data
def create_flight_group(self) -> FlyingGroup[Any]:
"""Creates the group for the flight and adds it to the mission.
@@ -74,8 +79,12 @@ class FlightGroupSpawner:
self.flight.state.is_waiting_for_start
or self.flight.state.spawn_type is not StartType.IN_FLIGHT
):
return self.generate_flight_at_departure()
return self.generate_mid_mission()
grp = self.generate_flight_at_departure()
self.flight.group_id = grp.id
return grp
grp = self.generate_mid_mission()
self.flight.group_id = grp.id
return grp
def create_idle_aircraft(self) -> FlyingGroup[Any]:
airport = self.flight.squadron.location.dcs_airport
@@ -136,6 +145,10 @@ class FlightGroupSpawner:
pos = self.flight.state.estimate_position()
pos += Vector2(random.randint(100, 1000), random.randint(100, 1000))
alt, alt_type = self.flight.state.estimate_altitude()
cp = self.flight.squadron.location.id
if cp not in self.mission_data.cp_stack:
self.mission_data.cp_stack[cp] = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
# We don't know where the ground is, so just make sure that any aircraft
# spawning at an MSL altitude is spawned at some minimum altitude.
@@ -145,8 +158,9 @@ class FlightGroupSpawner:
# Set a minimum AGL value for 'alt' if needed,
# otherwise planes might crash in trees and stuff.
if alt_type == "RADIO" and alt < MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL:
alt = MINIMUM_MID_MISSION_SPAWN_ALTITUDE_AGL
if alt_type == "RADIO" and alt < self.mission_data.cp_stack[cp]:
alt = self.mission_data.cp_stack[cp]
self.mission_data.cp_stack[cp] += STACK_SEPARATION
group = self.mission.flight_group(
country=self.country,
@@ -259,7 +273,9 @@ class FlightGroupSpawner:
# Setting Su-33s starting from the non-supercarrier Kuznetsov to take off from
# runway to work around a DCS AI issue preventing Su-33s from taking off when
# set to "Takeoff from ramp" (#1352)
if (
# Also setting the F-14A AI variant to start from cats since they are reported
# to have severe pathfinding problems when doing ramp starts (#1927)
if self.flight.unit_type.dcs_unit_type == F_14A or (
self.flight.unit_type.dcs_unit_type == Su_33
and group_units[0] is not None
and group_units[0].type == KUZNECOW.id

View File

@@ -0,0 +1,25 @@
from dcs import Mission
from dcs.action import SetFlag
from dcs.condition import TimeAfter
from dcs.task import ControlledTask
from dcs.triggers import TriggerOnce, Event
from game.ato import Package
def create_stop_orbit_trigger(
orbit: ControlledTask, package: Package, mission: Mission, elapsed: int
) -> None:
orbit.stop_if_user_flag(id(package), True)
orbits = [
x
for x in mission.triggerrules.triggers
if x.comment == f"StopOrbit{id(package)}"
]
if not any(orbits):
stop_trigger = TriggerOnce(Event.NoEvent, f"StopOrbit{id(package)}")
stop_condition = TimeAfter(elapsed)
stop_action = SetFlag(id(package))
stop_trigger.add_condition(stop_condition)
stop_trigger.add_action(stop_action)
mission.triggerrules.triggers.append(stop_trigger)

View File

@@ -1,24 +1,14 @@
from dcs.point import MovingPoint
from dcs.point import PointAction
from dcs.task import Land
from dcs.point import MovingPoint, PointAction
from game.utils import feet
from .pydcswaypointbuilder import PydcsWaypointBuilder
class CargoStopBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
# Create a landing task, currently only for Helos!
if self.flight.is_helo:
# Calculate a landing point with a small buffer to prevent AI from landing
# directly at the static ammo depot and exploding
landing_point = waypoint.position.random_point_within(15, 5)
# Use Land Task with 30s duration for helos
waypoint.add_task(Land(landing_point, duration=30))
else:
# Fixed wing will drop the cargo at the waypoint so we set a lower altitude
waypoint.alt = int(feet(10000).meters)
waypoint.alt_type = "BARO"
waypoint.action = PointAction.FlyOverPoint
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint

View File

@@ -4,6 +4,7 @@ from dcs.point import MovingPoint
from dcs.task import ControlledTask, OptFormation, OrbitAction
from game.ato.flightplans.loiter import LoiterFlightPlan
from ._helper import create_stop_orbit_trigger
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -22,8 +23,10 @@ class HoldPointBuilder(PydcsWaypointBuilder):
return
push_time = self.flight.flight_plan.push_time
self.waypoint.departure_time = push_time
loiter.stop_after_time(
int((push_time - self.elapsed_mission_time).total_seconds())
)
elapsed = int((push_time - self.elapsed_mission_time).total_seconds()) - 60
loiter.stop_after_time(elapsed)
# What follows is some code to cope with the broken 'stop after time' condition
create_stop_orbit_trigger(loiter, self.package, self.mission, elapsed)
# end of hotfix
waypoint.add_task(loiter)
waypoint.add_task(OptFormation.finger_four_close())

View File

@@ -1,60 +1,60 @@
from typing import List, Type
import random
from typing import List
from dcs.point import MovingPoint
from dcs.task import (
ControlledTask,
EngageTargets,
EscortTaskAction,
OptECMUsing,
OptFormation,
TargetType,
Targets,
)
from game.ato import FlightType
from game.theater import NavalControlPoint
from game.utils import nautical_miles
from game.utils import nautical_miles, feet
from .pydcswaypointbuilder import PydcsWaypointBuilder
class JoinPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
waypoint.tasks.append(OptFormation.finger_four_open())
doctrine = self.flight.coalition.doctrine
if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks(
waypoint,
[
Targets.All.Air.Planes.Fighters,
Targets.All.Air.Planes.MultiroleFighters,
Targets.All.Air.Planes.Fighters.id,
Targets.All.Air.Planes.MultiroleFighters.id,
],
max_dist=doctrine.escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.escort_spacing.feet,
)
if self.flight.count < 4:
waypoint.tasks.append(OptFormation.line_abreast_open())
else:
waypoint.tasks.append(OptFormation.spread_four_open())
elif self.flight.flight_type == FlightType.SEAD_ESCORT:
if isinstance(self.flight.package.target, NavalControlPoint):
self.configure_escort_tasks(
waypoint,
[
Targets.All.Naval,
Targets.All.GroundUnits.AirDefence.AAA.SAMRelated,
Targets.All.Naval.id,
Targets.All.GroundUnits.AirDefence.AAA.SAMRelated.id,
],
max_dist=doctrine.sead_escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.sead_escort_spacing.feet,
)
else:
self.configure_escort_tasks(
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
waypoint,
[Targets.All.GroundUnits.AirDefence.AAA.SAMRelated.id],
max_dist=doctrine.sead_escort_engagement_range.nautical_miles,
vertical_spacing=doctrine.sead_escort_spacing.feet,
)
# Let the AI use ECM to preemptively defend themselves.
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
waypoint.tasks.append(ecm_option)
if self.flight.count < 4:
waypoint.tasks.append(OptFormation.line_abreast_open())
else:
waypoint.tasks.append(OptFormation.spread_four_open())
elif not self.flight.flight_type.is_air_to_air:
# Capture any non A/A type to avoid issues with SPJs that use the primary radar such as the F/A-18C.
# You can bully them with STT to not be able to fire radar guided missiles at you,
@@ -64,48 +64,32 @@ class JoinPointBuilder(PydcsWaypointBuilder):
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar)
waypoint.tasks.append(ecm_option)
waypoint.tasks.append(OptFormation.finger_four_open())
@staticmethod
def configure_escort_tasks(
waypoint: MovingPoint, target_types: List[Type[TargetType]]
self,
waypoint: MovingPoint,
target_types: List[str],
max_dist: float = 30.0,
vertical_spacing: float = 2000.0,
) -> None:
# Ideally we would use the escort mission type and escort task to have
# the AI automatically but the AI only escorts AI flights while they are
# traveling between waypoints. When an AI flight performs an attack
# (such as attacking the mission target), AI escorts wander aimlessly
# until the escorted group resumes its flight plan.
#
# As such, we instead use the Search Then Engage task, which is an
# enroute task that causes the AI to follow their flight plan and engage
# enemies of the set type within a certain distance. The downside to
# this approach is that AI escorts are no longer related to the group
# they are escorting, aside from the fact that they fly a similar flight
# plan at the same time. With Escort, the escorts will follow the
# escorted group out of the area. The strike element may or may not fly
# directly over the target, and they may or may not require multiple
# attack runs. For the escort flight we must just assume a flight plan
# for the escort to fly. If the strike flight doesn't need to overfly
# the target, the escorts are needlessly going in harms way. If the
# strike flight needs multiple passes, the escorts may leave before the
# escorted aircraft do.
#
# Another possible option would be to use Search Then Engage for join ->
# ingress and egress -> split, but use a Search Then Engage in Zone task
# for the target area that is set to end on a flag flip that occurs when
# the strike aircraft finish their attack task.
#
# https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior
waypoint.add_task(
ControlledTask(
EngageTargets(
# TODO: From doctrine.
max_distance=int(nautical_miles(30).meters),
targets=target_types,
)
rx = (random.random() + 0.1) * 333
ry = feet(vertical_spacing).meters
rz = (random.random() + 0.1) * 50 * random.choice([-1, 1])
pos = {"x": rx, "y": ry, "z": rz}
group_id = None
if self.package.primary_flight is not None:
group_id = self.package.primary_flight.group_id
escort = ControlledTask(
EscortTaskAction(
group_id=group_id,
engagement_max_dist=int(nautical_miles(max_dist).meters),
targets=target_types,
position=pos,
)
)
# We could set this task to end at the split point. pydcs doesn't
# currently support that task end condition though, and we don't really
# need it.
escort.stop_if_user_flag(f"split-{id(self.package)}", True)
waypoint.tasks.append(escort)

View File

@@ -0,0 +1,17 @@
from dcs.point import MovingPoint
from dcs.task import Land
from .pydcswaypointbuilder import PydcsWaypointBuilder
class LandingZoneBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
# Create a landing task, currently only for Helos!
# Calculate a landing point with a small buffer to prevent AI from landing
# directly at the static ammo depot and exploding
landing_point = waypoint.position.random_point_within(15, 5)
# Use Land Task with 30s duration for helos
waypoint.add_task(Land(landing_point, duration=30))
return waypoint

View File

@@ -12,6 +12,7 @@ from dcs.task import (
from game.ato import FlightType
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from ._helper import create_stop_orbit_trigger
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -57,7 +58,11 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
racetrack = ControlledTask(orbit)
self.set_waypoint_tot(waypoint, flight_plan.patrol_start_time)
loiter_duration = flight_plan.patrol_end_time - self.elapsed_mission_time
racetrack.stop_after_time(int(loiter_duration.total_seconds()))
elapsed = int(loiter_duration.total_seconds())
racetrack.stop_after_time(elapsed)
# What follows is some code to cope with the broken 'stop after time' condition
create_stop_orbit_trigger(racetrack, self.package, self.mission, elapsed)
# end of hotfix
waypoint.add_task(racetrack)
def configure_refueling_actions(self, waypoint: MovingPoint) -> None:

View File

@@ -1,10 +1,12 @@
from dcs.point import MovingPoint
from dcs.task import RefuelingTaskAction
from dcs.task import RefuelingTaskAction, ControlledTask
from .pydcswaypointbuilder import PydcsWaypointBuilder
class RefuelPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
waypoint.add_task(RefuelingTaskAction())
refuel = ControlledTask(RefuelingTaskAction())
refuel.start_probability(10)
waypoint.add_task(refuel)
return super().add_tasks(waypoint)

View File

@@ -1,5 +1,5 @@
from dcs.point import MovingPoint
from dcs.task import OptECMUsing, OptFormation
from dcs.task import OptECMUsing, OptFormation, RunScript
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -16,4 +16,12 @@ class SplitPointBuilder(PydcsWaypointBuilder):
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfOnlyLockByRadar)
waypoint.tasks.append(ecm_option)
waypoint.tasks.append(OptFormation.finger_four_close())
waypoint.tasks.append(OptFormation.finger_four_close())
waypoint.speed_locked = True
waypoint.speed = self.flight.coalition.doctrine.rtb_speed.meters_per_second
waypoint.ETA_locked = False
if self.flight is self.package.primary_flight:
script = RunScript(
f'trigger.action.setUserFlag("split-{id(self.package)}", true)'
)
waypoint.tasks.append(script)

View File

@@ -1,14 +0,0 @@
from dcs.point import MovingPoint, PointAction
from .pydcswaypointbuilder import PydcsWaypointBuilder
class StopoverBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
if (control_point := self.waypoint.control_point) is not None:
waypoint.airdrome_id = control_point.airdrome_id_for_landing
return waypoint

View File

@@ -1,15 +1,22 @@
import copy
from dcs.planes import B_17G, B_52H, Tu_22M3
from dcs import Point
from dcs.planes import B_17G, B_52H, Tu_22M3, B_1B
from dcs.point import MovingPoint
from dcs.task import Bombing, OptFormation, WeaponType, Expend
from dcs.task import Bombing, Expend, OptFormation, WeaponType
from game.utils import mach, Distance
from .pydcswaypointbuilder import PydcsWaypointBuilder
class StrikeIngressBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
bomber = self.group.units[0].unit_type in [B_17G, Tu_22M3]
bomber_guided = self.group.units[0].unit_type in [B_1B, B_52H]
if bomber_guided:
self.add_strike_tasks(waypoint, WeaponType.Guided)
self.add_bombing_tasks(waypoint)
elif bomber:
self.add_bombing_tasks(waypoint)
else:
self.add_strike_tasks(waypoint)
@@ -21,24 +28,28 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
if not targets:
return
center = copy.copy(targets[0].position)
center: Point = copy.copy(targets[0].position)
for target in targets[1:]:
center += target.position
center /= len(targets)
ga = True if self.flight.count > 1 else False
bombing = Bombing(
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=True
center, weapon_type=WeaponType.Bombs, expend=Expend.All, group_attack=ga
)
waypoint.tasks.append(bombing)
def add_strike_tasks(self, waypoint: MovingPoint) -> None:
def add_strike_tasks(
self, waypoint: MovingPoint, weapon_type: WeaponType = WeaponType.Auto
) -> None:
ga = True if self.flight.count > 1 else False
for target in self.waypoint.targets:
bombing = Bombing(
target.position, weapon_type=WeaponType.Auto, group_attack=True
)
bombing = Bombing(target.position, weapon_type=weapon_type, group_attack=ga)
# If there is only one target, drop all ordnance in one pass.
if len(self.waypoint.targets) == 1:
bombing.params["expend"] = Expend.All.value
waypoint.tasks.append(bombing)
waypoint.speed = mach(0.85, Distance.from_feet(20000)).meters_per_second
# Register special waypoints
self.register_special_waypoints(self.waypoint.targets)

View File

@@ -16,12 +16,12 @@ from game.ato import Flight, FlightWaypoint
from game.ato.flightstate import InFlight, WaitingForStart
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.starttype import StartType
from game.missiongenerator.aircraft.waypoints.stopover import StopoverBuilder
from game.missiongenerator.aircraft.waypoints.cargostop import CargoStopBuilder
from game.missiongenerator.missiondata import MissionData
from game.settings import Settings
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
from .cargostop import CargoStopBuilder
from .landingzone import LandingZoneBuilder
from .casingress import CasIngressBuilder
from .deadingress import DeadIngressBuilder
from .default import DefaultWaypointBuilder
@@ -118,7 +118,6 @@ class WaypointGenerator:
def builder_for_waypoint(self, waypoint: FlightWaypoint) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.DROP_OFF: CargoStopBuilder,
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
@@ -133,9 +132,10 @@ class WaypointGenerator:
FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL: RaceTrackEndBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.PICKUP: CargoStopBuilder,
FlightWaypointType.PICKUP_ZONE: LandingZoneBuilder,
FlightWaypointType.DROPOFF_ZONE: LandingZoneBuilder,
FlightWaypointType.REFUEL: RefuelPointBuilder,
FlightWaypointType.STOPOVER: StopoverBuilder,
FlightWaypointType.CARGO_STOP: CargoStopBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(

View File

@@ -58,6 +58,7 @@ class MissionInfoGenerator:
self.awacs: List[AwacsInfo] = []
self.comms: List[CommInfo] = []
self.flights: List[FlightData] = []
self.player_flights: List[FlightData] = []
self.jtacs: List[JtacInfo] = []
self.tankers: List[TankerInfo] = []
self.frontlines: List[FrontLineInfo] = []
@@ -87,6 +88,8 @@ class MissionInfoGenerator:
flight: Flight information.
"""
self.flights.append(flight)
if len(flight.client_units) > 0:
self.player_flights.append(flight)
def add_jtac(self, jtac: JtacInfo) -> None:
"""Adds a JTAC to the mission.

View File

@@ -10,6 +10,7 @@ from dcs.unit import Vehicle
from dcs.unitgroup import VehicleGroup
from game.dcs.groundunittype import GroundUnitType
from game.missiongenerator.groundforcepainter import GroundForcePainter
from game.transfers import Convoy
from game.unitmap import UnitMap
from game.utils import kph
@@ -72,6 +73,7 @@ class ConvoyGenerator:
for_player: bool,
) -> VehicleGroup:
country = self.mission.country(self.game.coalition_for(for_player).country_name)
faction = self.game.faction_for(for_player)
unit_types = list(units.items())
main_unit_type, main_unit_count = unit_types[0]
@@ -97,6 +99,7 @@ class ConvoyGenerator:
v.position.x = position.x
v.position.y = next(y)
v.heading = 0
GroundForcePainter(faction, v).apply_livery()
group.add_unit(v)
return group

View File

@@ -17,6 +17,7 @@ class EnvironmentGenerator:
def set_atmospheric(self, atmospheric: AtmosphericConditions) -> None:
self.mission.weather.qnh = atmospheric.qnh.mm_hg
self.mission.weather.season_temperature = atmospheric.temperature_celsius
self.mission.weather.turbulence_at_ground = int(atmospheric.turbulence_per_10cm)
def set_clouds(self, clouds: Optional[Clouds]) -> None:
if clouds is None:

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import logging
import math
import random
from typing import List, Optional, TYPE_CHECKING, Tuple
@@ -43,6 +44,7 @@ from game.theater.controlpoint import ControlPoint
from game.unitmap import UnitMap
from game.utils import Heading
from .frontlineconflictdescription import FrontLineConflictDescription
from .groundforcepainter import GroundForcePainter
from .lasercoderegistry import LaserCodeRegistry
from .missiondata import JtacInfo, MissionData
@@ -221,7 +223,7 @@ class FlotGenerator:
u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads]
)[0]
self.mission.vehicle_group(
vg = self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, u),
u.dcs_unit_type,
@@ -230,6 +232,8 @@ class FlotGenerator:
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
vehicle = vg.units[0]
GroundForcePainter(faction, vehicle).apply_livery()
return
possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY))
@@ -246,7 +250,7 @@ class FlotGenerator:
weights=[u.spawn_weight for u in infantry_choices],
k=INFANTRY_GROUP_SIZE,
)
self.mission.vehicle_group(
vg = self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, units[0]),
units[0].dcs_unit_type,
@@ -255,10 +259,12 @@ class FlotGenerator:
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
vehicle = vg.units[0]
GroundForcePainter(faction, vehicle).apply_livery()
for unit in units[1:]:
position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group(
vg = self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, unit),
unit.dcs_unit_type,
@@ -267,6 +273,8 @@ class FlotGenerator:
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
vehicle = vg.units[0]
GroundForcePainter(faction, vehicle).apply_livery()
def _set_reform_waypoint(
self, dcs_group: VehicleGroup, forward_heading: Heading
@@ -769,6 +777,8 @@ class FlotGenerator:
heading: Heading,
) -> VehicleGroup:
cp = self.conflict.front_line.control_point_friendly_to(player)
faction = self.game.faction_for(player)
group = self.mission.vehicle_group(
side,
namegen.next_unit_name(side, unit_type),
@@ -783,5 +793,6 @@ class FlotGenerator:
for c in range(count):
vehicle: Vehicle = group.units[c]
vehicle.player_can_drive = True
GroundForcePainter(faction, vehicle).apply_livery()
return group

View File

@@ -0,0 +1,82 @@
from __future__ import annotations
import logging
import random
from typing import Any, Optional
from dcs.unit import Ship
from dcs.unitgroup import Vehicle
from game.factions.faction import Faction
class GroundForcePainter:
def __init__(self, faction: Faction, vehicle: Vehicle) -> None:
self.faction = faction
self.vehicle = vehicle
def livery_from_faction(self) -> Optional[str]:
faction = self.faction
try:
if (
choices := faction.liveries_overrides_ground_forces.get(
self.vehicle.type
)
) is not None:
return random.choice(choices)
except AttributeError:
logging.warning(
f"Faction {self.faction.name} is missing livery for ground unit {self.vehicle.type}"
)
return None
logging.warning(
f"Faction {self.faction.name} is missing livery for ground unit {self.vehicle.type}"
)
return None
def determine_livery(self) -> Optional[str]:
if (livery := self.livery_from_faction()) is not None:
return livery
return None
def apply_livery(self) -> None:
livery = self.determine_livery()
if livery is None:
return
self.vehicle.livery_id = livery
class NavalForcePainter:
def __init__(self, faction: Faction, vessel: Ship) -> None:
self.faction = faction
self.vessel = vessel
def livery_from_faction(self) -> Optional[str]:
faction = self.faction
try:
if (
choices := faction.liveries_overrides_ground_forces.get(
self.vessel.type
)
) is not None:
return random.choice(choices)
except AttributeError:
logging.warning(
f"Faction {self.faction.name} is missing livery for naval unit {self.vessel.type}"
)
return None
logging.warning(
f"Faction {self.faction.name} is missing livery for naval unit {self.vessel.type}"
)
return None
def determine_livery(self) -> Optional[str]:
if (livery := self.livery_from_faction()) is not None:
return livery
return None
def apply_livery(self) -> None:
livery = self.determine_livery()
if livery is None:
return
self.vessel.livery_id = livery

View File

@@ -400,6 +400,9 @@ class BriefingPage(KneeboardPage):
f"Temperature: {round(self.weather.atmospheric.temperature_celsius)} °C at sea level"
)
writer.text(f"QNH: {qnh_in_hg} inHg / {qnh_mm_hg} mmHg / {qnh_hpa} hPa")
writer.text(
f"Turbulence: {round(self.weather.atmospheric.turbulence_per_10cm)} per 10cm at ground level."
)
fl = self.flight
@@ -636,6 +639,8 @@ class SeadTaskPage(KneeboardPage):
class StrikeTaskPage(KneeboardPage):
"""A kneeboard page containing strike target information."""
WAYPOINT_DESC_MAX_LEN = 35
def __init__(self, flight: FlightData, dark_kneeboard: bool) -> None:
self.flight = flight
self.dark_kneeboard = dark_kneeboard
@@ -655,17 +660,21 @@ class StrikeTaskPage(KneeboardPage):
writer.title(f"{self.flight.callsign} Strike Task Info{custom_name_title}")
writer.table(
[self.target_info_row(t) for t in self.targets],
headers=["Steerpoint", "Description", "Location"],
[self.target_info_row(t, writer) for t in self.targets],
headers=["STPT", "Description", "Location"],
)
writer.write(path)
@staticmethod
def target_info_row(target: NumberedWaypoint) -> list[str]:
def target_info_row(
target: NumberedWaypoint, writer: KneeboardPageWriter
) -> list[str]:
return [
str(target.number),
target.waypoint.pretty_name,
writer.wrap_line(
target.waypoint.pretty_name, StrikeTaskPage.WAYPOINT_DESC_MAX_LEN
),
target.waypoint.position.latlng().format_dms(include_decimal_seconds=True),
]

View File

@@ -46,7 +46,7 @@ class LogisticsGenerator:
target_zone = f"{self.group.name}TARGET_ZONE"
self.mission.triggers.add_triggerzone(
self.flight.flight_plan.layout.target.position,
self.flight.flight_plan.engagement_distance.meters,
self.flight.flight_plan.ctld_target_zone_radius.meters,
False,
target_zone,
)
@@ -57,8 +57,8 @@ class LogisticsGenerator:
if (
waypoint.waypoint_type
not in [
FlightWaypointType.PICKUP,
FlightWaypointType.DROP_OFF,
FlightWaypointType.PICKUP_ZONE,
FlightWaypointType.DROPOFF_ZONE,
]
or waypoint.only_for_player
and not self.flight.client_count
@@ -69,7 +69,7 @@ class LogisticsGenerator:
self.mission.triggers.add_triggerzone(
waypoint.position, ZONE_RADIUS, False, zone_name
)
if waypoint.waypoint_type == FlightWaypointType.PICKUP:
if waypoint.waypoint_type == FlightWaypointType.PICKUP_ZONE:
pickup_point = waypoint.position
logistics_info.pickup_zone = zone_name
else:

View File

@@ -40,7 +40,7 @@ class LuaGenerator:
self.inject_plugins()
def generate_plugin_data(self) -> None:
lua_data = LuaData("dcsLiberation")
lua_data = LuaData("dcsRetribution")
install_path = lua_data.add_item("installPath")
install_path.set_value(os.path.abspath("."))
@@ -201,7 +201,7 @@ class LuaGenerator:
for role, connections in node.connections.items():
iads_element.add_data_array(role, connections)
trigger = TriggerStart(comment="Set DCS Liberation data")
trigger = TriggerStart(comment="Set DCS Retribution data")
trigger.add_action(DoScript(String(lua_data.create_operations_lua())))
self.mission.triggerrules.triggers.append(trigger)
@@ -380,7 +380,7 @@ class LuaData(LuaItem):
"""crates the liberation lua script for the dcs mission"""
lua_prefix = """
-- setting configuration table
env.info("DCSLiberation|: setting configuration table")
env.info("DCSRetribution|: setting configuration table")
"""
return lua_prefix + self.serialize()

View File

@@ -11,6 +11,8 @@ from game.runways import RunwayData
if TYPE_CHECKING:
from game.radio.radios import RadioFrequency
from game.radio.tacan import TacanChannel
from game.utils import Distance
from uuid import UUID
@dataclass
@@ -94,3 +96,4 @@ class MissionData:
tankers: list[TankerInfo] = field(default_factory=list)
jtacs: list[JtacInfo] = field(default_factory=list)
logistics: list[LogisticsInfo] = field(default_factory=list)
cp_stack: dict[UUID, Distance] = field(default_factory=dict)

View File

@@ -44,7 +44,7 @@ if TYPE_CHECKING:
from game import Game
COMBINED_ARMS_SLOTS = 1
COMBINED_ARMS_SLOTS = 3
def country_id_from_name(name: str) -> int:
@@ -330,6 +330,12 @@ class MissionGenerator:
gen.generate()
def setup_combined_arms(self) -> None:
observers = 3
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
self.mission.groundControl.blue_game_masters = 1
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
self.mission.groundControl.blue_observer = 1
self.mission.groundControl.blue_observer = observers
self.mission.groundControl.red_game_masters = 1
self.mission.groundControl.red_tactical_commander = COMBINED_ARMS_SLOTS
self.mission.groundControl.red_observer = observers
self.mission.groundControl.neutrals_observer = observers

View File

@@ -40,6 +40,10 @@ from dcs.unitgroup import MovingGroup, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import ShipType, VehicleType
from dcs.vehicles import vehicle_map
from game.missiongenerator.groundforcepainter import (
NavalForcePainter,
GroundForcePainter,
)
from game.missiongenerator.missiondata import CarrierInfo, MissionData
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage
@@ -122,6 +126,7 @@ class GroundObjectGenerator:
vehicle_group: Optional[VehicleGroup] = None
for unit in units:
assert issubclass(unit.type, VehicleType)
faction = unit.ground_object.control_point.coalition.faction
if vehicle_group is None:
vehicle_group = self.m.vehicle_group(
self.country,
@@ -134,11 +139,13 @@ class GroundObjectGenerator:
self.enable_eplrs(vehicle_group, unit.type)
vehicle_group.units[0].name = unit.unit_name
self.set_alarm_state(vehicle_group)
GroundForcePainter(faction, vehicle_group.units[0]).apply_livery()
else:
vehicle_unit = self.m.vehicle(unit.unit_name, unit.type)
vehicle_unit.player_can_drive = True
vehicle_unit.position = unit.position
vehicle_unit.heading = unit.position.heading.degrees
GroundForcePainter(faction, vehicle_unit).apply_livery()
vehicle_group.add_unit(vehicle_unit)
self._register_theater_unit(unit, vehicle_group.units[-1])
if vehicle_group is None:
@@ -154,6 +161,7 @@ class GroundObjectGenerator:
ship_group: Optional[ShipGroup] = None
for unit in units:
assert issubclass(unit.type, ShipType)
faction = unit.ground_object.control_point.coalition.faction
if ship_group is None:
ship_group = self.m.ship_group(
self.country,
@@ -166,12 +174,14 @@ class GroundObjectGenerator:
ship_group.set_frequency(frequency.hertz)
ship_group.units[0].name = unit.unit_name
self.set_alarm_state(ship_group)
NavalForcePainter(faction, ship_group.units[0]).apply_livery()
else:
ship_unit = self.m.ship(unit.unit_name, unit.type)
if frequency:
ship_unit.set_frequency(frequency.hertz)
ship_unit.position = unit.position
ship_unit.heading = unit.position.heading.degrees
NavalForcePainter(faction, ship_unit).apply_livery()
ship_group.add_unit(ship_unit)
self._register_theater_unit(unit, ship_group.units[-1])
if ship_group is None:

View File

@@ -44,7 +44,7 @@ class Silence(Option):
class TriggerGenerator:
capture_zone_types = (Fob,)
capture_zone_types = (Fob, Airfield)
capture_zone_flag = 600
def __init__(self, mission: Mission, game: Game) -> None:
@@ -159,7 +159,9 @@ class TriggerGenerator:
flag = self.get_capture_zone_flag()
capture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
capture_trigger.add_condition(
AllOfCoalitionOutsideZone(defending_coalition, trigger_zone.id)
AllOfCoalitionOutsideZone(
defending_coalition, trigger_zone.id, unit_type="GROUND"
)
)
capture_trigger.add_condition(
PartOfCoalitionInZone(
@@ -176,7 +178,9 @@ class TriggerGenerator:
recapture_trigger = TriggerCondition(Event.NoEvent, "Capture Trigger")
recapture_trigger.add_condition(
AllOfCoalitionOutsideZone(attacking_coalition, trigger_zone.id)
AllOfCoalitionOutsideZone(
attacking_coalition, trigger_zone.id, unit_type="GROUND"
)
)
recapture_trigger.add_condition(
PartOfCoalitionInZone(

View File

@@ -162,7 +162,7 @@ class LuaPlugin(PluginSettings):
for option in self.options:
enabled = str(option.enabled).lower()
name = option.identifier
option_decls.append(f" dcsLiberation.plugins.{name} = {enabled}")
option_decls.append(f" dcsRetribution.plugins.{name} = {enabled}")
joined_options = "\n".join(option_decls)
@@ -170,11 +170,11 @@ class LuaPlugin(PluginSettings):
f"""\
-- {self.identifier} plugin configuration.
if dcsLiberation then
if not dcsLiberation.plugins then
dcsLiberation.plugins = {{}}
if dcsRetribution then
if not dcsRetribution.plugins then
dcsRetribution.plugins = {{}}
end
dcsLiberation.plugins.{self.identifier} = {{}}
dcsRetribution.plugins.{self.identifier} = {{}}
{joined_options}
end

View File

@@ -256,6 +256,52 @@ RADIOS: List[Radio] = [
RadioRange(MHz(225), MHz(400), kHz(25), Modulation.AM),
),
),
# MB-339A
Radio(
"AN/ARC-150(V) 2",
(
RadioRange(
MHz(225),
MHz(400),
MHz(1),
Modulation.AM,
frozenset((MHz(243),)),
),
),
),
Radio(
"SRT-651/N",
(
RadioRange(
MHz(30),
MHz(88),
MHz(1),
Modulation.FM,
frozenset((MHz(40, 500),)),
),
RadioRange(
MHz(108),
MHz(156),
MHz(1),
Modulation.AM,
frozenset((MHz(121, 500),)),
),
RadioRange(
MHz(156),
MHz(174),
MHz(1),
Modulation.FM,
frozenset((MHz(156, 800),)),
),
RadioRange(
MHz(225),
MHz(400),
MHz(1),
Modulation.AM, # Actually AM/FM, but we can't represent that.
frozenset((MHz(243),)),
),
),
),
]

View File

@@ -4,9 +4,7 @@ from fastapi import APIRouter, Depends
from shapely.geometry import LineString, Point as ShapelyPoint
from game import Game
from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightplans.cas import CasFlightPlan
from game.ato.flightplans.patrolling import PatrollingFlightPlan
from game.ato.flightplans.uizonedisplay import UiZoneDisplay
from game.server import GameContext
from game.server.flights.models import FlightJs
from game.server.leaflet import LeafletPoly, ShapelyUtil
@@ -40,23 +38,12 @@ def commit_boundary(
flight_id: UUID, game: Game = Depends(GameContext.require)
) -> LeafletPoly:
flight = game.db.flights.get(flight_id)
if isinstance(flight.flight_plan, CasFlightPlan) or isinstance(
flight.flight_plan, AirAssaultFlightPlan
):
# Special Commit boundary for CAS and AirAssault
center = flight.flight_plan.layout.target.position
commit_center = ShapelyPoint(center.x, center.y)
elif isinstance(flight.flight_plan, PatrollingFlightPlan):
# Commit boundary for standard patrolling flight plan
start = flight.flight_plan.layout.patrol_start
end = flight.flight_plan.layout.patrol_end
commit_center = LineString(
[
ShapelyPoint(start.x, start.y),
ShapelyPoint(end.x, end.y),
]
)
else:
if not isinstance(flight.flight_plan, UiZoneDisplay):
return []
bubble = commit_center.buffer(flight.flight_plan.engagement_distance.meters)
zone = flight.flight_plan.ui_zone()
if len(zone.points) == 1:
center = ShapelyPoint(zone.points[0].x, zone.points[0].y)
else:
center = LineString([ShapelyPoint(p.x, p.y) for p in zone.points])
bubble = center.buffer(zone.radius.meters)
return ShapelyUtil.poly_to_leaflet(bubble, game.theater)

View File

@@ -14,6 +14,7 @@ from .minutesoption import minutes_option
from .optiondescription import OptionDescription, SETTING_DESCRIPTION_KEY
from .skilloption import skill_option
from ..ato.starttype import StartType
from ..weather import NightMissions
@unique
@@ -56,19 +57,19 @@ class Settings:
"Player coalition skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="Good",
default="High",
)
enemy_skill: str = skill_option(
"Enemy coalition skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="Average",
default="High",
)
enemy_vehicle_skill: str = skill_option(
"Enemy AA and vehicles skill",
page=DIFFICULTY_PAGE,
section=AI_DIFFICULTY_SECTION,
default="Average",
default="High",
)
player_income_multiplier: float = bounded_float_option(
"Player income multiplier",
@@ -105,11 +106,16 @@ class Settings:
section=MISSION_DIFFICULTY_SECTION,
default=True,
)
night_disabled: bool = boolean_option(
"No night missions",
night_day_missions: NightMissions = choices_option(
"Night/day mission options",
page=DIFFICULTY_PAGE,
section=MISSION_DIFFICULTY_SECTION,
default=False,
choices={
"Generate night and day missions": NightMissions.DayAndNight,
"Only generate day missions": NightMissions.OnlyDay,
"Only generate night missions": NightMissions.OnlyNight,
},
default=NightMissions.DayAndNight,
)
# Mission Restrictions
labels: str = choices_option(
@@ -205,10 +211,10 @@ class Settings:
)
#: Feature flag for squadron limits.
enable_squadron_pilot_limits: bool = boolean_option(
"Enable per-squadron pilot limits (WIP)",
"Enable per-squadron pilot limits",
CAMPAIGN_MANAGEMENT_PAGE,
PILOTS_AND_SQUADRONS_SECTION,
default=False,
default=True,
detail=(
"If set, squadrons will be limited to a maximum number of pilots and dead "
"pilots will replenish at a fixed rate, each defined with the settings"
@@ -225,7 +231,7 @@ class Settings:
CAMPAIGN_MANAGEMENT_PAGE,
PILOTS_AND_SQUADRONS_SECTION,
default=12,
min=12,
min=6,
max=72,
detail=(
"Sets the maximum number of pilots a squadron may have active. "
@@ -476,6 +482,12 @@ class Settings:
section=PERFORMANCE_SECTION,
default=True,
)
perf_disable_idle_aircraft: bool = boolean_option(
"Disable idle aircraft at airfields",
page=MISSION_GENERATOR_PAGE,
section=PERFORMANCE_SECTION,
default=False,
)
# Performance culling
perf_culling: bool = boolean_option(
"Culling of distant units enabled",

View File

@@ -37,6 +37,7 @@ from dcs.ships import (
LHA_Tarawa,
Stennis,
Type_071,
Hms_invincible,
)
from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unitgroup import ShipGroup, StaticGroup
@@ -1203,6 +1204,7 @@ class NavalControlPoint(ControlPoint, ABC):
LHA_Tarawa,
KUZNECOW,
Type_071,
Hms_invincible,
]:
return True
return False

View File

@@ -53,7 +53,7 @@ class SkynetNode:
# Statics will be placed as dead unit
return group.units[0].unit_name
# If no alive unit is available and not static raise error
raise IadsNetworkException("Group has no skynet usable units")
raise IadsNetworkException(f"{group.name} has no skynet usable units")
else:
# Use the GroupName for SAMs, SAMAsEWR and PDs
return group.group_name
@@ -121,6 +121,10 @@ class IadsNetwork:
# Skip culled ground objects
continue
all_dead = not any([x.alive for x in node.group.units])
if all_dead:
continue
# SkynetNode.from_group(node.group) may raise an exception
# (originating from SkynetNode.dcs_name_for_group)
# but if it does, we want to know because it's supposed to be impossible afaict
@@ -194,8 +198,11 @@ class IadsNetwork:
# Not participating
return
events.update_iads_node(node)
if self.advanced_iads and not self.iads_config:
self._make_advanced_connections_by_range(node)
if self.advanced_iads:
if self.iads_config:
self._add_connections_from_config(node)
else:
self._make_advanced_connections_by_range(node)
def node_for_group(self, group: IadsGroundGroup) -> IadsNetworkNode:
"""Get existing node from the iads network or create a new node"""
@@ -217,7 +224,7 @@ class IadsNetwork:
node: Optional[IadsNetworkNode] = None
for group in tgo.groups:
# TODO Cleanup
if isinstance(group, IadsGroundGroup):
if isinstance(group, IadsGroundGroup) and group.alive_units > 0:
# The first IadsGroundGroup is always the primary Group
if not node and group.iads_role.participate:
# Primary Node
@@ -255,17 +262,17 @@ class IadsNetwork:
def initialize_network_from_config(self) -> None:
"""Initialize the IADS Network from a configuration"""
for element_name, connections in self.iads_config.items():
for primary_node in self.iads_config.keys():
warning_msg = (
f"IADS: No ground object found for {element_name}."
f"IADS: No ground object found for {primary_node}."
f" This can be normal behaviour."
)
if element_name in self.ground_objects:
node = self.node_for_tgo(self.ground_objects[element_name])
if primary_node in self.ground_objects:
node = self.node_for_tgo(self.ground_objects[primary_node])
else:
node = None
warning_msg = (
f"IADS: No ground object found for connection {element_name}"
f"IADS: No ground object found for connection {primary_node}"
)
if node is None:
@@ -274,16 +281,20 @@ class IadsNetwork:
# available. Therefore the TGO will not get populated at all
logging.warning(warning_msg)
continue
self._add_connections_from_config(node)
# Find all connected ground_objects
for node_name in connections:
try:
node.add_connection_for_tgo(self.ground_objects[node_name])
except KeyError:
logging.error(
f"IADS: No ground object found for connection {node_name}"
)
continue
def _add_connections_from_config(self, node: IadsNetworkNode) -> None:
"""Add all connections for the given primary node based on the iads_config"""
primary_node = node.group.ground_object.original_name
connections = self.iads_config[primary_node]
for secondary_node in connections:
try:
node.add_connection_for_tgo(self.ground_objects[secondary_node])
except KeyError:
logging.error(
f"IADS: No ground object found for connection {secondary_node}"
)
continue
def initialize_network_from_range(self) -> None:
"""Initialize the IADS Network by range"""

View File

@@ -45,4 +45,9 @@ class SeasonalConditions:
winter_avg_temperature: float
temperature_day_night_difference: float
high_avg_yearly_turbulence_per_10cm: float
low_avg_yearly_turbulence_per_10cm: float
solar_noon_turbulence_per_10cm: float
midnight_turbulence_per_10cm: float
weather_type_chances: dict[Season, WeatherTypeChances]

View File

@@ -55,6 +55,7 @@ class ModSettings:
a6a_intruder: bool = False
f4b_phantom: bool = False
f15d_baz: bool = False
f_16_idf: bool = False
f22_raptor: bool = False
f84g_thunderjet: bool = False
f100_supersabre: bool = False
@@ -67,6 +68,7 @@ class ModSettings:
su57_felon: bool = False
frenchpack: bool = False
high_digit_sams: bool = False
ov10a_bronco: bool = False
class GameGenerator:

View File

@@ -57,6 +57,23 @@ class SeasonData:
)
@dataclass(frozen=True)
class TurbulenceData:
high_avg_yearly_turbulence_per_10cm: float | None
low_avg_yearly_turbulence_per_10cm: float | None
solar_noon_turbulence_per_10cm: float | None
midnight_turbulence_per_10cm: float | None
@staticmethod
def from_yaml(data: dict[str, Any]) -> TurbulenceData:
return TurbulenceData(
data.get("high_avg_yearly_turbulence_per_10cm"),
data.get("low_avg_yearly_turbulence_per_10cm"),
data.get("solar_noon_turbulence_per_10cm"),
data.get("midnight_turbulence_per_10cm"),
)
class TheaterLoader:
THEATER_RESOURCE_DIR = Path("resources/theaters")
@@ -113,6 +130,7 @@ class TheaterLoader:
spring = SeasonData.from_yaml(climate_data["seasons"]["spring"])
summer = SeasonData.from_yaml(climate_data["seasons"]["summer"])
fall = SeasonData.from_yaml(climate_data["seasons"]["fall"])
turbulence = TurbulenceData.from_yaml(climate_data["turbulence"])
if summer.average_pressure is None:
raise RuntimeError(
f"{self.descriptor_path} does not define a summer average pressure"
@@ -129,12 +147,32 @@ class TheaterLoader:
raise RuntimeError(
f"{self.descriptor_path} does not define a winter average temperature"
)
if turbulence.high_avg_yearly_turbulence_per_10cm is None:
raise RuntimeError(
f"{self.descriptor_path} does not define a yearly average high turbulence"
)
if turbulence.low_avg_yearly_turbulence_per_10cm is None:
raise RuntimeError(
f"{self.descriptor_path} does not define a yearly average low turbulence"
)
if turbulence.solar_noon_turbulence_per_10cm is None:
raise RuntimeError(
f"{self.descriptor_path} does not define a solar noon turbulence"
)
if turbulence.midnight_turbulence_per_10cm is None:
raise RuntimeError(
f"{self.descriptor_path} does not define a midnight turbulence"
)
return SeasonalConditions(
summer.average_pressure,
winter.average_pressure,
summer.average_temperature,
winter.average_temperature,
climate_data["day_night_temperature_difference"],
turbulence.high_avg_yearly_turbulence_per_10cm,
turbulence.low_avg_yearly_turbulence_per_10cm,
turbulence.solar_noon_turbulence_per_10cm,
turbulence.midnight_turbulence_per_10cm,
{
Season.Winter: winter.weather,
Season.Spring: spring.weather,

View File

@@ -161,10 +161,15 @@ VERSION = _build_version_string()
#:
#: Version 10.3
#: * Campaign files can optionally include a start time in their recommended_start_date
#: field. For example, `recommended_start_data: 2022-08-31 13:30:00` will have the
#: field. For example, `recommended_start_date: 2022-08-31 13:30:00` will have the
#: first turn start at 13:30. If omitted, or if only a date is given, the mission will
#: start at a random hour in the middle of the day as before.
#:
#: Version 10.4
#: * Support for the Falklands.
CAMPAIGN_FORMAT_VERSION = (10, 4)
#:
#: Version 10.5
#: * Support for scenery objectives defined by quad zones.
#: * Campaign designers can now define `max_frontline_length` (in km) setting:
#: `max_frontline_length: 25`
CAMPAIGN_FORMAT_VERSION = (10, 5)

View File

@@ -2,8 +2,10 @@ from __future__ import annotations
import datetime
import logging
import math
import random
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, TYPE_CHECKING
from dcs.cloud_presets import Clouds as PydcsClouds
@@ -11,7 +13,15 @@ from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
from game.theater.seasonalconditions import determine_season
from game.timeofday import TimeOfDay
from game.utils import Distance, Heading, Pressure, inches_hg, interpolate, meters
from game.utils import (
Distance,
Heading,
Pressure,
inches_hg,
interpolate,
knots,
meters,
)
if TYPE_CHECKING:
from game.settings import Settings
@@ -19,6 +29,12 @@ if TYPE_CHECKING:
from game.theater.seasonalconditions import SeasonalConditions
class NightMissions(Enum):
DayAndNight = "nightmissions_nightandday"
OnlyDay = "nightmissions_onlyday"
OnlyNight = "nightmissions_onlynight"
@dataclass(frozen=True)
class AtmosphericConditions:
#: Pressure at sea level.
@@ -27,6 +43,9 @@ class AtmosphericConditions:
#: Temperature at sea level in Celcius.
temperature_celsius: float
#: Turbulence per 10 cm.
turbulence_per_10cm: float
@dataclass(frozen=True)
class WindConditions:
@@ -99,18 +118,38 @@ class Weather:
day,
)
seasonal_turbulence = self.interpolate_seasonal_turbulence(
seasonal_conditions.high_avg_yearly_turbulence_per_10cm,
seasonal_conditions.low_avg_yearly_turbulence_per_10cm,
day,
)
day_turbulence = seasonal_conditions.solar_noon_turbulence_per_10cm
night_turbulence = seasonal_conditions.midnight_turbulence_per_10cm
time_of_day_turbulence = self.interpolate_solar_activity(
time_of_day, day_turbulence, night_turbulence
)
random_turbulence = random.normalvariate(mu=0, sigma=0.5)
turbulence = abs(
seasonal_turbulence + time_of_day_turbulence + random_turbulence
)
if time_of_day == TimeOfDay.Day:
temperature += seasonal_conditions.temperature_day_night_difference / 2
if time_of_day == TimeOfDay.Night:
temperature -= seasonal_conditions.temperature_day_night_difference / 2
pressure += self.pressure_adjustment
temperature += self.temperature_adjustment
turbulence += self.turbulence_adjustment
logging.debug(
"Weather: Before random: temp {} press {}".format(temperature, pressure)
)
conditions = AtmosphericConditions(
qnh=self.random_pressure(pressure),
temperature_celsius=self.random_temperature(temperature),
turbulence_per_10cm=turbulence,
)
logging.debug(
"Weather: After random: temp {} press {}".format(
@@ -127,6 +166,10 @@ class Weather:
def temperature_adjustment(self) -> float:
raise NotImplementedError
@property
def turbulence_adjustment(self) -> float:
raise NotImplementedError
def generate_clouds(self) -> Optional[Clouds]:
raise NotImplementedError
@@ -147,24 +190,62 @@ class Weather:
wind_direction_2000m = wind_direction + Heading.random(-90, 90)
wind_direction_8000m = wind_direction + Heading.random(-90, 90)
at_0m_factor = 1
at_2000m_factor = 2
at_8000m_factor = 3
at_2000m_factor = 3 + random.choice([0, 0, 0, 0, 0, 1, 1])
high_alt_variation = random.choice(
[
-3,
-3,
-2,
-2,
-2,
-2,
-2,
-2,
-1,
-1,
-1,
-1,
-1,
-1,
-1,
0,
0,
0,
1,
1,
2,
3,
]
)
at_8000m_factor = at_2000m_factor + 5 + high_alt_variation
base_wind = random.randint(minimum, maximum)
# DCS is limited to 97 knots wind speed.
max_supported_wind_speed = knots(97).meters_per_second
return WindConditions(
# Always some wind to make the smoke move a bit.
at_0m=Wind(wind_direction.degrees, max(1, base_wind * at_0m_factor)),
at_2000m=Wind(wind_direction_2000m.degrees, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction_8000m.degrees, base_wind * at_8000m_factor),
at_2000m=Wind(
wind_direction_2000m.degrees,
min(max_supported_wind_speed, base_wind * at_2000m_factor),
),
at_8000m=Wind(
wind_direction_8000m.degrees,
min(max_supported_wind_speed, base_wind * at_8000m_factor),
),
)
@staticmethod
def random_cloud_base() -> int:
return random.randint(2000, 3000)
return random.randint(1000, 5000)
@staticmethod
def random_cloud_thickness() -> int:
return random.randint(100, 400)
# values lower than 400m can generate clear skies in some cases
return random.randint(400, 2000)
@staticmethod
def random_pressure(average_pressure: float) -> Pressure:
@@ -197,6 +278,42 @@ class Weather:
winter_factor = distance_from_peak_summer / day_of_year_peak_summer
return interpolate(summer_value, winter_value, winter_factor, clamp=True)
@staticmethod
def interpolate_seasonal_turbulence(
high_value: float, low_value: float, day: datetime.date
) -> float:
day_of_year = day.timetuple().tm_yday
day_of_year_peak_summer = 183
distance_from_peak_summer = -day_of_year_peak_summer + day_of_year
amplitude = 0.5 * (high_value - low_value)
offset = amplitude + low_value
# A high peak in summer and winter, between high_value and low_value.
return (
amplitude * math.cos(4 * math.pi * distance_from_peak_summer / 365.25)
+ offset
)
@staticmethod
def interpolate_solar_activity(
time_of_day: TimeOfDay, high: float, low: float
) -> float:
scale: float = 0
match time_of_day:
case TimeOfDay.Dawn:
scale = 0.4
case TimeOfDay.Day:
scale = 1
case TimeOfDay.Dusk:
scale = 0.6
case TimeOfDay.Night:
scale = 0
return interpolate(value1=low, value2=high, factor=scale, clamp=True)
class ClearSkies(Weather):
@property
@@ -207,6 +324,10 @@ class ClearSkies(Weather):
def temperature_adjustment(self) -> float:
return 3.0
@property
def turbulence_adjustment(self) -> float:
return 0.0
def generate_clouds(self) -> Optional[Clouds]:
return None
@@ -226,6 +347,10 @@ class Cloudy(Weather):
def temperature_adjustment(self) -> float:
return 0.0
@property
def turbulence_adjustment(self) -> float:
return 0.75
def generate_clouds(self) -> Optional[Clouds]:
return Clouds.random_preset(rain=False)
@@ -234,7 +359,7 @@ class Cloudy(Weather):
return None
def generate_wind(self) -> WindConditions:
return self.random_wind(1, 4)
return self.random_wind(1, 5)
class Raining(Weather):
@@ -246,6 +371,10 @@ class Raining(Weather):
def temperature_adjustment(self) -> float:
return -3.0
@property
def turbulence_adjustment(self) -> float:
return 1.5
def generate_clouds(self) -> Optional[Clouds]:
return Clouds.random_preset(rain=True)
@@ -254,7 +383,7 @@ class Raining(Weather):
return None
def generate_wind(self) -> WindConditions:
return self.random_wind(1, 6)
return self.random_wind(2, 6)
class Thunderstorm(Weather):
@@ -266,6 +395,10 @@ class Thunderstorm(Weather):
def temperature_adjustment(self) -> float:
return -3.0
@property
def turbulence_adjustment(self) -> float:
return 3.0
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
@@ -275,7 +408,7 @@ class Thunderstorm(Weather):
)
def generate_wind(self) -> WindConditions:
return self.random_wind(1, 8)
return self.random_wind(2, 8)
@dataclass
@@ -298,7 +431,7 @@ class Conditions:
_start_time = datetime.datetime.combine(day, forced_time)
else:
_start_time = cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled
theater, day, time_of_day, settings.night_day_missions
)
return cls(
@@ -313,11 +446,11 @@ class Conditions:
theater: ConflictTheater,
day: datetime.date,
time_of_day: TimeOfDay,
night_disabled: bool,
night_day_missions: NightMissions,
) -> datetime.datetime:
if night_disabled:
from game.theater import DaytimeMap
from game.theater import DaytimeMap
if night_day_missions == NightMissions.OnlyDay:
logging.info("Skip Night mission due to user settings")
time_range = DaytimeMap(
dawn=(datetime.time(hour=8), datetime.time(hour=9)),
@@ -325,6 +458,14 @@ class Conditions:
dusk=(datetime.time(hour=12), datetime.time(hour=14)),
night=(datetime.time(hour=14), datetime.time(hour=17)),
).range_of(time_of_day)
elif night_day_missions == NightMissions.OnlyNight:
logging.info("Skip Day mission due to user settings")
time_range = DaytimeMap(
dawn=(datetime.time(hour=0), datetime.time(hour=3)),
day=(datetime.time(hour=3), datetime.time(hour=6)),
dusk=(datetime.time(hour=21), datetime.time(hour=22)),
night=(datetime.time(hour=22), datetime.time(hour=23)),
).range_of(time_of_day)
else:
time_range = theater.daytime_map.range_of(time_of_day)

View File

@@ -13,6 +13,7 @@ from .jas39 import *
from .su30 import *
from .su57 import *
from .uh60l import *
from .ov10a import *
def load_mods() -> None:

View File

@@ -88,39 +88,49 @@ class WeaponsA4EC:
"name": "BDU-33 *6 (MER)",
"weight": 167.6,
}
CBU_1_A = {"clsid": "{CBU-1/A}", "name": "CBU-1/A", "weight": 458.921706}
BLU_3B_x_19 = {"clsid": "{BLU_3B_GROUP}", "name": "BLU-3B x 19", "weight": 0.793786}
BLU_4B_x_27 = {
"clsid": "{BLU_4B_GROUP}",
"name": "BLU-4B x 27",
"weight": 0.5443104,
}
CBU_1_A = {"clsid": "{CBU-1/A}", "name": "CBU-1/A", "weight": 337.2910112}
CBU_1_A_2 = {"clsid": "{CBU-1/A*2}", "name": "CBU-1/A*2", "weight": 616.5222464}
CBU_1_A__2 = {
"clsid": "{CBU-1/A_TER_2_L}",
"name": "CBU-1/A *2",
"weight": 713.473056,
"weight": 722.1820224,
}
CBU_1_A__2_ = {
"clsid": "{CBU-1/A_TER_2_R}",
"name": "CBU-1/A *2",
"weight": 713.473056,
"weight": 722.1820224,
}
CBU_2B_A = {"clsid": "{CBU-2B/A}", "name": "CBU-2B/A", "weight": 379.543106}
CBU_2B_A = {"clsid": "{CBU-2B/A}", "name": "CBU-2B/A", "weight": 344.616522}
CBU_2B_A_2 = {"clsid": "{CBU-2B/A*2}", "name": "CBU-2B/A*2", "weight": 631.173268}
CBU_2B_A__2 = {
"clsid": "{CBU-2B/A_TER_2_L}",
"name": "CBU-2B/A *2",
"weight": 806.686212,
"weight": 827.324648,
}
CBU_2B_A__2_ = {
"clsid": "{CBU-2B/A_TER_2_R}",
"name": "CBU-2B/A *2",
"weight": 806.686212,
"weight": 827.324648,
}
CBU_2_A = {"clsid": "{CBU-2/A}", "name": "CBU-2/A", "weight": 343.822736}
CBU_2_A = {"clsid": "{CBU-2/A}", "name": "CBU-2/A", "weight": 344.616522}
CBU_2_A_2 = {"clsid": "{CBU-2/A*2}", "name": "CBU-2/A*2", "weight": 631.173268}
CBU_2_A__2 = {
"clsid": "{CBU-2/A_TER_2_L}",
"name": "CBU-2/A *2",
"weight": 735.245472,
"weight": 736.833044,
}
CBU_2_A__2_ = {
"clsid": "{CBU-2/A_TER_2_R}",
"name": "CBU-2/A *2",
"weight": 735.245472,
"weight": 736.833044,
}
CBU_DUMMY = {"clsid": "{CBU_DUMMY}", "name": "CBU_DUMMY", "weight": 58.059776}
Fuel_Tank_150_gallons = {
"clsid": "{DFT-150gal}",
"name": "Fuel Tank 150 gallons",

View File

@@ -297,6 +297,10 @@ class VSN_F100(PlaneType):
7,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
LAU_118a_with_AGM_45B_Shrike_ARM__Imp_ = (
7,
Weapons.LAU_118a_with_AGM_45B_Shrike_ARM__Imp_,
)
Fuel_tank_500_Liter = (7, WeaponsF100.Fuel_tank_500_Liter)
# ERRR <CLEAN>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,174 @@
from typing import Any, Dict, Set
from dcs import task
from dcs.liveries_scanner import Liveries
from dcs.planes import PlaneType
from dcs.weapons_data import Weapons
from game.modsupport import planemod
from pydcs_extensions.weapon_injector import inject_weapons
class WeaponsOV10A:
OV10_SMOKE = {"clsid": "{OV10_SMOKE}", "name": "OV10_SMOKE", "weight": 1}
ParaTrooper = {"clsid": "{PARA}", "name": "ParaTrooper", "weight": 80}
Fuel_Tank_150_gallons_ = {
"clsid": "{150gal}",
"name": "Fuel Tank 150 gallons",
"weight": 499.5592,
}
inject_weapons(WeaponsOV10A)
@planemod
class Bronco_OV_10A(PlaneType):
id = "Bronco-OV-10A"
flyable = True
height = 4.62
width = 12.9
length = 12.76
fuel_max = 940
max_speed = 684
eplrs = True
category = "Interceptor" # {78EFB7A2-FD52-4b57-A6A6-3BF0E1D6555F}
radio_frequency = 127.5
panel_radio = {
1: {
"channels": {6: 41, 2: 31, 8: 50, 3: 32, 1: 30, 4: 33, 5: 40, 7: 42},
},
}
livery_name = "BRONCO-OV-10A" # from type
Liveries = Liveries()[livery_name]
class Pylon1:
LAU_7_with_AIM_9P_Sidewinder_IR_AAM = (
1,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
# ERRR {MK-81}
class Pylon2:
Mk_82___500lb_GP_Bomb_LD = (2, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_82_Snakeye___500lb_GP_Bomb_HD = (2, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (2, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (2, Weapons.M117___750lb_GP_Bomb_LD)
LAU3_WP156 = (2, Weapons.LAU3_WP156)
LAU3_WP1B = (2, Weapons.LAU3_WP1B)
LAU3_WP61 = (2, Weapons.LAU3_WP61)
LAU3_HE5 = (2, Weapons.LAU3_HE5)
LAU3_HE151 = (2, Weapons.LAU3_HE151)
M260_HYDRA = (2, Weapons.M260_HYDRA)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
2,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
2,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
# ERRR {MK-81}
class Pylon3:
Mk_82___500lb_GP_Bomb_LD = (3, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_82_Snakeye___500lb_GP_Bomb_HD = (3, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (3, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (3, Weapons.M117___750lb_GP_Bomb_LD)
LAU3_WP156 = (3, Weapons.LAU3_WP156)
LAU3_WP1B = (3, Weapons.LAU3_WP1B)
LAU3_WP61 = (3, Weapons.LAU3_WP61)
LAU3_HE5 = (3, Weapons.LAU3_HE5)
LAU3_HE151 = (3, Weapons.LAU3_HE151)
M260_HYDRA = (3, Weapons.M260_HYDRA)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
3,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
3,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
class Pylon4:
Fuel_Tank_150_gallons_ = (4, Weapons.Fuel_Tank_150_gallons_)
# ERRR {MK-81}
Mk_82___500lb_GP_Bomb_LD = (4, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_82_Snakeye___500lb_GP_Bomb_HD = (4, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (4, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (4, Weapons.M117___750lb_GP_Bomb_LD)
# ERRR {MK-81}
class Pylon5:
Mk_82___500lb_GP_Bomb_LD = (5, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_82_Snakeye___500lb_GP_Bomb_HD = (5, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (5, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (5, Weapons.M117___750lb_GP_Bomb_LD)
LAU3_WP156 = (5, Weapons.LAU3_WP156)
LAU3_WP1B = (5, Weapons.LAU3_WP1B)
LAU3_WP61 = (5, Weapons.LAU3_WP61)
LAU3_HE5 = (5, Weapons.LAU3_HE5)
LAU3_HE151 = (5, Weapons.LAU3_HE151)
M260_HYDRA = (5, Weapons.M260_HYDRA)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
5,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
5,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
# ERRR {MK-81}
class Pylon6:
Mk_82___500lb_GP_Bomb_LD = (6, Weapons.Mk_82___500lb_GP_Bomb_LD)
Mk_82_Snakeye___500lb_GP_Bomb_HD = (6, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (6, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (6, Weapons.M117___750lb_GP_Bomb_LD)
LAU3_WP156 = (6, Weapons.LAU3_WP156)
LAU3_WP1B = (6, Weapons.LAU3_WP1B)
LAU3_WP61 = (6, Weapons.LAU3_WP61)
LAU3_HE5 = (6, Weapons.LAU3_HE5)
LAU3_HE151 = (6, Weapons.LAU3_HE151)
M260_HYDRA = (6, Weapons.M260_HYDRA)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
6,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
6,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
class Pylon7:
LAU_7_with_AIM_9P_Sidewinder_IR_AAM = (
7,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
class Pylon8:
ParaTrooper = (8, Weapons.ParaTrooper)
class Pylon9:
OV10_SMOKE = (9, Weapons.OV10_SMOKE)
pylons: Set[int] = {1, 2, 3, 4, 5, 6, 7, 8, 9}
tasks = [
task.GroundAttack,
task.RunwayAttack,
task.PinpointStrike,
task.CAS,
task.AFAC,
task.CAP,
task.Escort,
task.FighterSweep,
task.Intercept,
]
task_default = task.CAS

View File

@@ -0,0 +1,17 @@
from typing import Tuple, Any
def inject_pylon(to_pylon: Any, from_pylon: Any) -> None:
"""
Inject weapons/ordnance added by mods into the pylons of existing aircraft.
This is done to support mods such as the CJS Super Hornet, which modify aircraft
that exist in stock DCS. Ornance is injected pydcs aircraft classes via introspection
:param to_pylon: The pydcs pylon class of the target aircraft
:param from_pylon: The custom pylon class containing tuples with added weapon info
:return: None
"""
for key, value in from_pylon.__dict__.items():
if key.startswith("__"):
continue
if isinstance(value, Tuple):
setattr(to_pylon, key, value)

View File

@@ -28,6 +28,7 @@ from qt_ui import (
liberation_theme,
uiconstants,
)
from qt_ui.uiflags import UiFlags
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
@@ -56,12 +57,24 @@ def inject_custom_payloads(user_path: Path) -> None:
PayloadDirectories.set_preferred(user_path / "MissionEditor" / "UnitPayloads")
def inject_mod_payloads(mod_path: Path) -> None:
if mod_path.exists():
payloads = mod_path
else:
raise RuntimeError(
f"Could not find mod payloads at {mod_path}."
f"Aircraft will have no payloads."
)
# We configure these as preferred so the mod's loadouts override the stock ones.
PayloadDirectories.set_preferred(payloads)
def on_game_load(game: Game | None) -> None:
EventStream.drain()
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
def run_ui(game: Game | None, dev: bool) -> None:
def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" # Potential fix for 4K screens
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
@@ -153,7 +166,7 @@ def run_ui(game: Game | None, dev: bool) -> None:
GameUpdateSignal.get_instance().game_loaded.connect(on_game_load)
# Start window
window = QLiberationWindow(game, dev)
window = QLiberationWindow(game, ui_flags)
window.showMaximized()
splash.finish(window)
qt_execution_code = app.exec_()
@@ -183,6 +196,19 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--dev", action="store_true", help="Enable development mode.")
speed_controls_group = parser.add_argument_group()
speed_controls_group.add_argument(
"--show-sim-speed-controls",
action="store_true",
help="Shows the sim speed controls in the top panel.",
)
speed_controls_group.add_argument(
"--no-show-sim-speed-controls",
dest="show_sim_speed_controls",
action="store_false",
help="Hides the sim speed controls in the top panel (default).",
)
parser.add_argument("--new-map", help="Deprecated. Does nothing.")
parser.add_argument("--old-map", help="Deprecated. Does nothing.")
@@ -373,7 +399,7 @@ def main():
return
with Server().run_in_thread():
run_ui(game, args.dev)
run_ui(game, UiFlags(args.dev, args.show_sim_speed_controls))
if __name__ == "__main__":

View File

@@ -205,6 +205,7 @@ def load_aircraft_icons():
os.path.join("./resources/ui/units/aircrafts/icons/", aircraft)
)
AIRCRAFT_ICONS["F-16C_50"] = AIRCRAFT_ICONS["F-16C"]
AIRCRAFT_ICONS["F-16A_MLU"] = AIRCRAFT_ICONS["F-16A"]
AIRCRAFT_ICONS["FA-18C_hornet"] = AIRCRAFT_ICONS["FA-18C"]
AIRCRAFT_ICONS["A-10C_2"] = AIRCRAFT_ICONS["A-10C"]
f1_refuel = ["Mirage-F1CT", "Mirage-F1EE", "Mirage-F1M-EE", "Mirage-F1EQ"]

17
qt_ui/uiflags.py Normal file
View File

@@ -0,0 +1,17 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class UiFlags:
"""Flags for the UI that are not exposed in settings.
These flags are hidden from the settings UI because they can only be controlled from
the command-line during startup.
"""
# True if the front-end should connect to the development react webserver instead of
# the built front-end app.
dev_ui_webserver: bool
# True if the play/pause/speed controls should be visible in the top panel.
show_sim_speed_controls: bool

View File

@@ -17,6 +17,7 @@ from game.profiling import logged_duration
from game.utils import meters
from qt_ui.models import GameModel
from qt_ui.simcontroller import SimController
from qt_ui.uiflags import UiFlags
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.widgets.QConditionsWidget import QConditionsWidget
from qt_ui.widgets.QFactionsInfos import QFactionsInfos
@@ -30,7 +31,9 @@ from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResul
class QTopPanel(QFrame):
def __init__(self, game_model: GameModel, sim_controller: SimController) -> None:
def __init__(
self, game_model: GameModel, sim_controller: SimController, ui_flags: UiFlags
) -> None:
super(QTopPanel, self).__init__()
self.game_model = game_model
self.sim_controller = sim_controller
@@ -82,7 +85,8 @@ class QTopPanel(QFrame):
self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout()
self.proceedBoxLayout.addLayout(self.simSpeedControls)
if ui_flags.show_sim_speed_controls:
self.proceedBoxLayout.addLayout(self.simSpeedControls)
self.proceedBoxLayout.addLayout(MaxPlayerCount(self.game_model.ato_model))
self.proceedBoxLayout.addWidget(self.passTurnButton)
self.proceedBoxLayout.addWidget(self.proceedButton)

View File

@@ -7,7 +7,6 @@ from game.missiongenerator.frontlineconflictdescription import (
FrontLineConflictDescription,
)
from game.theater.controlpoint import ControlPointType
from game.theater.theatergroundobject import BuildingGroundObject, IadsGroundObject
from game.utils import Distance
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
@@ -69,7 +68,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if self.include_frontlines:
for front_line in self.game.theater.conflicts():
pos = FrontLineConflictDescription.frontline_position(
front_line, self.game.theater
front_line, self.game.theater, self.game.settings
)[0]
wptname = f"Frontline {front_line.name} [CAS]"
wpt = FlightWaypoint(
@@ -81,66 +80,25 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.description = "Frontline"
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_targets:
for cp in self.game.theater.controlpoints:
if (self.include_enemy and not cp.captured) or (
self.include_friendly and cp.captured
):
for ground_object in cp.ground_objects:
if not ground_object.is_dead and isinstance(
ground_object, BuildingGroundObject
):
wpt = FlightWaypoint(
ground_object.waypoint_name,
FlightWaypointType.CUSTOM,
ground_object.position,
Distance.from_meters(0),
)
wpt.alt_type = "RADIO"
wpt.pretty_name = wpt.name
wpt.obj_name = ground_object.obj_name
wpt.targets.append(ground_object)
if cp.captured:
wpt.description = "Friendly Building"
else:
wpt.description = "Enemy Building"
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_units:
for cp in self.game.theater.controlpoints:
if (self.include_enemy and not cp.captured) or (
self.include_friendly and cp.captured
):
for ground_object in cp.ground_objects:
if not ground_object.is_dead and (
isinstance(ground_object, IadsGroundObject)
):
for g in ground_object.groups:
for j, u in enumerate(g.units):
wptname = (
"["
+ str(ground_object.obj_name)
+ "] : "
+ u.name
+ " #"
+ str(j)
)
wpt = FlightWaypoint(
wptname,
FlightWaypointType.CUSTOM,
u.position,
Distance.from_meters(0),
)
wpt.alt_type = "RADIO"
wpt.pretty_name = wptname
wpt.targets.append(u)
wpt.obj_name = ground_object.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly unit: " + u.name
else:
wpt.description = "Enemy unit: " + u.name
i = add_model_item(i, model, wpt.pretty_name, wpt)
for tgo in self.game.theater.ground_objects:
for target_idx, target in enumerate(tgo.strike_targets):
wptname = f"[{tgo.obj_name}] : {target.name} #{target_idx}"
wpt = FlightWaypoint(
wptname,
FlightWaypointType.CUSTOM,
target.position,
Distance.from_meters(0),
)
wpt.alt_type = "RADIO"
wpt.pretty_name = wptname
wpt.targets.append(target)
wpt.obj_name = tgo.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM
if tgo.is_friendly(to_player=True):
wpt.description = f"Friendly unit: {target.name}"
else:
wpt.description = f"Enemy unit: {target.name}"
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases:
for cp in self.game.theater.controlpoints:

View File

@@ -1,71 +0,0 @@
from PySide2.QtGui import QStandardItem, QStandardItemModel
from game import Game
from game.theater import SamGroundObject
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
class StrikeTargetInfo:
def __init__(self):
self.name = ""
self.location = None
self.units = []
self.buildings = []
class QStrikeTargetSelectionComboBox(QFilteredComboBox):
def __init__(self, game: Game, parent=None):
super(QStrikeTargetSelectionComboBox, self).__init__(parent)
self.game = game
self.find_possible_strike_targets()
for t in self.targets:
print(t.name + " - " + str(len(t.units)) + " " + str(len(t.buildings)))
def get_selected_target(self) -> StrikeTargetInfo:
n = self.currentText()
for target in self.targets:
if target.name == n:
return target
def find_possible_strike_targets(self):
self.targets = []
i = 0
model = QStandardItemModel()
def add_model_item(i, model, target):
item = QStandardItem(target.name)
model.setItem(i, 0, item)
self.targets.append(target)
return i + 1
for cp in self.game.theater.controlpoints:
if cp.captured:
continue
added_obj_names = []
for g in cp.ground_objects:
if g.obj_name in added_obj_names:
continue
target = StrikeTargetInfo()
target.location = g
target.name = g.obj_name
if isinstance(g, SamGroundObject):
target.name = g.obj_name + " [units]"
for group in g.groups:
for u in group.units:
target.units.append(u)
else:
target.name = g.obj_name + " [" + g.category + "]"
for g2 in cp.ground_objects:
if g2 is not g and g2.obj_name == g.obj_name:
target.buildings.append(g2)
i = add_model_item(i, model, target)
added_obj_names.append(g.obj_name)
self.setModel(model)

View File

@@ -1,69 +0,0 @@
from PySide2.QtGui import QStandardItemModel, QStandardItem
from PySide2.QtWidgets import (
QGroupBox,
QVBoxLayout,
QListView,
QAbstractItemView,
)
from qt_ui.widgets.combos.QStrikeTargetSelectionComboBox import StrikeTargetInfo
class QStrikeTargetInfoView(QGroupBox):
"""
UI Component to display info about a strike target
"""
def __init__(self, strike_target_infos: StrikeTargetInfo):
if strike_target_infos is None:
strike_target_infos = StrikeTargetInfo()
super(QStrikeTargetInfoView, self).__init__(
"Target : " + strike_target_infos.name
)
self.strike_target_infos = strike_target_infos
self.listView = QListView()
self.init_ui()
def init_ui(self):
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.addWidget(self.listView)
self.setLayout(layout)
def setTarget(self, target):
self.setTitle(target.name)
self.strike_target_infos = target
model = QStandardItemModel()
self.listView.setSelectionMode(QAbstractItemView.NoSelection)
if len(self.strike_target_infos.units) > 0:
dic = {}
for u in self.strike_target_infos.units:
if u.type.id in dic.keys():
dic[u.type.id] = dic[u.type.id] + 1
else:
dic[u.type.id] = 1
for k, v in dic.items():
model.appendRow(QStandardItem(k + " x " + str(v)))
print(k + " x " + str(v))
else:
dic = {}
for b in self.strike_target_infos.buildings:
id = b.dcs_identifier
if b.is_dead:
id = id + "[Destroyed]"
if id in dic.keys():
dic[id] = dic[id] + 1
else:
dic[id] = 1
for k, v in dic.items():
model.appendRow(QStandardItem(k + " x " + str(v)))
print(k + " x " + str(v))
self.listView.setModel(model)

View File

@@ -161,6 +161,14 @@ class SquadronLiverySelector(QComboBox):
valid_livery = livery.countries is None or cc in livery.countries
if valid_livery or cc in ["BLUE", "RED"]:
liveries.add(livery)
faction = squadron.coalition.faction
overrides = [
x
for x in faction.liveries_overrides.get(self.aircraft_type, [])
if x in [y.id.lower() for y in liveries]
]
if len(overrides) > 0:
self.addItem("Use livery overrides", userData=None)
for livery in sorted(liveries):
self.addItem(livery.name, userData=livery.id)
if selected_livery is not None:

View File

@@ -8,7 +8,9 @@ from PySide2.QtWidgets import (
QGroupBox,
QLabel,
QPushButton,
QScrollArea,
QVBoxLayout,
QWidget,
)
from game.debriefing import Debriefing
@@ -52,6 +54,19 @@ class LossGrid(QGridLayout):
self.addWidget(QLabel(str(count)), row, 1)
class ScrollingCasualtyReportContainer(QGroupBox):
def __init__(self, debriefing: Debriefing, player: bool) -> None:
country = debriefing.player_country if player else debriefing.enemy_country
super().__init__(f"{country}'s lost units:")
scroll_content = QWidget()
scroll_content.setLayout(LossGrid(debriefing, player))
scroll_area = QScrollArea()
scroll_area.setWidget(scroll_content)
layout = QVBoxLayout()
layout.addWidget(scroll_area)
self.setLayout(layout)
class QDebriefingWindow(QDialog):
def __init__(self, debriefing: Debriefing):
super(QDebriefingWindow, self).__init__()
@@ -75,12 +90,10 @@ class QDebriefingWindow(QDialog):
title = QLabel("<b>Casualty report</b>")
layout.addWidget(title)
player_lost_units = QGroupBox(f"{self.debriefing.player_country}'s lost units:")
player_lost_units.setLayout(LossGrid(debriefing, player=True))
player_lost_units = ScrollingCasualtyReportContainer(debriefing, player=True)
layout.addWidget(player_lost_units)
enemy_lost_units = QGroupBox(f"{self.debriefing.enemy_country}'s lost units:")
enemy_lost_units.setLayout(LossGrid(debriefing, player=False))
enemy_lost_units = ScrollingCasualtyReportContainer(debriefing, player=False)
layout.addWidget(enemy_lost_units)
okay = QPushButton("Okay")

View File

@@ -19,7 +19,7 @@ from PySide2.QtWidgets import (
)
import qt_ui.uiconstants as CONST
from game import Game, VERSION, persistency
from game import Game, VERSION, persistency, Migrator
from game.debriefing import Debriefing
from game.game import TurnState
from game.layout import LAYOUTS
@@ -31,6 +31,7 @@ from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.simcontroller import SimController
from qt_ui.uiconstants import URLS
from qt_ui.uiflags import UiFlags
from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler
from qt_ui.widgets.QTopPanel import QTopPanel
from qt_ui.widgets.ato import QAirTaskingOrderPanel
@@ -55,7 +56,7 @@ class QLiberationWindow(QMainWindow):
tgo_info_signal = Signal(TheaterGroundObject)
control_point_info_signal = Signal(ControlPoint)
def __init__(self, game: Game | None, dev: bool) -> None:
def __init__(self, game: Game | None, ui_flags: UiFlags) -> None:
super().__init__()
self._uncaught_exception_handler = UncaughtExceptionHandler(self)
@@ -80,14 +81,16 @@ class QLiberationWindow(QMainWindow):
Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = QInfoPanel(self.game)
self.liberation_map = QLiberationMap(self.game_model, dev, self)
self.liberation_map = QLiberationMap(
self.game_model, ui_flags.dev_ui_webserver, self
)
self.setGeometry(300, 100, 270, 100)
self.updateWindowTitle()
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage("Ready")
self.initUi()
self.initUi(ui_flags)
self.initActions()
self.initToolbar()
self.initMenuBar()
@@ -108,6 +111,7 @@ class QLiberationWindow(QMainWindow):
try:
logging.info("Loading last saved game : " + str(last_save_file))
game = persistency.load_game(last_save_file)
Migrator(game)
self.onGameGenerated(game)
self.updateWindowTitle(last_save_file if game else None)
except:
@@ -117,7 +121,7 @@ class QLiberationWindow(QMainWindow):
else:
self.onGameGenerated(self.game)
def initUi(self):
def initUi(self, ui_flags: UiFlags) -> None:
hbox = QSplitter(Qt.Horizontal)
vbox = QSplitter(Qt.Vertical)
hbox.addWidget(self.ato_panel)
@@ -130,7 +134,7 @@ class QLiberationWindow(QMainWindow):
hbox.setSizes([1, 10000000])
vbox.setSizes([600, 100])
self.top_panel = QTopPanel(self.game_model, self.sim_controller)
self.top_panel = QTopPanel(self.game_model, self.sim_controller, ui_flags)
vbox = QVBoxLayout()
vbox.setMargin(0)
vbox.addWidget(self.top_panel)
@@ -317,6 +321,7 @@ class QLiberationWindow(QMainWindow):
)
if file is not None and file[0] != "":
game = persistency.load_game(file[0])
Migrator(game)
GameUpdateSignal.get_instance().game_loaded.emit(game)
self.updateWindowTitle(file[0])
@@ -482,6 +487,10 @@ class QLiberationWindow(QMainWindow):
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
"<b>Walder </b> <i>for the Skynet-IADS script</i><br/>"
"<b>Anubis Yinepu </b> <i>for the Hercules Cargo script</i><br/>"
'<a href="https://www.flaticon.com/free-icons/bug" title="bug icons" style="color: #ffffff">Bug icons created by Freepik - Flaticon</a><br />'
'Contains information from <a href="https://osmdata.openstreetmap.de/" style="color: #ffffff">OpenStreetMap © OpenStreetMap contributors</a>, which is made available here under the <a href="https://opendatacommons.org/licenses/odbl/1-0/" style="color: #ffffff">Open Database License (ODbL)</a>.<br />'
'<a href="https://download.geofabrik.de/index.html/" style="color: #ffffff">OpenStreetMap Data Extracts from Geofabrik</a><br />'
'<a href="https://www.earthdata.nasa.gov/" style="color: #ffffff">NASA EarthData</a><br />'
+ "<h4>Splash Screen :</h4>"
+ "Artwork by Andriy Dankovych (CC BY-SA)"
" <a href='https://www.facebook.com/AndriyDankovych' style='color:white'>"
@@ -546,10 +555,13 @@ class QLiberationWindow(QMainWindow):
result = QMessageBox.question(
self,
"Quit Retribution?",
"Are you sure you want to quit? All unsaved progress will be lost.",
QMessageBox.Yes | QMessageBox.No,
"Would you like to save before quitting?",
QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel,
QMessageBox.Cancel,
)
if result == QMessageBox.Yes:
if result in [QMessageBox.Yes, QMessageBox.No]:
if result == QMessageBox.Yes:
self.saveGame()
self._save_window_geometry()
super().closeEvent(event)
self.dialog = None

View File

@@ -238,5 +238,5 @@ class QWaitingForMissionResultWindow(QDialog):
# Stop the current waiting thread as we manually submit the results
self.wait_thread.stop()
self.on_debriefing_update(
self.sim_controller.debrief_current_state(Path(file[0], force_end=True))
self.sim_controller.debrief_current_state(Path(file[0]), force_end=True)
)

View File

@@ -13,7 +13,7 @@ from PySide2.QtWidgets import (
from game import Game
from game.ato.flight import Flight
from game.ato.flightplans.custom import CustomFlightPlan, CustomLayout
from game.ato.flightplans.custom import CustomFlightPlan
from game.ato.flightplans.formationattack import FormationAttackFlightPlan
from game.ato.flightplans.planningerror import PlanningError
from game.ato.flightplans.waypointbuilder import WaypointBuilder
@@ -147,15 +147,7 @@ class QFlightWaypointTab(QFrame):
def degrade_to_custom_flight_plan(self) -> None:
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
self.flight.flight_plan = CustomFlightPlan(
self.flight,
CustomLayout(
departure=WaypointBuilder(self.flight, self.coalition).takeoff(
self.flight.departure
),
custom_waypoints=self.flight.flight_plan.waypoints[1:],
),
)
self.flight.degrade_to_custom_flight_plan()
def confirm_recreate(self, task: FlightType) -> None:
result = QMessageBox.question(

View File

@@ -6,7 +6,7 @@ from typing import List
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QDate, QItemSelectionModel, QPoint, Qt, Signal
from PySide2.QtWidgets import QCheckBox, QLabel, QTextEdit, QVBoxLayout
from PySide2.QtWidgets import QCheckBox, QLabel, QTextEdit, QVBoxLayout, QTextBrowser
from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.campaignloader.campaign import Campaign, DEFAULT_BUDGET
@@ -142,6 +142,7 @@ class NewGameWizard(QtWidgets.QWizard):
),
automate_aircraft_reinforcements=self.field("automate_aircraft_purchases"),
supercarrier=self.field("supercarrier"),
max_frontline_length=campaign.data.get("max_frontline_length", 80),
)
generator_settings = GeneratorSettings(
start_date=start_date,
@@ -162,6 +163,7 @@ class NewGameWizard(QtWidgets.QWizard):
a6a_intruder=self.field("a6a_intruder"),
f4b_phantom=self.field("f4b_phantom"),
f15d_baz=self.field("f15d_baz"),
f_16_idf=self.field("f_16_idf"),
f22_raptor=self.field("f22_raptor"),
f84g_thunderjet=self.field("f84g_thunderjet"),
f100_supersabre=self.field("f100_supersabre"),
@@ -172,6 +174,7 @@ class NewGameWizard(QtWidgets.QWizard):
jas39_gripen=self.field("jas39_gripen"),
su30_flanker_h=self.field("su30_flanker_h"),
su57_felon=self.field("su57_felon"),
ov10a_bronco=self.field("ov10a_bronco"),
frenchpack=self.field("frenchpack"),
high_digit_sams=self.field("high_digit_sams"),
)
@@ -257,11 +260,13 @@ class FactionSelection(QtWidgets.QWizardPage):
redFaction.setBuddy(self.redFactionSelect)
# Faction description
self.blueFactionDescription = QTextEdit("")
self.blueFactionDescription = QTextBrowser()
self.blueFactionDescription.setReadOnly(True)
self.blueFactionDescription.setOpenExternalLinks(True)
self.redFactionDescription = QTextEdit("")
self.redFactionDescription = QTextBrowser()
self.redFactionDescription.setReadOnly(True)
self.redFactionDescription.setOpenExternalLinks(True)
# Setup default selected factions
for i, r in enumerate(FACTIONS):
@@ -383,8 +388,9 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
self.registerField("selectedCampaign", self.campaignList)
# Faction description
self.campaignMapDescription = QTextEdit("")
self.campaignMapDescription = QTextBrowser()
self.campaignMapDescription.setReadOnly(True)
self.campaignMapDescription.setOpenExternalLinks(True)
self.campaignMapDescription.setMaximumHeight(200)
self.performanceText = QTextEdit("")
@@ -668,6 +674,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("f4b_phantom", f4b_phantom)
f15d_baz = QtWidgets.QCheckBox()
self.registerField("f15d_baz", f15d_baz)
f_16_idf = QtWidgets.QCheckBox()
self.registerField("f_16_idf", f_16_idf)
f22_raptor = QtWidgets.QCheckBox()
self.registerField("f22_raptor", f22_raptor)
f84g_thunderjet = QtWidgets.QCheckBox()
@@ -684,6 +692,8 @@ class GeneratorOptions(QtWidgets.QWizardPage):
self.registerField("su30_flanker_h", su30_flanker_h)
su57_felon = QtWidgets.QCheckBox()
self.registerField("su57_felon", su57_felon)
ov10a_bronco = QtWidgets.QCheckBox()
self.registerField("ov10a_bronco", ov10a_bronco)
frenchpack = QtWidgets.QCheckBox()
self.registerField("frenchpack", frenchpack)
high_digit_sams = QtWidgets.QCheckBox()
@@ -696,93 +706,41 @@ class GeneratorOptions(QtWidgets.QWizardPage):
modLayout = QtWidgets.QGridLayout()
modLayout_row = 1
modLayout.addWidget(
QtWidgets.QLabel("A-4E Skyhawk (version 2.0.0)"), modLayout_row, 0
)
modLayout.addWidget(a4_skyhawk, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("A-6A Intruder (v2.7.5.01)"), modLayout_row, 0
)
modLayout.addWidget(a6a_intruder, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("F-4B Phantom II (v2.7.10.02 EFM + Patch 2022.10.02)"),
modLayout_row,
0,
)
modLayout.addWidget(f4b_phantom, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(QtWidgets.QLabel("F-15D Baz (v1.0)"), modLayout_row, 0)
modLayout.addWidget(f15d_baz, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(QtWidgets.QLabel("F-22A Raptor"), modLayout_row, 0)
modLayout.addWidget(f22_raptor, modLayout_row, 1)
modLayout_row += 1
# Section break here for readability
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, 0)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("F-84G Thunderjet (version 2.5.7.01)"),
modLayout_row,
0,
)
modLayout.addWidget(f84g_thunderjet, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("F-100 Super Sabre (version 2.7.18.30765 patch 09.10.22)"),
modLayout_row,
0,
)
modLayout.addWidget(f100_supersabre, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("F-104 Starfighter (version 2.7.11.222.01)"),
modLayout_row,
0,
)
modLayout.addWidget(f104_starfighter, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("F-105 Thunderchief (version 2.7.12.23x)"),
modLayout_row,
0,
)
modLayout.addWidget(f105_thunderchief, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("C-130J-30 Super Hercules"), modLayout_row, 0
)
modLayout.addWidget(hercules, modLayout_row, 1)
modLayout_row += 1
# Section break here for readability
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, 0)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("UH-60L Black Hawk (version 1.3.1)"), modLayout_row, 0
)
modLayout.addWidget(uh_60l, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("JAS 39 Gripen (version v1.8.0-beta)"), modLayout_row, 0
)
modLayout.addWidget(jas39_gripen, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(
QtWidgets.QLabel("Su-30 Flanker-H (V2.01B)"), modLayout_row, 0
)
modLayout.addWidget(su30_flanker_h, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(QtWidgets.QLabel("Su-57 Felon"), modLayout_row, 0)
modLayout.addWidget(su57_felon, modLayout_row, 1)
modLayout_row += 1
modLayout.addWidget(QtWidgets.QLabel("Frenchpack"), modLayout_row, 0)
modLayout.addWidget(frenchpack, modLayout_row, 1)
# new column starts here
modLayout_row = 1
modLayout.addWidget(QtWidgets.QLabel("High Digit SAMs"), modLayout_row, 2)
modLayout.addWidget(high_digit_sams, modLayout_row, 3)
mod_pairs = [
("A-4E Skyhawk (v2.1.0)", a4_skyhawk),
("A-6A Intruder (v2.7.5.01)", a6a_intruder),
("C-130J-30 Super Hercules", hercules),
("F-4B Phantom II (v2.7.10.02 EFM + Patch 2022.10.02)", f4b_phantom),
("F-15D Baz (v1.0)", f15d_baz),
("F-16I Sufa & F-16D (v2.2 by IDF Mods Project)", f_16_idf),
("F-22A Raptor", f22_raptor),
("F-84G Thunderjet (v2.5.7.01)", f84g_thunderjet),
("F-100 Super Sabre (v2.7.18.30765 patch 20.10.22)", f100_supersabre),
("F-104 Starfighter (v2.7.11.222.01)", f104_starfighter),
("F-105 Thunderchief (v2.7.12.23x)", f105_thunderchief),
("Frenchpack", frenchpack),
("High Digit SAMs", high_digit_sams),
("JAS 39 Gripen (v1.8.0-beta)", jas39_gripen),
("OV-10A Bronco", ov10a_bronco),
("Su-30 Flanker-H (V2.01B)", su30_flanker_h),
("Su-57 Felon", su57_felon),
("UH-60L Black Hawk (v1.3.1)", uh_60l),
]
for i in range(len(mod_pairs)):
if i % 15 == 0:
modLayout_row = 1
col = 2 * (i // 15)
if i % 5 == 0:
# Section break here for readability
modLayout.addWidget(QtWidgets.QWidget(), modLayout_row, col)
modLayout_row += 1
label, cb = mod_pairs[i]
modLayout.addWidget(QLabel(label), modLayout_row, col)
modLayout.addWidget(cb, modLayout_row, col + 1)
modLayout_row += 1
modSettingsGroup.setLayout(modLayout)
mlayout = QVBoxLayout()

View File

@@ -4,37 +4,36 @@ asgiref==3.5.2
atomicwrites==1.4.1
attrs==22.1.0
black==22.10.0
certifi==2022.9.24
certifi==2022.12.7
cfgv==3.3.1
click==8.1.3
colorama==0.4.6
distlib==0.3.6
Faker==15.1.1
fastapi==0.85.1
Faker==15.3.3
fastapi==0.87.0
filelock==3.8.0
future==0.18.2
h11==0.14.0
httptools==0.5.0
identify==2.5.7
identify==2.5.9
idna==3.4
iniconfig==1.1.1
Jinja2==3.1.2
MarkupSafe==2.1.1
mypy==0.982
mypy==0.991
mypy-extensions==0.4.3
nodeenv==1.7.0
packaging==21.3
pathspec==0.10.1
pathspec==0.10.2
pefile==2022.5.30
Pillow==9.2.0
platformdirs==2.5.2
Pillow==9.3.0
platformdirs==2.5.4
pluggy==1.0.0
pre-commit==2.20.0
py==1.11.0
pydantic==1.10.2
-e git+https://github.com/pydcs/dcs@16ea5d01c57a395ea5868630615c8a42dd2ea1a9#egg=pydcs
pyinstaller==5.6.1
pyinstaller-hooks-contrib==2022.10
-e git+https://github.com/dcs-retribution/pydcs@f0bb4da4d458d714dedd1f3bba30deae5f25a80d#egg=pydcs
pyinstaller==5.6.2
pyinstaller-hooks-contrib==2022.13
pyparsing==3.0.9
pyproj==3.4.0
pyshp==2.3.1
@@ -48,18 +47,18 @@ Shapely==1.8.5.post1
shiboken2==5.15.2.1
six==1.16.0
sniffio==1.3.0
starlette==0.20.4
starlette==0.21.0
tabulate==0.9.0
text-unidecode==1.3
toml==0.10.2
tomli==2.0.1
types-Jinja2==2.11.9
types-MarkupSafe==1.1.10
types-Pillow==9.2.2.2
types-PyYAML==6.0.12
types-Pillow==9.3.0.2
types-PyYAML==6.0.12.2
types-tabulate==0.9.0.0
typing_extensions==4.4.0
uvicorn==0.19.0
virtualenv==20.16.5
uvicorn==0.20.0
virtualenv==20.16.7
watchgod==0.8.2
websockets==10.3
websockets==10.4

View File

@@ -1,6 +1,6 @@
{# TODO: DO NOT REMOVE NEWLINES, THEY ARE PART OF THE FORMATTING! #}
DCS Retribution Turn {{ game.turn }}
====================
Most briefing information, including communications and flight plan information, can be found on your kneeboard.
Current situation:
@@ -62,17 +62,20 @@ On this location, our ground forces have been ordered to hold still, and defend
{%+ endfor %}{% endif %}
{%- if player_flights|length > 0 %}
Your flights:
====================
{% for flight in flights if flight.client_units %}
{% for flight in player_flights %}
--------------------------------------------------
{{ flight.flight_type }} {{ flight.units[0].type }} x {{ flight.size }}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
Freq : {{ flight|intra_flight_channel }}
{% for waypoint in flight.waypoints %}
{{ loop.index0 }} {{waypoint|waypoint_timing("Depart ")}}-- {{waypoint.name}} : {{ waypoint.description}}
{% endfor %}
--------------------------------------------------{% endfor %}
--------------------------------------------------
{% endfor %}
{% endif %}
{%- if allied_flights_by_departure|length > 0 %}
Planned ally flights:
@@ -83,6 +86,7 @@ Planned ally flights:
{% for flight in allied_flights_by_departure[dep] %}
{{ flight.flight_type }} {{ flight.units[0].type }} x {{flight.size}}, departing in {{ flight.departure_delay }}, {{ flight.package.target.name}}
{% endfor %}
{% endfor %}
{% endif %}
@@ -98,12 +102,22 @@ TACAN : {{ runway.tacan }} {{ runway.tacan_callsign }}
{% endif %}
{% endfor %}
{% endif %}
{%- if awacs|length > 0 %}
AWACS:
====================
{% for i in awacs %}{{ i.callsign }} -- Freq : {{i.freq.mhz}}
{% endfor %}
{% endif %}
{%- if tankers|length > 0 %}
TANKERS:
====================
{% for i in tankers %}{{ i.callsign }} -- Freq : {{i.freq.mhz}}, TACAN : {{ i.tacan }}
{% endfor %}
{% endif %}
{%- if jtacs|length > 0 %}
@@ -111,4 +125,5 @@ JTACS [F-10 Menu] :
====================
{% for jtac in jtacs %}Frontline {{ jtac.region }} -- Code : {{ jtac.code }}, Freq : {{ jtac.freq.mhz }}
{% endfor %}
{% endif %}

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