Compare commits

..

7 Commits

Author SHA1 Message Date
Dan Albert
2bdf9d3423 Black the pydcs extensions directory. 2021-04-11 13:55:29 -07:00
Dan Albert
0cd359f36b Black the resoureces directory. 2021-04-11 13:54:55 -07:00
Dan Albert
39bd6e3c10 Avoid warning in GitHub workflow.
(cherry picked from commit 4ec8994c38)
2021-04-11 13:37:16 -07:00
Dan Albert
ed89c49fd4 Add black workflow.
(cherry picked from commit 16b0dcad71)
2021-04-11 13:31:10 -07:00
Dan Albert
4000b42df2 Add pre-commit configuration for black.
To set up, run `pre-commit install`.

(cherry picked from commit 9c1265d50d)
2021-04-11 13:31:09 -07:00
Dan Albert
f73a68aeca Note the font crash fix in the changelog.
(cherry picked from commit cce736bc16)
2021-04-11 13:29:30 -07:00
Hanninho
7f8dae003f Force the basic layout engine when generating the kneeboard.
The libraqm backed layout engine causes crashes on some machines.

Fixes #531.

(cherry picked from commit 2a1127e637)
2021-04-11 13:25:32 -07:00
602 changed files with 12080 additions and 20097 deletions

View File

@@ -1,2 +0,0 @@
# Black
a47bef1f1336fd264d0b175f4421758339a30acb

View File

@@ -9,8 +9,6 @@ assignees: ''
Before filing, please search the issue tracker to see if the issue has already been reported. Before filing, please search the issue tracker to see if the issue has already been reported.
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
**Describe the bug** **Describe the bug**
A clear and concise description of what the bug is. A clear and concise description of what the bug is.
@@ -30,8 +28,7 @@ We will usually need more information for debugging. Include as much of the foll
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`). - DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`). - The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`). - A tacview track file, especially when demonstrating an issue with AI behavior. By default these are locaed in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
**Version information (please complete the following information):** **Version information (please complete the following information):**
- DCS Liberation [e.g. 2.3.1]: - DCS Liberation [e.g. 2.3.1]:

View File

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

View File

@@ -9,8 +9,6 @@ assignees: ''
Before filing, please search the issue tracker to see if this feature has already been requested. 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.** **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 [...] A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]

View File

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

View File

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

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

@@ -1,117 +1,9 @@
# 3.1.0 # 2.4.4
## Features/Improvements
## Fixes ## Fixes
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
* **[UI]** Made non-interactive map elements less obstructive.
# 3.0.0
Saves from 2.5 are not compatible with 3.0.
## Features/Improvements
* **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information.
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
* **[Campaign]** Non-control point FOBs will no longer spawn.
* **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
* **[Campaign]** Capturing a base now depopulates all of its attached objectives with units: air defenses, EWRs, ships, armor groups, etc. Buildings are captured.
* **[Campaign]** Ammunition Depots determine how many ground units can be deployed on the frontline by a control point.
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
* **[Campaign AI]** Auto purchase will aim to purchase enough ground units to support the frontline, plus 30% reserve units.
* **[Campaign AI]** Auto purchase will now adjust its air/ground balance to favor whichever is under-funded.
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
* **[Flight Planner]** Flight plans now include bullseye waypoints.
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
* **[Flight Planner]** Planned airspeed increased to 0.85 mach for supersonic airframes and 85% of max speed for subsonic.
* **[Flight Planner]** Taxi time estimation for airfields increased from 5 minutes to 8 minutes.
* **[Flight Planner]** Reduce expected error margin for flight plans from 10% to 5%.
* **[Flight Planner]** SEAD flights are scheduled one minute ahead of the package's TOT so that they can suppress the site ahead of the strike.
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map.
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
* **[UI]** Added global aircraft inventory view under Air Wing dialog.
* **[UI]** Base menu now shows information about ground unit deployment limits.
* **[Modding]** Campaigns now choose locations for factories to spawn.
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
* **[Modding]** Campaigns now use map structures as strike targets.
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance. Support for random objective generation was removed.
* **[Modding]** Campaigns may now place AAA objectives.
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
* **[Skynet]** Updated to 2.1.0.
## Fixes
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
* **[Campaign AI]** Fixed bug causing AI to over-purchase cheap aircraft.
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
* **[Campaign]** EWR sites are now purchasable.
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
* **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites).
* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game.
# 2.5.1
## Features/Improvements
* **[UI]** Engagement ranges are now displayed by default.
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
## Fixes
* **[Campaigns]** EWRs associated with a base will now only be generated near the base.
* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines.
# 2.5.0
Saves from 2.4 are not compatible with 2.5.
## Features/Improvements
* **[Engine]** DCS 2.7 Support
* **[UI]** Improved FOB menu, added a custom banner, and do not display aircraft recruitment menu
* **[Flight Planner]** Added AEW&C missions. (by siKruger)
* **[Kneeboard]** Added dark kneeboard option (by GvonH)
* **[Campaigns]** Multiple EWR sites may now be generated, and EWR sites may be generated outside bases (by SnappyComebacks)
* **[Mission Generation]** Cloudy and rainy (but not thunderstorm) weather will use the cloud presets from DCS 2.7.
* **[Plugins]** Added LotATC export plugin (by drsoran)
* **[Plugins]** Added Splash Damage Plugin (by Wheelijoe)
* **[Loadouts]** Replaced Litening with ATFLIR for all default F/A-18C loadouts.
## Fixes
* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical.
* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups.
* **[Flight Planner]** Mig-19 was not allowed for CAS roles fixed
* **[Flight Planner]** Increased size of navigation planning area to avoid plannign failures with distant waypoints.
* **[Flight Planner]** Fixed UI refresh when unchecking the "default loadout" box in the loadout editor.
* **[Objective names]** Fixed typos in objective name : ARMADILLLO -> ARMADILLO (by SnappyComebacks)
* **[Payloads]** F-86 Sabre was missing a custom payload
* **[Payloads]** Added GAR-8 period restrictions (by Mustang-25)
* **[Campaign]** Date now progresses.
* **[Campaign]** Added game over message when a coalition runs out of functioning airbases.
* **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines. * **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines.
## Regressions
* **[Mod Support]** Stopped support for 2.5.5 Rafale Mode, and removed factions that were using it
* **[Mod Support]** Su-57 mod support might be out of date
# 2.4.3 # 2.4.3
## Features/Improvements ## Features/Improvements
@@ -122,6 +14,7 @@ Saves from 2.4 are not compatible with 2.5.
* **[Mods]** Updated C-130J mod data to version 6.4 * **[Mods]** Updated C-130J mod data to version 6.4
* **[Mods]** Updated F-22A mod to latest version * **[Mods]** Updated F-22A mod to latest version
* **[Payload]** Mirage-2000C : Added Eclair counter measures pod to all default loadouts
# 2.4.2 # 2.4.2

View File

View File

@@ -2,21 +2,20 @@ from dcs.vehicles import AirDefence
AAA_UNITS = [ AAA_UNITS = [
AirDefence.SPAAA_Gepard, AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.SPAAA_Vulcan_M163, AirDefence.AAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed_Emplacement, AirDefence.AAA_ZU_23_Closed,
AirDefence.AAA_ZU_23_Emplacement, AirDefence.AAA_ZU_23_Emplacement,
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375, AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Closed_Emplacement, AirDefence.AAA_ZU_23_Insurgent_Closed,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375, AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Emplacement, AirDefence.AAA_ZU_23_Insurgent,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_Flak_38_20mm, AirDefence.AAA_Flak_38,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_Vierling_38,
AirDefence.AAA_SP_Kdo_G_40, AirDefence.AAA_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm, AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
] ]

View File

@@ -1,40 +0,0 @@
from dcs.unit import Unit
from dcs.vehicles import AirDefence
class AlicCodes:
CODES = {
AirDefence.EWR_1L13.id: 101,
AirDefence.EWR_55G6.id: 102,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR.id: 103,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR.id: 104,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR.id: 107,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR.id: 108,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR.id: 109,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR.id: 110,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL.id: 115,
AirDefence.SAM_SA_8_Osa_Gecko_TEL.id: 117,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL.id: 118,
AirDefence.SAM_SA_15_Tor_Gauntlet.id: 119,
AirDefence.SAM_SA_19_Tunguska_Grison.id: 120,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id: 121,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3.id: 122,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR.id: 123,
AirDefence.SAM_Rapier_Blindfire_TR.id: 124,
AirDefence.SAM_Rapier_LN.id: 125,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR.id: 126,
AirDefence.HQ_7_Self_Propelled_LN.id: 127,
AirDefence.HQ_7_Self_Propelled_STR.id: 128,
AirDefence.SAM_Roland_ADS.id: 201,
AirDefence.SAM_Patriot_STR.id: 202,
AirDefence.SAM_Hawk_SR__AN_MPQ_50.id: 203,
AirDefence.SAM_Hawk_TR__AN_MPQ_46.id: 204,
AirDefence.SAM_Roland_EWR.id: 205,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55.id: 206,
AirDefence.SPAAA_Gepard.id: 207,
AirDefence.SPAAA_Vulcan_M163.id: 208,
}
@classmethod
def code_for(cls, unit: Unit) -> int:
return cls.CODES[unit.type]

View File

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

View File

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

View File

@@ -1,239 +0,0 @@
from enum import unique, Enum
from typing import Type
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
from dcs.unittype import VehicleType
from pydcs_extensions.frenchpack import frenchpack
@unique
class GroundUnitClass(Enum):
Tank = (
"Tank",
(
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_72B3,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2A4,
Armor.MBT_Leopard_2A4_Trs,
Armor.MBT_Leopard_2A5,
Armor.MBT_Leopard_2A6M,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_Chieftain_Mk_3,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_IV,
Armor.ZTZ_96B,
# WW2
# Axis
Armor.Tk_PzIV_H,
Armor.SPG_Sturmpanzer_IV_Brummbar,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
# Allies
Armor.Tk_M4_Sherman,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
),
)
Atgm = (
"ATGM",
(
Armor.ATGM_HMMWV,
Armor.ATGM_VAB_Mephisto,
Armor.ATGM_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
# Axxis
Armor.SPG_StuG_III_Ausf__G,
Armor.SPG_StuG_IV,
Armor.SPG_Jagdpanzer_IV,
Armor.SPG_Jagdpanther_G1,
Armor.SPG_Sd_Kfz_184_Elefant,
# Allies
Armor.SPG_M10_GMC,
Armor.MT_M4A4_Sherman_Firefly,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
),
)
Ifv = (
"IFV",
(
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_Warrior,
Armor.SPG_Stryker_MGS,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# Mods
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13,
),
)
Apc = (
"APC",
(
Armor.IFV_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.IFV_BTR_82A,
Armor.APC_MTLB,
Armor.APC_AAV_7_Amphibious,
Armor.APC_TPz_Fuchs,
Armor.APC_BTR_RD,
# WW2
Armor.APC_M2A1_Halftrack,
Armor.APC_Sd_Kfz_251_Halftrack,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
),
)
Artillery = (
"Artillery",
(
Artillery.Grad_MRL_FDDM__FC,
Artillery.MLRS_9A52_Smerch_HE_300mm,
Artillery.SPH_2S1_Gvozdika_122mm,
Artillery.SPH_2S3_Akatsia_152mm,
Artillery.MLRS_BM_21_Grad_122mm,
Artillery.MLRS_9K57_Uragan_BM_27_220mm,
Artillery.SPH_M109_Paladin_155mm,
Artillery.MLRS_M270_227mm,
Artillery.SPM_2S9_Nona_120mm_M,
Artillery.SPH_Dana_vz77_152mm,
Artillery.SPH_T155_Firtina_155mm,
Artillery.PLZ_05,
Artillery.SPH_2S19_Msta_152mm,
Artillery.MLRS_9A52_Smerch_CM_300mm,
# WW2
Artillery.SPG_M12_GMC_155mm,
),
)
Logistics = (
"Logistics",
(
Unarmed.Carrier_M30_Cargo,
Unarmed.Truck_M818_6x6,
Unarmed.Truck_KAMAZ_43101,
Unarmed.Truck_Ural_375,
Unarmed.Truck_GAZ_66,
Unarmed.Truck_GAZ_3307,
Unarmed.Truck_GAZ_3308,
Unarmed.Truck_Ural_4320_31_Arm_d,
Unarmed.Truck_Ural_4320T,
Unarmed.Truck_Opel_Blitz,
Unarmed.LUV_Kubelwagen_82,
Unarmed.Carrier_Sd_Kfz_7_Tractor,
Unarmed.LUV_Kettenrad,
Unarmed.Car_Willys_Jeep,
Unarmed.LUV_Land_Rover_109,
Unarmed.Truck_Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
),
)
Recon = (
"Recon",
(
Armor.Scout_HMMWV,
Armor.Scout_Cobra,
Armor.LT_PT_76,
Armor.IFV_LAV_25,
Armor.Scout_BRDM_2,
# WW2
Armor.LT_Mk_VII_Tetrarch,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.Car_M8_Greyhound_Armored,
Armor.Car_Daimler_Armored,
# Mods
frenchpack.ERC_90,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
),
)
Infantry = (
"Infantry",
(
Infantry.Insurgent_AK_74,
Infantry.Infantry_AK_74,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_M4_Georgia,
Infantry.Infantry_AK_74_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Infantry_M249,
Infantry.Infantry_M4,
Infantry.Infantry_RPG,
),
)
Shorads = (
"SHORADS",
(
AirDefence.SPAAA_ZU_23_2_Mounted_Ural_375,
AirDefence.SPAAA_ZU_23_2_Insurgent_Mounted_Ural_375,
AirDefence.SPAAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
AirDefence.SAM_SA_13_Strela_10M3_Gopher_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163,
AirDefence.SAM_Linebacker___Bradley_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger__Stinger,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_S_60_57mm,
AirDefence.AAA_M1_37mm,
AirDefence.AAA_QF_3_7,
),
)
def __init__(
self, class_name: str, unit_list: tuple[Type[VehicleType], ...]
) -> None:
self.class_name = class_name
self.unit_list = unit_list
def __contains__(self, unit_type: Type[VehicleType]) -> bool:
return unit_type in self.unit_list

View File

@@ -1,6 +1,6 @@
from dcs.ships import ( from dcs.ships import (
Battlecruiser_1144_2_Pyotr_Velikiy, CGN_1144_2_Pyotr_Velikiy,
Cruiser_1164_Moskva, CG_1164_Moskva,
CVN_70_Carl_Vinson, CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt, CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln, CVN_72_Abraham_Lincoln,
@@ -8,101 +8,65 @@ from dcs.ships import (
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov, CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5_Admiral_Kuznetsov_2017,
Frigate_11540_Neustrashimy, FFG_11540_Neustrashimy,
Corvette_1124_4_Grisha, FFL_1124_4_Grisha,
Frigate_1135M_Rezky, FF_1135M_Rezky,
Corvette_1241_1_Molniya, FSG_1241_1MP_Molniya,
LHA_1_Tarawa, LHA_1_Tarawa,
FFG_Oliver_Hazzard_Perry, Oliver_Hazzard_Perry_class,
CG_Ticonderoga, Ticonderoga_class,
Type_052B_Destroyer, Type_052B_Destroyer,
Type_052C_Destroyer, Type_052C_Destroyer,
Type_054A_Frigate, Type_054A_Frigate,
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
) )
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
TELARS = { UNITS_WITH_RADAR = [
AirDefence.SAM_SA_19_Tunguska_Grison,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL,
AirDefence.SAM_SA_8_Osa_Gecko_TEL,
AirDefence.SAM_SA_15_Tor_Gauntlet,
AirDefence.SAM_Roland_ADS,
}
TRACK_RADARS = {
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Patriot_STR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_STR,
}
LAUNCHER_TRACKER_PAIRS = {
AirDefence.SAM_SA_6_Kub_Gainful_TEL: AirDefence.SAM_SA_6_Kub_Straight_Flush_STR,
AirDefence.SAM_SA_3_S_125_Goa_LN: AirDefence.SAM_SA_3_S_125_Low_Blow_TR,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C: AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR,
AirDefence.SAM_Hawk_LN_M192: AirDefence.SAM_Hawk_TR__AN_MPQ_46,
AirDefence.SAM_Patriot_LN: AirDefence.SAM_Patriot_STR,
AirDefence.SAM_SA_2_S_75_Guideline_LN: AirDefence.SAM_SA_2_S_75_Fan_Song_TR,
AirDefence.SAM_Rapier_LN: AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_LN: AirDefence.HQ_7_Self_Propelled_STR,
}
UNITS_WITH_RADAR = {
# Radars # Radars
AirDefence.SAM_SA_19_Tunguska_Grison, AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_11_Buk_Gadfly_Fire_Dome_TEL, AirDefence.SAM_SA_11_Buk_CC_9S470M1,
AirDefence.SAM_SA_8_Osa_Gecko_TEL, AirDefence.SAM_Patriot_AMG_AN_MRC_137,
AirDefence.SAM_SA_15_Tor_Gauntlet, AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
AirDefence.SPAAA_Gepard, AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_Vulcan_M163, AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Roland_ADS, AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
AirDefence.EWR_1L13, AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_Straight_Flush_STR, AirDefence.SAM_SA_6_Kub_STR_9S91,
AirDefence.SAM_SA_10_S_300_Grumble_Flap_Lid_TR, AirDefence.SAM_SA_10_S_300PS_TR_30N6,
AirDefence.SAM_SA_10_S_300_Grumble_Clam_Shell_SR, AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
AirDefence.EWR_55G6, AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300_Grumble_Big_Bird_SR, AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
AirDefence.SAM_SA_11_Buk_Gadfly_Snow_Drift_SR, AirDefence.SAM_SA_11_Buk_SR_9S18M1,
AirDefence.MCC_SR_Sborka_Dog_Ear_SR, AirDefence.CP_9S80M1_Sborka,
AirDefence.SAM_Hawk_TR__AN_MPQ_46, AirDefence.SAM_Hawk_TR_AN_MPQ_46,
AirDefence.SAM_Hawk_SR__AN_MPQ_50, AirDefence.SAM_Hawk_SR_AN_MPQ_50,
AirDefence.SAM_Patriot_STR, AirDefence.SAM_Patriot_STR_AN_MPQ_53,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55, AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_P19_Flat_Face_SR__SA_2_3, AirDefence.SAM_SR_P_19,
AirDefence.SAM_Roland_EWR, AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_Low_Blow_TR, AirDefence.SAM_SA_3_S_125_TR_SNR,
AirDefence.SAM_SA_2_S_75_Fan_Song_TR, AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
AirDefence.SAM_Rapier_Blindfire_TR,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.HQ_7_Self_Propelled_STR, AirDefence.HQ_7_Self_Propelled_STR,
AirDefence.EWR_FuMG_401_Freya_LZ,
AirDefence.EWR_FuSe_65_Würzburg_Riese,
# Ships # Ships
CVN_70_Carl_Vinson, CVN_70_Carl_Vinson,
FFG_Oliver_Hazzard_Perry, Oliver_Hazzard_Perry_class,
CG_Ticonderoga, Ticonderoga_class,
Corvette_1124_4_Grisha, FFL_1124_4_Grisha,
CV_1143_5_Admiral_Kuznetsov, CV_1143_5_Admiral_Kuznetsov,
Corvette_1241_1_Molniya, FSG_1241_1MP_Molniya,
Cruiser_1164_Moskva, CG_1164_Moskva,
Frigate_11540_Neustrashimy, FFG_11540_Neustrashimy,
Battlecruiser_1144_2_Pyotr_Velikiy, CGN_1144_2_Pyotr_Velikiy,
Frigate_1135M_Rezky, FF_1135M_Rezky,
CV_1143_5_Admiral_Kuznetsov_2017, CV_1143_5_Admiral_Kuznetsov_2017,
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
CVN_71_Theodore_Roosevelt, CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln, CVN_72_Abraham_Lincoln,
CVN_73_George_Washington, CVN_73_George_Washington,
DDG_Arleigh_Burke_IIa, USS_Arleigh_Burke_IIa,
LHA_1_Tarawa, LHA_1_Tarawa,
Type_052B_Destroyer, Type_052B_Destroyer,
Type_054A_Frigate, Type_054A_Frigate,
Type_052C_Destroyer, Type_052C_Destroyer,
} ]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
from __future__ import annotations from __future__ import annotations
from game.data.groundunitclass import GroundUnitClass
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -28,9 +27,6 @@ from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@dataclass @dataclass
class Faction: class Faction:
#: List of locales to use when generating random names. If not set, Faker will
#: choose the default locale.
locales: Optional[List[str]]
# Country used by this faction # Country used by this faction
country: str = field(default="") country: str = field(default="")
@@ -74,9 +70,6 @@ class Faction:
# Possible Missile site generators for this faction # Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list) missiles: List[str] = field(default_factory=list)
# Possible costal site generators for this faction
coastal_defenses: List[str] = field(default_factory=list)
# Required mods or asset packs # Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict) requirements: Dict[str, str] = field(default_factory=dict)
@@ -107,9 +100,6 @@ class Faction:
# How many missiles group should we try to generate per CP on startup for this faction # How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1) missiles_group_count: int = field(default=1)
# How many coastal group should we try to generate per CP on startup for this faction
coastal_group_count: int = field(default=1)
# Whether this faction has JTAC access # Whether this faction has JTAC access
has_jtac: bool = field(default=False) has_jtac: bool = field(default=False)
@@ -134,19 +124,10 @@ class Faction:
#: both will use it. #: both will use it.
unrestricted_satnav: bool = False unrestricted_satnav: bool = False
def has_access_to_unittype(self, unitclass: GroundUnitClass) -> bool:
has_access = False
for vehicle in unitclass.unit_list:
if vehicle in self.frontline_units:
return True
if vehicle in self.artillery_units:
return True
return has_access
@classmethod @classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction: def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
faction = Faction(locales=json.get("locales"))
faction = Faction()
faction.country = json.get("country", "/") faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]: if faction.country not in [c.name for c in country_dict.values()]:
@@ -167,8 +148,6 @@ class Faction:
faction.awacs = load_all_aircraft(json.get("awacs", [])) faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", [])) faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.aircrafts = list(set(faction.aircrafts + faction.awacs))
faction.frontline_units = load_all_vehicles(json.get("frontline_units", [])) faction.frontline_units = load_all_vehicles(json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(json.get("artillery_units", [])) faction.artillery_units = load_all_vehicles(json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(json.get("infantry_units", [])) faction.infantry_units = load_all_vehicles(json.get("infantry_units", []))
@@ -183,7 +162,6 @@ class Faction:
faction.air_defenses.extend(json.get("shorads", [])) faction.air_defenses.extend(json.get("shorads", []))
faction.missiles = json.get("missiles", []) faction.missiles = json.get("missiles", [])
faction.coastal_defenses = json.get("coastal_defenses", [])
faction.requirements = json.get("requirements", {}) faction.requirements = json.get("requirements", {})
faction.carrier_names = json.get("carrier_names", []) faction.carrier_names = json.get("carrier_names", [])
@@ -201,7 +179,6 @@ class Faction:
faction.jtac_unit = None faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1)) faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0)) faction.missiles_group_count = int(json.get("missiles_group_count", 0))
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
# Load doctrine # Load doctrine
doctrine = json.get("doctrine", "modern") doctrine = json.get("doctrine", "modern")

View File

@@ -2,9 +2,8 @@ from __future__ import annotations
import json import json
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, Iterator, List, Optional, Type from typing import Dict, Iterator, Optional, Type
from game import persistency
from game.factions.faction import Faction from game.factions.faction import Faction
FACTION_DIRECTORY = Path("./resources/factions/") FACTION_DIRECTORY = Path("./resources/factions/")
@@ -24,22 +23,15 @@ class FactionLoader:
if self._factions is None: if self._factions is None:
self._factions = self.load_factions() self._factions = self.load_factions()
@staticmethod
def find_faction_files_in(path: Path) -> List[Path]:
return [f for f in path.glob("*.json") if f.is_file()]
@classmethod @classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]: def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions" files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
files = cls.find_faction_files_in(
FACTION_DIRECTORY
) + cls.find_faction_files_in(user_faction_path)
factions = {} factions = {}
for f in files: for f in files:
try: try:
with f.open("r", encoding="utf-8") as fdata: with f.open("r", encoding="utf-8") as fdata:
data = json.load(fdata) data = json.load(fdata, encoding="utf-8")
factions[data["name"]] = Faction.from_json(data) factions[data["name"]] = Faction.from_json(data)
logging.info("Loaded faction : " + str(f)) logging.info("Loaded faction : " + str(f))
except Exception: except Exception:

View File

@@ -4,19 +4,17 @@ import random
import sys import sys
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from enum import Enum from enum import Enum
from typing import Any, Dict, List, Iterator from typing import Any, Dict, List
from dcs.action import Coalition from dcs.action import Coalition
from dcs.mapping import Point from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike from dcs.task import CAP, CAS, PinpointStrike
from dcs.vehicles import AirDefence from dcs.vehicles import AirDefence
from faker import Faker
from game import db from game import db
from game.inventory import GlobalAircraftInventory from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import MissileSiteGroundObject
from gen.ato import AirTaskingOrder from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -25,21 +23,17 @@ from gen.flights.flight import FlightType
from gen.ground_forces.ai_ground_planner import GroundPlanner from gen.ground_forces.ai_ground_planner import GroundPlanner
from . import persistency from . import persistency
from .debriefing import Debriefing from .debriefing import Debriefing
from .event.event import Event from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction from .factions.faction import Faction
from .income import Income from .income import Income
from .infos.information import Information from .infos.information import Information
from .navmesh import NavMesh from .navmesh import NavMesh
from .procurement import AircraftProcurementRequest, ProcurementAi from .procurement import ProcurementAi
from .profiling import logged_duration from .settings import Settings
from .settings import Settings, AutoAtoBehavior from .theater import ConflictTheater, ControlPoint, TheaterGroundObject
from .squadrons import Pilot, AirWing from game.theater.theatergroundobject import MissileSiteGroundObject
from .theater import ConflictTheater
from .theater.bullseye import Bullseye
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .threatzones import ThreatZones from .threatzones import ThreatZones
from .transfers import PendingTransfers
from .unitmap import UnitMap from .unitmap import UnitMap
from .weather import Conditions, TimeOfDay from .weather import Conditions, TimeOfDay
@@ -101,10 +95,7 @@ class Game:
self.player_country = db.FACTIONS[player_name].country self.player_country = db.FACTIONS[player_name].country
self.enemy_name = enemy_name self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name].country self.enemy_country = db.FACTIONS[enemy_name].country
# pass_turn() will be called when initialization is complete which will self.turn = 0
# increment this to turn 0 before it reaches the player.
self.turn = -1
# NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day) self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats() self.game_stats = GameStats()
self.game_stats.update(self) self.game_stats.update(self)
@@ -113,6 +104,8 @@ class Game:
self.informations.append(Information("Game Start", "-" * 40, 0)) self.informations.append(Information("Game Start", "-" * 40, 0))
# Culling Zones are for areas around points of interest that contain things we may not wish to cull. # Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = [] self.__culling_zones: List[Point] = []
# Culling Points are for individual theater ground objects that we don't wish to cull.
self.__culling_points: List[Point] = []
self.__destroyed_units: List[str] = [] self.__destroyed_units: List[str] = []
self.savepath = "" self.savepath = ""
self.budget = player_budget self.budget = player_budget
@@ -122,31 +115,25 @@ class Game:
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
self.blue_transit_network = TransitNetwork()
self.red_transit_network = TransitNetwork()
self.blue_procurement_requests: List[AircraftProcurementRequest] = []
self.red_procurement_requests: List[AircraftProcurementRequest] = []
self.blue_ato = AirTaskingOrder() self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder() self.red_ato = AirTaskingOrder()
self.blue_bullseye = Bullseye(Point(0, 0))
self.red_bullseye = Bullseye(Point(0, 0))
self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints) self.aircraft_inventory = GlobalAircraftInventory(self.theater.controlpoints)
self.transfers = PendingTransfers(self)
self.sanitize_sides() self.sanitize_sides()
self.blue_faker = Faker(self.player_faction.locales) self.on_load()
self.red_faker = Faker(self.enemy_faction.locales)
self.blue_air_wing = AirWing(self, player=True) # Turn 0 procurement. We don't actually have any missions to plan, but
self.red_air_wing = AirWing(self, player=False) # the planner will tell us what it would like to plan so we can use that
# to drive purchase decisions.
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
self.on_load(game_still_initializing=True) red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
self.plan_procurement(blue_planner, red_planner)
def __getstate__(self) -> Dict[str, Any]: def __getstate__(self) -> Dict[str, Any]:
state = self.__dict__.copy() state = self.__dict__.copy()
@@ -156,8 +143,6 @@ class Game:
del state["red_threat_zone"] del state["red_threat_zone"]
del state["blue_navmesh"] del state["blue_navmesh"]
del state["red_navmesh"] del state["red_navmesh"]
del state["blue_faker"]
del state["red_faker"]
return state return state
def __setstate__(self, state: Dict[str, Any]) -> None: def __setstate__(self, state: Dict[str, Any]) -> None:
@@ -165,26 +150,9 @@ class Game:
# Regenerate any state that was not persisted. # Regenerate any state that was not persisted.
self.on_load() self.on_load()
def ato_for(self, player: bool) -> AirTaskingOrder:
if player:
return self.blue_ato
return self.red_ato
def procurement_requests_for(
self, player: bool
) -> List[AircraftProcurementRequest]:
if player:
return self.blue_procurement_requests
return self.red_procurement_requests
def transit_network_for(self, player: bool) -> TransitNetwork:
if player:
return self.blue_transit_network
return self.red_transit_network
def generate_conditions(self) -> Conditions: def generate_conditions(self) -> Conditions:
return Conditions.generate( return Conditions.generate(
self.theater, self.current_day, self.current_turn_time_of_day, self.settings self.theater, self.date, self.current_turn_time_of_day, self.settings
) )
def sanitize_sides(self): def sanitize_sides(self):
@@ -213,26 +181,6 @@ class Game:
return self.player_faction return self.player_faction
return self.enemy_faction return self.enemy_faction
def faker_for(self, player: bool) -> Faker:
if player:
return self.blue_faker
return self.red_faker
def air_wing_for(self, player: bool) -> AirWing:
if player:
return self.blue_air_wing
return self.red_air_wing
def country_for(self, player: bool) -> str:
if player:
return self.player_country
return self.enemy_country
def bullseye_for(self, player: bool) -> Bullseye:
if player:
return self.blue_bullseye
return self.red_bullseye
def _roll(self, prob, mult): def _roll(self, prob, mult):
if self.settings.version == "dev": if self.settings.version == "dev":
# always generate all events for dev # always generate all events for dev
@@ -253,11 +201,11 @@ class Game:
) )
def _generate_events(self): def _generate_events(self):
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts(True):
self._generate_player_event( self._generate_player_event(
FrontlineAttackEvent, FrontlineAttackEvent,
front_line.blue_cp, front_line.control_point_a,
front_line.red_cp, front_line.control_point_b,
) )
def adjust_budget(self, amount: float, player: bool) -> None: def adjust_budget(self, amount: float, player: bool) -> None:
@@ -299,40 +247,27 @@ class Game:
else: else:
raise RuntimeError(f"{event} was passed when an Event type was expected") raise RuntimeError(f"{event} was passed when an Event type was expected")
def on_load(self, game_still_initializing: bool = False) -> None: def on_load(self) -> None:
LuaPluginManager.load_settings(self.settings) LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater) ObjectiveDistanceCache.set_theater(self.theater)
self.compute_conflicts_position() self.compute_conflicts_position()
if not game_still_initializing: self.compute_threat_zones()
self.compute_threat_zones()
self.blue_faker = Faker(self.faction_for(player=True).locales)
self.red_faker = Faker(self.faction_for(player=False).locales)
def reset_ato(self) -> None: def pass_turn(self, no_action: bool = False) -> None:
self.blue_ato.clear() logging.info("Pass turn")
self.red_ato.clear()
def finish_turn(self, skipped: bool = False) -> None:
self.informations.append( self.informations.append(
Information("End of turn #" + str(self.turn), "-" * 40, 0) Information("End of turn #" + str(self.turn), "-" * 40, 0)
) )
self.turn += 1 self.turn += 1
# Need to recompute before transfers and deliveries to account for captures.
# This happens in in initialize_turn as well, because cheating doesn't advance a
# turn but can capture bases so we need to recompute there as well.
self.compute_transit_networks()
# Must happen *before* unit deliveries are handled, or else new units will spawn
# one hop ahead. ControlPoint.process_turn handles unit deliveries.
self.transfers.perform_transfers()
# Needs to happen *before* planning transfers so we don't cancel the
self.reset_ato()
for control_point in self.theater.controlpoints: for control_point in self.theater.controlpoints:
control_point.process_turn(self) control_point.process_turn(self)
if not skipped and self.turn > 1: self.process_enemy_income()
self.process_player_income()
if not no_action and self.turn > 1:
for cp in self.theater.player_points(): for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY) cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
else: else:
@@ -342,54 +277,26 @@ class Game:
self.conditions = self.generate_conditions() self.conditions = self.generate_conditions()
self.process_enemy_income()
self.process_player_income()
def begin_turn_0(self) -> None:
self.turn = 0
self.initialize_turn() self.initialize_turn()
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
with logged_duration("Turn finalization"):
self.finish_turn(no_action)
with logged_duration("Turn initialization"):
self.initialize_turn()
# Autosave progress # Autosave progress
persistency.autosave(self) persistency.autosave(self)
def check_win_loss(self): def check_win_loss(self):
player_airbases = { captured_states = {i.captured for i in self.theater.controlpoints}
cp for cp in self.theater.player_points() if cp.runway_is_operational() if True not in captured_states:
}
if not player_airbases:
return TurnState.LOSS return TurnState.LOSS
if False not in captured_states:
enemy_airbases = {
cp for cp in self.theater.enemy_points() if cp.runway_is_operational()
}
if not enemy_airbases:
return TurnState.WIN return TurnState.WIN
return TurnState.CONTINUE return TurnState.CONTINUE
def set_bullseye(self) -> None:
player_cp, enemy_cp = self.theater.closest_opposing_control_points()
self.blue_bullseye = Bullseye(enemy_cp.position)
self.red_bullseye = Bullseye(player_cp.position)
def initialize_turn(self) -> None: def initialize_turn(self) -> None:
self.events = [] self.events = []
self._generate_events() self._generate_events()
self.set_bullseye()
# Update statistics # Update statistics
self.game_stats.update(self) self.game_stats.update(self)
self.blue_air_wing.reset()
self.red_air_wing.reset()
self.aircraft_inventory.reset() self.aircraft_inventory.reset()
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp) self.aircraft_inventory.set_from_control_point(cp)
@@ -400,30 +307,17 @@ class Game:
return self.process_win_loss(turn_state) return self.process_win_loss(turn_state)
# Plan flights & combat for next turn # Plan flights & combat for next turn
with logged_duration("Computing conflict positions"): self.compute_conflicts_position()
self.compute_conflicts_position() self.compute_threat_zones()
with logged_duration("Threat zone computation"):
self.compute_threat_zones()
with logged_duration("Transit network identification"):
self.compute_transit_networks()
self.ground_planners = {} self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
self.blue_procurement_requests.clear() blue_planner = CoalitionMissionPlanner(self, is_player=True)
self.red_procurement_requests.clear() blue_planner.plan_missions()
with logged_duration("Procurement of airlift assets"): red_planner = CoalitionMissionPlanner(self, is_player=False)
self.transfers.order_airlift_assets() red_planner.plan_missions()
with logged_duration("Transport planning"):
self.transfers.plan_transports()
with logged_duration("Blue mission planning"):
if self.settings.auto_ato_behavior is not AutoAtoBehavior.Disabled:
blue_planner = CoalitionMissionPlanner(self, is_player=True)
blue_planner.plan_missions()
with logged_duration("Red mission planning"):
red_planner = CoalitionMissionPlanner(self, is_player=False)
red_planner.plan_missions()
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
if cp.has_frontline: if cp.has_frontline:
@@ -431,15 +325,19 @@ class Game:
gplanner.plan_groundwar() gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner self.ground_planners[cp.id] = gplanner
self.plan_procurement() self.plan_procurement(blue_planner, red_planner)
def plan_procurement(self) -> None: def plan_procurement(
self,
blue_planner: CoalitionMissionPlanner,
red_planner: CoalitionMissionPlanner,
) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it # The first turn needs to buy a *lot* of aircraft to fill CAPs, so it
# gets much more of the budget that turn. Otherwise budget (after # gets much more of the budget that turn. Otherwise budget (after
# repairs) is split evenly between air and ground. For the default # repairs) is split evenly between air and ground. For the default
# starting budget of 2000 this gives 600 to ground forces and 1400 to # starting budget of 2000 this gives 600 to ground forces and 1400 to
# aircraft. After that the budget will be spend proportionally based on how much is already invested # aircraft.
ground_portion = 0.3 if self.turn == 0 else 0.5
self.budget = ProcurementAi( self.budget = ProcurementAi(
self, self,
for_player=True, for_player=True,
@@ -447,7 +345,8 @@ class Game:
manage_runways=self.settings.automate_runway_repair, manage_runways=self.settings.automate_runway_repair,
manage_front_line=self.settings.automate_front_line_reinforcements, manage_front_line=self.settings.automate_front_line_reinforcements,
manage_aircraft=self.settings.automate_aircraft_reinforcements, manage_aircraft=self.settings.automate_aircraft_reinforcements,
).spend_budget(self.budget) front_line_budget_share=ground_portion,
).spend_budget(self.budget, blue_planner.procurement_requests)
self.enemy_budget = ProcurementAi( self.enemy_budget = ProcurementAi(
self, self,
@@ -456,7 +355,8 @@ class Game:
manage_runways=True, manage_runways=True,
manage_front_line=True, manage_front_line=True,
manage_aircraft=True, manage_aircraft=True,
).spend_budget(self.enemy_budget) front_line_budget_share=ground_portion,
).spend_budget(self.enemy_budget, red_planner.procurement_requests)
def message(self, text: str) -> None: def message(self, text: str) -> None:
self.informations.append(Information(text, turn=self.turn)) self.informations.append(Information(text, turn=self.turn))
@@ -483,13 +383,6 @@ class Game:
self.current_group_id += 1 self.current_group_id += 1
return self.current_group_id return self.current_group_id
def compute_transit_networks(self) -> None:
self.blue_transit_network = self.compute_transit_network_for(player=True)
self.red_transit_network = self.compute_transit_network_for(player=False)
def compute_transit_network_for(self, player: bool) -> TransitNetwork:
return TransitNetworkBuilder(self.theater, player).build()
def compute_threat_zones(self) -> None: def compute_threat_zones(self) -> None:
self.blue_threat_zone = ThreatZones.for_faction(self, player=True) self.blue_threat_zone = ThreatZones.for_faction(self, player=True)
self.red_threat_zone = ThreatZones.for_faction(self, player=False) self.red_threat_zone = ThreatZones.for_faction(self, player=False)
@@ -516,15 +409,23 @@ class Game:
:return: List of points of interests :return: List of points of interests
""" """
zones = [] zones = []
points = []
# By default, use the existing frontline conflict position # By default, use the existing frontline conflict position
for front_line in self.theater.conflicts(): for front_line in self.theater.conflicts():
position = Conflict.frontline_position(front_line, self.theater) position = Conflict.frontline_position(
front_line.control_point_a, front_line.control_point_b, self.theater
)
zones.append(position[0]) zones.append(position[0])
zones.append(front_line.blue_cp.position) zones.append(front_line.control_point_a.position)
zones.append(front_line.red_cp.position) zones.append(front_line.control_point_b.position)
for cp in self.theater.controlpoints: for cp in self.theater.controlpoints:
# Don't cull missile sites - their range is long enough to make them
# easily culled despite being a threat.
for tgo in cp.ground_objects:
if isinstance(tgo, MissileSiteGroundObject):
points.append(tgo.position)
# If do_not_cull_carrier is enabled, add carriers as culling point # If do_not_cull_carrier is enabled, add carriers as culling point
if self.settings.perf_do_not_cull_carrier: if self.settings.perf_do_not_cull_carrier:
if cp.is_carrier or cp.is_lha: if cp.is_carrier or cp.is_lha:
@@ -568,6 +469,7 @@ class Game:
zones.append(Point(0, 0)) zones.append(Point(0, 0))
self.__culling_zones = zones self.__culling_zones = zones
self.__culling_points = points
def add_destroyed_units(self, data): def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"]) pos = Point(data["x"], data["z"])
@@ -583,12 +485,19 @@ class Game:
:param pos: Position you are tryng to spawn stuff at :param pos: Position you are tryng to spawn stuff at
:return: True if units can not be added at given position :return: True if units can not be added at given position
""" """
if not self.settings.perf_culling: if self.settings.perf_culling == False:
return False return False
for z in self.__culling_zones: else:
if z.distance_to_point(pos) < self.settings.perf_culling_distance * 1000: for z in self.__culling_zones:
return False if (
return True z.distance_to_point(pos)
< self.settings.perf_culling_distance * 1000
):
return False
for p in self.__culling_points:
if p.distance_to_point(pos) < 2500:
return False
return True
def get_culling_zones(self): def get_culling_zones(self):
""" """
@@ -597,6 +506,13 @@ class Game:
""" """
return self.__culling_zones return self.__culling_zones
def get_culling_points(self):
"""
Check culling points
:return: List of culling points
"""
return self.__culling_points
# 1 = red, 2 = blue # 1 = red, 2 = blue
def get_player_coalition_id(self): def get_player_coalition_id(self):
return 2 return 2

View File

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

View File

@@ -21,10 +21,6 @@ from game.threatzones import ThreatZones
from game.utils import nautical_miles from game.utils import nautical_miles
class NavMeshError(RuntimeError):
pass
class NavMeshPoly: class NavMeshPoly:
def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None: def __init__(self, ident: int, poly: Polygon, threatened: bool) -> None:
self.ident = ident self.ident = ident
@@ -103,7 +99,7 @@ class NavMesh:
# currently. # currently.
p = ShapelyPoint(point.x, point.y) p = ShapelyPoint(point.x, point.y)
for navpoly in self.polys: for navpoly in self.polys:
if navpoly.poly.intersects(p): if navpoly.poly.contains(p):
return navpoly return navpoly
return None return None
@@ -129,7 +125,7 @@ class NavMesh:
path.append(current.world_point) path.append(current.world_point)
previous = came_from[current] previous = came_from[current]
if previous is None: if previous is None:
raise NavMeshError( raise RuntimeError(
f"Could not reconstruct path to {destination} from {origin}" f"Could not reconstruct path to {destination} from {origin}"
) )
current = previous current = previous
@@ -144,12 +140,10 @@ class NavMesh:
def shortest_path(self, origin: Point, destination: Point) -> List[Point]: def shortest_path(self, origin: Point, destination: Point) -> List[Point]:
origin_poly = self.localize(origin) origin_poly = self.localize(origin)
if origin_poly is None: if origin_poly is None:
raise NavMeshError(f"Origin point {origin} is outside the navmesh") raise ValueError(f"Origin point {origin} is outside the navmesh")
destination_poly = self.localize(destination) destination_poly = self.localize(destination)
if destination_poly is None: if destination_poly is None:
raise NavMeshError( raise ValueError(f"Origin point {destination} is outside the navmesh")
f"Destination point {destination} is outside the navmesh"
)
return self._shortest_path( return self._shortest_path(
NavPoint(self.dcs_to_shapely_point(origin), origin_poly), NavPoint(self.dcs_to_shapely_point(origin), origin_poly),
@@ -209,7 +203,7 @@ class NavMesh:
# threatened airbases at the map edges have room to retreat from the # threatened airbases at the map edges have room to retreat from the
# threat without running off the navmesh. # threat without running off the navmesh.
return box(*LineString(points).bounds).buffer( return box(*LineString(points).bounds).buffer(
nautical_miles(200).meters, resolution=1 nautical_miles(100).meters, resolution=1
) )
@staticmethod @staticmethod

View File

@@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from game.theater.theatergroundobject import TheaterGroundObject
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Iterable, List, Set, TYPE_CHECKING from typing import TYPE_CHECKING, Iterable, List, Optional, Set
from dcs import Mission from dcs import Mission
from dcs.action import DoScript, DoScriptFile from dcs.action import DoScript, DoScriptFile
@@ -13,9 +14,7 @@ from dcs.lua.parse import loads
from dcs.mapping import Point from dcs.mapping import Point
from dcs.translation import String from dcs.translation import String
from dcs.triggers import TriggerStart from dcs.triggers import TriggerStart
from game.plugins import LuaPluginManager from game.plugins import LuaPluginManager
from game.theater.theatergroundobject import TheaterGroundObject
from gen import Conflict, FlightType, VisualGenerator from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA from gen.airfields import AIRFIELD_DATA
@@ -23,8 +22,6 @@ from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
from gen.cargoshipgen import CargoShipGenerator
from gen.convoygen import ConvoyGenerator
from gen.environmentgen import EnvironmentGenerator from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator from gen.groundobjectsgen import GroundObjectsGenerator
@@ -33,8 +30,9 @@ from gen.naming import namegen
from gen.radios import RadioFrequency, RadioRegistry from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from .. import db from .. import db
from ..theater import Airfield, FrontLine from ..theater import Airfield
from ..unitmap import UnitMap from ..unitmap import UnitMap
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -44,13 +42,18 @@ if TYPE_CHECKING:
class Operation: class Operation:
"""Static class for managing the final Mission generation""" """Static class for managing the final Mission generation"""
current_mission: Mission current_mission = None # type: Mission
airgen: AircraftConflictGenerator airgen = None # type: AircraftConflictGenerator
airsupportgen: AirSupportConflictGenerator triggersgen = None # type: TriggersGenerator
groundobjectgen: GroundObjectsGenerator airsupportgen = None # type: AirSupportConflictGenerator
radio_registry: RadioRegistry visualgen = None # type: VisualGenerator
tacan_registry: TacanRegistry groundobjectgen = None # type: GroundObjectsGenerator
game: Game briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None
game = None # type: Game
environment_settings = None
trigger_radius = TRIGGER_RADIUS_MEDIUM trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None is_quick = None
player_awacs_enabled = True player_awacs_enabled = True
@@ -76,7 +79,8 @@ class Operation:
for frontline in cls.game.theater.conflicts(): for frontline in cls.game.theater.conflicts():
yield Conflict( yield Conflict(
cls.game.theater, cls.game.theater,
frontline, frontline.control_point_a,
frontline.control_point_b,
cls.game.player_name, cls.game.player_name,
cls.game.enemy_name, cls.game.enemy_name,
cls.game.player_country, cls.game.player_country,
@@ -94,7 +98,8 @@ class Operation:
) )
return Conflict( return Conflict(
cls.game.theater, cls.game.theater,
FrontLine(player_cp, enemy_cp), player_cp,
enemy_cp,
cls.game.player_name, cls.game.player_name,
cls.game.enemy_name, cls.game.enemy_name,
cls.game.player_country, cls.game.player_country,
@@ -108,12 +113,8 @@ class Operation:
@classmethod @classmethod
def _setup_mission_coalitions(cls): def _setup_mission_coalitions(cls):
cls.current_mission.coalition["blue"] = Coalition( cls.current_mission.coalition["blue"] = Coalition("blue")
"blue", bullseye=cls.game.blue_bullseye.to_pydcs() cls.current_mission.coalition["red"] = Coalition("red")
)
cls.current_mission.coalition["red"] = Coalition(
"red", bullseye=cls.game.red_bullseye.to_pydcs()
)
p_country = cls.game.player_country p_country = cls.game.player_country
e_country = cls.game.enemy_country e_country = cls.game.enemy_country
@@ -165,7 +166,6 @@ class Operation:
airgen: AircraftConflictGenerator, airgen: AircraftConflictGenerator,
): ):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)""" """Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)"""
gens: List[MissionInfoGenerator] = [ gens: List[MissionInfoGenerator] = [
KneeboardGenerator(cls.current_mission, cls.game), KneeboardGenerator(cls.current_mission, cls.game),
BriefingGenerator(cls.current_mission, cls.game), BriefingGenerator(cls.current_mission, cls.game),
@@ -175,16 +175,14 @@ class Operation:
gen.add_dynamic_runway(dynamic_runway) gen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
if tanker.blue: gen.add_tanker(tanker)
gen.add_tanker(tanker)
for aewc in airsupportgen.air_support.awacs: if cls.player_awacs_enabled:
if aewc.blue: for awacs in airsupportgen.air_support.awacs:
gen.add_awacs(aewc) gen.add_awacs(awacs)
for jtac in jtacs: for jtac in jtacs:
if jtac.blue: gen.add_jtac(jtac)
gen.add_jtac(jtac)
for flight in airgen.flights: for flight in airgen.flights:
gen.add_flight(flight) gen.add_flight(flight)
@@ -310,7 +308,6 @@ class Operation:
# Set mission time and weather conditions. # Set mission time and weather conditions.
EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate() EnvironmentGenerator(cls.current_mission, cls.game.conditions).generate()
cls._generate_ground_units() cls._generate_ground_units()
cls._generate_transports()
cls._generate_destroyed_units() cls._generate_destroyed_units()
cls._generate_air_units() cls._generate_air_units()
cls.assign_channels_to_flights( cls.assign_channels_to_flights(
@@ -324,8 +321,13 @@ class Operation:
# Setup combined arms parameters # Setup combined arms parameters
cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0 cls.current_mission.groundControl.pilot_can_control_vehicles = cls.ca_slots > 0
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots if cls.game.player_country in [
cls.current_mission.groundControl.blue_observer = 1 country.name
for country in cls.current_mission.coalition["blue"].countries.values()
]:
cls.current_mission.groundControl.blue_tactical_commander = cls.ca_slots
else:
cls.current_mission.groundControl.red_tactical_commander = cls.ca_slots
# Options # Options
forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game) forcedoptionsgen = ForcedOptionsGenerator(cls.current_mission, cls.game)
@@ -376,9 +378,7 @@ class Operation:
cls.game, cls.game,
cls.radio_registry, cls.radio_registry,
cls.unit_map, cls.unit_map,
air_support=cls.airsupportgen.air_support,
) )
cls.airgen.clear_parking_slots() cls.airgen.clear_parking_slots()
cls.airgen.generate_flights( cls.airgen.generate_flights(
@@ -399,16 +399,16 @@ class Operation:
@classmethod @classmethod
def _generate_ground_conflicts(cls) -> None: def _generate_ground_conflicts(cls) -> None:
"""For each frontline in the Operation, generate the ground conflicts and JTACs""" """For each frontline in the Operation, generate the ground conflicts and JTACs"""
cls.jtacs = [] for front_line in cls.game.theater.conflicts(True):
for front_line in cls.game.theater.conflicts(): player_cp = front_line.control_point_a
player_cp = front_line.blue_cp enemy_cp = front_line.control_point_b
enemy_cp = front_line.red_cp
conflict = Conflict.frontline_cas_conflict( conflict = Conflict.frontline_cas_conflict(
cls.game.player_name, cls.game.player_name,
cls.game.enemy_name, cls.game.enemy_name,
cls.current_mission.country(cls.game.player_country), cls.current_mission.country(cls.game.player_country),
cls.current_mission.country(cls.game.enemy_country), cls.current_mission.country(cls.game.enemy_country),
front_line, player_cp,
enemy_cp,
cls.game.theater, cls.game.theater,
) )
# Generate frontline ops # Generate frontline ops
@@ -426,12 +426,6 @@ class Operation:
ground_conflict_gen.generate() ground_conflict_gen.generate()
cls.jtacs.extend(ground_conflict_gen.jtacs) cls.jtacs.extend(ground_conflict_gen.jtacs)
@classmethod
def _generate_transports(cls) -> None:
"""Generates convoys for unit transfers by road."""
ConvoyGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
CargoShipGenerator(cls.current_mission, cls.game, cls.unit_map).generate()
@classmethod @classmethod
def reset_naming_ids(cls): def reset_naming_ids(cls):
namegen.reset_numbers() namegen.reset_numbers()
@@ -450,13 +444,11 @@ class Operation:
"AWACs": {}, "AWACs": {},
"JTACs": {}, "JTACs": {},
"TargetPoints": {}, "TargetPoints": {},
"RedAA": {},
"BlueAA": {},
} # type: ignore } # type: ignore
for tanker in airsupportgen.air_support.tankers: for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = { luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.group_name, "dcsGroupName": tanker.dcsGroupName,
"callsign": tanker.callsign, "callsign": tanker.callsign,
"variant": tanker.variant, "variant": tanker.variant,
"radio": tanker.freq.mhz, "radio": tanker.freq.mhz,
@@ -466,14 +458,14 @@ class Operation:
if airsupportgen.air_support.awacs: if airsupportgen.air_support.awacs:
for awacs in airsupportgen.air_support.awacs: for awacs in airsupportgen.air_support.awacs:
luaData["AWACs"][awacs.callsign] = { luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.group_name, "dcsGroupName": awacs.dcsGroupName,
"callsign": awacs.callsign, "callsign": awacs.callsign,
"radio": awacs.freq.mhz, "radio": awacs.freq.mhz,
} }
for jtac in jtacs: for jtac in jtacs:
luaData["JTACs"][jtac.callsign] = { luaData["JTACs"][jtac.callsign] = {
"dcsGroupName": jtac.group_name, "dcsGroupName": jtac.dcsGroupName,
"callsign": jtac.callsign, "callsign": jtac.callsign,
"zone": jtac.region, "zone": jtac.region,
"dcsUnit": jtac.unit_name, "dcsUnit": jtac.unit_name,
@@ -509,26 +501,6 @@ class Operation:
}, },
} }
for cp in cls.game.theater.controlpoints:
for ground_object in cp.ground_objects:
if ground_object.might_have_aa and not ground_object.is_dead:
for g in ground_object.groups:
threat_range = ground_object.threat_range(g)
if not threat_range:
continue
faction = "BlueAA" if cp.captured else "RedAA"
luaData[faction][g.name] = {
"name": ground_object.name,
"range": threat_range.meters,
"position": {
"x": ground_object.position.x,
"y": ground_object.position.y,
},
}
# set a LUA table with data from Liberation that we want to set # set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function # at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts # later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
@@ -593,7 +565,8 @@ class Operation:
zone = data["zone"] zone = data["zone"]
laserCode = data["laserCode"] laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"] dcsUnit = data["dcsUnit"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone={repr(zone)}, laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n" lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
# lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
lua += "}" lua += "}"
# Process the Target Points # Process the Target Points
@@ -620,33 +593,7 @@ class Operation:
-- list the aircraft carriers generated by Liberation -- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {} -- dcsLiberation.Carriers = {}
-- list the Red AA generated by Liberation -- later, we'll add more data to the table
dcsLiberation.RedAA = {
"""
for key in luaData["RedAA"]:
data = luaData["RedAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
-- list the Blue AA generated by Liberation
dcsLiberation.BlueAA = {
"""
for key in luaData["BlueAA"]:
data = luaData["BlueAA"][key]
name = data["name"]
radius = data["range"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{dcsGroupName='{key}', name='{name}', range='{radius}', positionX='{positionX}', positionY='{positionY}' }}, \n"
lua += "}"
lua += """
""" """

View File

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

View File

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

View File

@@ -1,35 +0,0 @@
from __future__ import annotations
import logging
import timeit
from collections import defaultdict
from contextlib import contextmanager
from datetime import timedelta
from typing import Iterator
@contextmanager
def logged_duration(event: str) -> Iterator[None]:
start = timeit.default_timer()
yield
end = timeit.default_timer()
logging.debug("%s took %s", event, timedelta(seconds=end - start))
class MultiEventTracer:
def __init__(self) -> None:
self.events: dict[str, timedelta] = defaultdict(timedelta)
def __enter__(self) -> MultiEventTracer:
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
for event, duration in self.events.items():
logging.debug("%s took %s", event, duration)
@contextmanager
def trace(self, event: str) -> Iterator[None]:
start = timeit.default_timer()
yield
end = timeit.default_timer()
self.events[event] += timedelta(seconds=end - start)

View File

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

View File

@@ -1,19 +1,9 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta
from enum import Enum, unique
from typing import Dict, Optional from typing import Dict, Optional
from dcs.forcedoptions import ForcedOptions from dcs.forcedoptions import ForcedOptions
@unique
class AutoAtoBehavior(Enum):
Disabled = "Disabled"
Never = "Never assign player pilots"
Default = "No preference"
Prefer = "Prefer player pilots"
@dataclass @dataclass
class Settings: class Settings:
@@ -35,29 +25,19 @@ class Settings:
default_start_type: str = "Cold" default_start_type: str = "Cold"
# Mission specific
desired_player_mission_duration: timedelta = timedelta(minutes=60)
# Campaign management # Campaign management
automate_runway_repair: bool = False automate_runway_repair: bool = False
automate_front_line_reinforcements: bool = False automate_front_line_reinforcements: bool = False
automate_aircraft_reinforcements: bool = False automate_aircraft_reinforcements: bool = False
restrict_weapons_by_date: bool = False restrict_weapons_by_date: bool = False
disable_legacy_aewc: bool = True
generate_dark_kneeboard: bool = False
invulnerable_player_pilots: bool = True
auto_ato_behavior: AutoAtoBehavior = AutoAtoBehavior.Default
auto_ato_player_missions_asap: bool = True
# Performance oriented # Performance oriented
perf_red_alert_state: bool = True perf_red_alert_state: bool = True
perf_smoke_gen: bool = True perf_smoke_gen: bool = True
perf_smoke_spacing = 1600
perf_artillery: bool = True perf_artillery: bool = True
perf_moving_units: bool = True perf_moving_units: bool = True
perf_infantry: bool = True perf_infantry: bool = True
perf_destroyed_units: bool = True perf_destroyed_units: bool = True
reserves_procurement_target: int = 10
# Performance culling # Performance culling
perf_culling: bool = False perf_culling: bool = False

View File

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

View File

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

View File

@@ -4,12 +4,13 @@ import math
import typing import typing
from typing import Dict, Type from typing import Dict, Type
from dcs.task import AWACS, CAP, CAS, Embarking, PinpointStrike, Task, Transport from dcs.task import CAP, CAS, Embarking, PinpointStrike, Task
from dcs.unittype import FlyingType, UnitType, VehicleType from dcs.unittype import FlyingType, UnitType, VehicleType
from dcs.vehicles import AirDefence, Armor from dcs.vehicles import AirDefence, Armor
from game import db from game import db
from game.db import PRICES from game.db import PRICES
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
STRENGTH_AA_ASSEMBLE_MIN = 0.2 STRENGTH_AA_ASSEMBLE_MIN = 0.2
PLANES_SCRAMBLE_MIN_BASE = 2 PLANES_SCRAMBLE_MIN_BASE = 2
@@ -24,7 +25,6 @@ class Base:
def __init__(self): def __init__(self):
self.aircraft: Dict[Type[FlyingType], int] = {} self.aircraft: Dict[Type[FlyingType], int] = {}
self.armor: Dict[Type[VehicleType], int] = {} self.armor: Dict[Type[VehicleType], int] = {}
# TODO: Appears unused.
self.aa: Dict[AirDefence, int] = {} self.aa: Dict[AirDefence, int] = {}
self.commision_points: Dict[Type, float] = {} self.commision_points: Dict[Type, float] = {}
self.strength = 1 self.strength = 1
@@ -47,6 +47,10 @@ class Base:
logging.exception(f"No price found for {unit_type.id}") logging.exception(f"No price found for {unit_type.id}")
return total return total
@property
def total_frontline_aa(self) -> int:
return sum([v for k, v in self.armor.items() if k in TYPE_SHORAD])
@property @property
def total_aa(self) -> int: def total_aa(self) -> int:
return sum(self.aa.values()) return sum(self.aa.values())
@@ -143,13 +147,7 @@ class Base:
for_task = db.unit_task(unit_type) for_task = db.unit_task(unit_type)
target_dict = None target_dict = None
if ( if for_task == CAS or for_task == CAP or for_task == Embarking:
for_task == AWACS
or for_task == CAS
or for_task == CAP
or for_task == Embarking
or for_task == Transport
):
target_dict = self.aircraft target_dict = self.aircraft
elif for_task == PinpointStrike: elif for_task == PinpointStrike:
target_dict = self.armor target_dict = self.armor

View File

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

View File

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

View File

@@ -1,11 +1,16 @@
from __future__ import annotations from __future__ import annotations
import itertools import itertools
import math import json
import logging
from dataclasses import dataclass from dataclasses import dataclass
from functools import cached_property from functools import cached_property
from itertools import tee
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Iterator, List, Optional, Tuple from typing import Any, Dict, Iterator, List, Optional, Set, Tuple, Union, cast
from shapely import geometry
from shapely import ops
from dcs import Mission from dcs import Mission
from dcs.countries import ( from dcs.countries import (
@@ -16,12 +21,11 @@ from dcs.country import Country
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import F_15C from dcs.planes import F_15C
from dcs.ships import ( from dcs.ships import (
Bulker_Handy_Wind,
CVN_74_John_C__Stennis, CVN_74_John_C__Stennis,
DDG_Arleigh_Burke_IIa,
LHA_1_Tarawa, LHA_1_Tarawa,
USS_Arleigh_Burke_IIa,
) )
from dcs.statics import Fortification, Warehouse from dcs.statics import Fortification
from dcs.terrain import ( from dcs.terrain import (
caucasus, caucasus,
nevada, nevada,
@@ -39,26 +43,21 @@ from dcs.unitgroup import (
VehicleGroup, VehicleGroup,
) )
from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed from dcs.vehicles import AirDefence, Armor, MissilesSS, Unarmed
from pyproj import CRS, Transformer
from shapely import geometry, ops
from gen.flights.flight import FlightType
from .controlpoint import ( from .controlpoint import (
Airfield, Airfield,
Carrier, Carrier,
ControlPoint, ControlPoint,
Fob,
Lha, Lha,
MissionTarget, MissionTarget,
OffMapSpawn, OffMapSpawn,
Fob,
) )
from .frontline import FrontLine
from .landmap import Landmap, load_landmap, poly_contains from .landmap import Landmap, load_landmap, poly_contains
from .latlon import LatLon from ..utils import Distance, meters, nautical_miles
from .projections import TransverseMercator
from ..point_with_heading import PointWithHeading Numeric = Union[int, float]
from ..profiling import logged_duration
from ..scenery_group import SceneryGroup
from ..utils import Distance, meters
SIZE_TINY = 150 SIZE_TINY = 150
SIZE_SMALL = 600 SIZE_SMALL = 600
@@ -70,6 +69,18 @@ IMPORTANCE_LOW = 1
IMPORTANCE_MEDIUM = 1.2 IMPORTANCE_MEDIUM = 1.2
IMPORTANCE_HIGH = 1.4 IMPORTANCE_HIGH = 1.4
FRONTLINE_MIN_CP_DISTANCE = 5000
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
class MizCampaignLoader: class MizCampaignLoader:
BLUE_COUNTRY = CombinedJointTaskForcesBlue() BLUE_COUNTRY = CombinedJointTaskForcesBlue()
@@ -80,58 +91,37 @@ class MizCampaignLoader:
CV_UNIT_TYPE = CVN_74_John_C__Stennis.id CV_UNIT_TYPE = CVN_74_John_C__Stennis.id
LHA_UNIT_TYPE = LHA_1_Tarawa.id LHA_UNIT_TYPE = LHA_1_Tarawa.id
FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id FRONT_LINE_UNIT_TYPE = Armor.APC_M113.id
SHIPPING_LANE_UNIT_TYPE = Bulker_Handy_Wind.id
FOB_UNIT_TYPE = Unarmed.Truck_SKP_11_Mobile_ATC.id FOB_UNIT_TYPE = Unarmed.CP_SKP_11_ATC_Mobile_Command_Post.id
FARP_HELIPAD = "SINGLE_HELIPAD"
EWR_UNIT_TYPE = AirDefence.EWR_55G6.id
SAM_UNIT_TYPE = AirDefence.SAM_SA_10_S_300PS_SR_64H6E.id
GARRISON_UNIT_TYPE = AirDefence.SAM_SA_19_Tunguska_2S6.id
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
SHIP_UNIT_TYPE = DDG_Arleigh_Burke_IIa.id SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
MISSILE_SITE_UNIT_TYPE = MissilesSS.SSM_SS_1C_Scud_B.id MISSILE_SITE_UNIT_TYPE = MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M.id
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.AShM_SS_N_2_Silkworm.id COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.SS_N_2_Silkworm.id
# Multiple options for air defenses so campaign designers can more accurately see # Multiple options for the required SAMs so campaign designers can more
# the coverage of their IADS for the expected type. # accurately see the coverage of their IADS for the expected type.
LONG_RANGE_SAM_UNIT_TYPES = { REQUIRED_LONG_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Patriot_LN.id, AirDefence.SAM_Patriot_LN_M901.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_C.id, AirDefence.SAM_SA_10_S_300PS_LN_5P85C.id,
AirDefence.SAM_SA_10_S_300_Grumble_TEL_D.id, AirDefence.SAM_SA_10_S_300PS_LN_5P85D.id,
} }
MEDIUM_RANGE_SAM_UNIT_TYPES = { REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES = {
AirDefence.SAM_Hawk_LN_M192.id, AirDefence.SAM_Hawk_LN_M192.id,
AirDefence.SAM_SA_2_S_75_Guideline_LN.id, AirDefence.SAM_SA_2_LN_SM_90.id,
AirDefence.SAM_SA_3_S_125_Goa_LN.id, AirDefence.SAM_SA_3_S_125_LN_5P73.id,
} }
SHORT_RANGE_SAM_UNIT_TYPES = { BASE_DEFENSE_RADIUS = nautical_miles(2)
AirDefence.SAM_Avenger__Stinger.id,
AirDefence.SAM_Rapier_LN.id,
AirDefence.SAM_SA_19_Tunguska_Grison.id,
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL.id,
}
AAA_UNIT_TYPES = {
AirDefence.AAA_8_8cm_Flak_18.id,
AirDefence.SPAAA_Vulcan_M163.id,
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish.id,
}
EWR_UNIT_TYPE = AirDefence.EWR_1L13.id
ARMOR_GROUP_UNIT_TYPE = Armor.MBT_M1A2_Abrams.id
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse.Ammunition_depot.id
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
def __init__(self, miz: Path, theater: ConflictTheater) -> None: def __init__(self, miz: Path, theater: ConflictTheater) -> None:
self.theater = theater self.theater = theater
self.mission = Mission() self.mission = Mission()
with logged_duration("Loading miz"): self.mission.load_file(str(miz))
self.mission.load_file(str(miz))
self.control_point_id = itertools.count(1000) self.control_point_id = itertools.count(1000)
# If there are no red carriers there usually aren't red units. Make sure # If there are no red carriers there usually aren't red units. Make sure
@@ -203,92 +193,58 @@ class MizCampaignLoader:
@property @property
def ships(self) -> Iterator[ShipGroup]: def ships(self) -> Iterator[ShipGroup]:
for group in self.red.ship_group: for group in self.blue.ship_group:
if group.units[0].type == self.SHIP_UNIT_TYPE: if group.units[0].type == self.SHIP_UNIT_TYPE:
yield group yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.EWR_UNIT_TYPE:
yield group
@property
def sams(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.SAM_UNIT_TYPE:
yield group
@property
def garrisons(self) -> Iterator[VehicleGroup]:
for group in self.blue.vehicle_group:
if group.units[0].type == self.GARRISON_UNIT_TYPE:
yield group
@property @property
def offshore_strike_targets(self) -> Iterator[StaticGroup]: def offshore_strike_targets(self) -> Iterator[StaticGroup]:
for group in self.red.static_group: for group in self.blue.static_group:
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE: if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
yield group yield group
@property @property
def missile_sites(self) -> Iterator[VehicleGroup]: def missile_sites(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.blue.vehicle_group:
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE: if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
yield group yield group
@property @property
def coastal_defenses(self) -> Iterator[VehicleGroup]: def coastal_defenses(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.blue.vehicle_group:
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE: if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
yield group yield group
@property @property
def long_range_sams(self) -> Iterator[VehicleGroup]: def required_long_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES: if group.units[0].type in self.REQUIRED_LONG_RANGE_SAM_UNIT_TYPES:
yield group yield group
@property @property
def medium_range_sams(self) -> Iterator[VehicleGroup]: def required_medium_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group: for group in self.red.vehicle_group:
if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES: if group.units[0].type in self.REQUIRED_MEDIUM_RANGE_SAM_UNIT_TYPES:
yield group yield group
@property
def short_range_sams(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES:
yield group
@property
def aaa(self) -> Iterator[VehicleGroup]:
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
if group.units[0].type in self.AAA_UNIT_TYPES:
yield group
@property
def ewrs(self) -> Iterator[VehicleGroup]:
for group in self.red.vehicle_group:
if group.units[0].type in self.EWR_UNIT_TYPE:
yield group
@property
def armor_groups(self) -> Iterator[VehicleGroup]:
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE:
yield group
@property
def helipads(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type == self.FARP_HELIPAD:
yield group
@property
def factories(self) -> Iterator[StaticGroup]:
for group in self.blue.static_group:
if group.units[0].type in self.FACTORY_UNIT_TYPE:
yield group
@property
def ammunition_depots(self) -> Iterator[StaticGroup]:
for group in itertools.chain(self.blue.static_group, self.red.static_group):
if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE:
yield group
@property
def strike_targets(self) -> Iterator[StaticGroup]:
for group in itertools.chain(self.blue.static_group, self.red.static_group):
if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE:
yield group
@property
def scenery(self) -> List[SceneryGroup]:
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
@cached_property @cached_property
def control_points(self) -> Dict[int, ControlPoint]: def control_points(self) -> Dict[int, ControlPoint]:
control_points = {} control_points = {}
@@ -314,7 +270,7 @@ class MizCampaignLoader:
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
control_points[control_point.id] = control_point control_points[control_point.id] = control_point
for group in self.lhas(blue): for group in self.lhas(blue):
# TODO: Name the LHA.db # TODO: Name the LHA.
control_point = Lha("lha", group.position, next(self.control_point_id)) control_point = Lha("lha", group.position, next(self.control_point_id))
control_point.captured = blue control_point.captured = blue
control_point.captured_invert = group.late_activation control_point.captured_invert = group.late_activation
@@ -335,17 +291,14 @@ class MizCampaignLoader:
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE: if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
yield group yield group
@property @cached_property
def shipping_lane_groups(self) -> Iterator[ShipGroup]: def front_lines(self) -> Dict[str, ComplexFrontLine]:
for group in self.country(blue=True).ship_group: # Dict of front line ID to a front line.
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE: front_lines = {}
yield group
def add_supply_routes(self) -> None:
for group in self.front_line_path_groups: for group in self.front_line_path_groups:
# The unit will have its first waypoint at the source CP and the final # The unit will have its first waypoint at the source CP and the
# waypoint at the destination CP. Each waypoint defines the path of the # final waypoint at the destination CP. Intermediate waypoints
# cargo ship. # define the curve of the front line.
waypoints = [p.position for p in group.points] waypoints = [p.position for p in group.points]
origin = self.theater.closest_control_point(waypoints[0]) origin = self.theater.closest_control_point(waypoints[0])
if origin is None: if origin is None:
@@ -358,32 +311,14 @@ class MizCampaignLoader:
f"No control point near the final waypoint of {group.name}" f"No control point near the final waypoint of {group.name}"
) )
self.control_points[origin.id].create_convoy_route(destination, waypoints) # Snap the begin and end points to the control points.
self.control_points[destination.id].create_convoy_route( waypoints[0] = origin.position
origin, list(reversed(waypoints)) waypoints[-1] = destination.position
) front_line_id = f"{origin.id}|{destination.id}"
front_lines[front_line_id] = ComplexFrontLine(origin, waypoints)
def add_shipping_lanes(self) -> None: self.control_points[origin.id].connect(self.control_points[destination.id])
for group in self.shipping_lane_groups: self.control_points[destination.id].connect(self.control_points[origin.id])
# The unit will have its first waypoint at the source CP and the final return front_lines
# waypoint at the destination CP. Each waypoint defines the path of the
# cargo ship.
waypoints = [p.position for p in group.points]
origin = self.theater.closest_control_point(waypoints[0])
if origin is None:
raise RuntimeError(
f"No control point near the first waypoint of {group.name}"
)
destination = self.theater.closest_control_point(waypoints[-1])
if destination is None:
raise RuntimeError(
f"No control point near the final waypoint of {group.name}"
)
self.control_points[origin.id].create_shipping_lane(destination, waypoints)
self.control_points[destination.id].create_shipping_lane(
origin, list(reversed(waypoints))
)
def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]: def objective_info(self, group: Group) -> Tuple[ControlPoint, Distance]:
closest = self.theater.closest_control_point(group.position) closest = self.theater.closest_control_point(group.position)
@@ -391,100 +326,53 @@ class MizCampaignLoader:
return closest, distance return closest, distance
def add_preset_locations(self) -> None: def add_preset_locations(self) -> None:
for group in self.offshore_strike_targets: for group in self.garrisons:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.offshore_strike_locations.append( if distance < self.BASE_DEFENSE_RADIUS:
PointWithHeading.from_point(group.position, group.units[0].heading) closest.preset_locations.base_garrisons.append(group.position)
) else:
logging.warning(f"Found garrison unit too far from base: {group.name}")
for group in self.ships: for group in self.sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ships.append( if distance < self.BASE_DEFENSE_RADIUS:
PointWithHeading.from_point(group.position, group.units[0].heading) closest.preset_locations.base_air_defense.append(group.position)
) else:
closest.preset_locations.strike_locations.append(group.position)
for group in self.missile_sites:
closest, distance = self.objective_info(group)
closest.preset_locations.missile_sites.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.coastal_defenses:
closest, distance = self.objective_info(group)
closest.preset_locations.coastal_defenses.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.long_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.long_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.medium_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.medium_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.short_range_sams:
closest, distance = self.objective_info(group)
closest.preset_locations.short_range_sams.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.aaa:
closest, distance = self.objective_info(group)
closest.preset_locations.aaa.append(
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ewrs: for group in self.ewrs:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ewrs.append( closest.preset_locations.ewrs.append(group.position)
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.armor_groups: for group in self.offshore_strike_targets:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.armor_groups.append( closest.preset_locations.offshore_strike_locations.append(group.position)
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.helipads: for group in self.ships:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.helipads.append( closest.preset_locations.ships.append(group.position)
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.factories: for group in self.missile_sites:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.factories.append( closest.preset_locations.missile_sites.append(group.position)
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.ammunition_depots: for group in self.coastal_defenses:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.ammunition_depots.append( closest.preset_locations.coastal_defenses.append(group.position)
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.strike_targets: for group in self.required_long_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.strike_locations.append( closest.preset_locations.required_long_range_sams.append(group.position)
PointWithHeading.from_point(group.position, group.units[0].heading)
)
for group in self.scenery: for group in self.required_medium_range_sams:
closest, distance = self.objective_info(group) closest, distance = self.objective_info(group)
closest.preset_locations.scenery.append(group) closest.preset_locations.required_medium_range_sams.append(group.position)
def populate_theater(self) -> None: def populate_theater(self) -> None:
for control_point in self.control_points.values(): for control_point in self.control_points.values():
self.theater.add_controlpoint(control_point) self.theater.add_controlpoint(control_point)
self.add_preset_locations() self.add_preset_locations()
self.add_supply_routes() self.theater.set_frontline_data(self.front_lines)
self.add_shipping_lanes()
@dataclass @dataclass
@@ -503,40 +391,43 @@ class ConflictTheater:
land_poly = None # type: Polygon land_poly = None # type: Polygon
""" """
daytime_map: Dict[str, Tuple[int, int]] daytime_map: Dict[str, Tuple[int, int]]
_frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
def __init__(self): def __init__(self):
self.controlpoints: List[ControlPoint] = [] self.controlpoints: List[ControlPoint] = []
self.point_to_ll_transformer = Transformer.from_crs( self._frontline_data: Optional[Dict[str, ComplexFrontLine]] = None
self.projection_parameters.to_crs(), CRS("WGS84")
)
self.ll_to_point_transformer = Transformer.from_crs(
CRS("WGS84"), self.projection_parameters.to_crs()
)
""" """
self.land_poly = geometry.Polygon(self.landmap[0][0]) self.land_poly = geometry.Polygon(self.landmap[0][0])
for x in self.landmap[1]: for x in self.landmap[1]:
self.land_poly = self.land_poly.difference(geometry.Polygon(x)) self.land_poly = self.land_poly.difference(geometry.Polygon(x))
""" """
def __getstate__(self) -> Dict[str, Any]: @property
state = self.__dict__.copy() def frontline_data(self) -> Optional[Dict[str, ComplexFrontLine]]:
# Avoid persisting any volatile types that can be deterministically if self._frontline_data is None:
# recomputed on load for the sake of save compatibility. self.load_frontline_data_from_file()
del state["point_to_ll_transformer"] return self._frontline_data
del state["ll_to_point_transformer"]
return state
def __setstate__(self, state: Dict[str, Any]) -> None: def load_frontline_data_from_file(self) -> None:
self.__dict__.update(state) if self._frontline_data is not None:
# Regenerate any state that was not persisted. logging.warning("Replacing existing frontline data from file")
self.point_to_ll_transformer = Transformer.from_crs( self._frontline_data = FrontLine.load_json_frontlines(self)
self.projection_parameters.to_crs(), CRS("WGS84") if self._frontline_data is None:
) self._frontline_data = {}
self.ll_to_point_transformer = Transformer.from_crs(
CRS("WGS84"), self.projection_parameters.to_crs() def set_frontline_data(self, data: Dict[str, ComplexFrontLine]) -> None:
) if self._frontline_data is not None:
logging.warning("Replacing existing frontline data")
self._frontline_data = data
def add_controlpoint(
self, point: ControlPoint, connected_to: Optional[List[ControlPoint]] = None
):
if connected_to is None:
connected_to = []
for connected_point in connected_to:
point.connect(to=connected_point)
def add_controlpoint(self, point: ControlPoint):
self.controlpoints.append(point) self.controlpoints.append(point)
def find_ground_objects_by_obj_name(self, obj_name): def find_ground_objects_by_obj_name(self, obj_name):
@@ -617,12 +508,12 @@ class ConflictTheater:
def player_points(self) -> List[ControlPoint]: def player_points(self) -> List[ControlPoint]:
return list(self.control_points_for(player=True)) return list(self.control_points_for(player=True))
def conflicts(self) -> Iterator[FrontLine]: def conflicts(self, from_player=True) -> Iterator[FrontLine]:
for player_cp in [x for x in self.controlpoints if x.captured]: for cp in [x for x in self.controlpoints if x.captured == from_player]:
for enemy_cp in [ for connected_point in [
x for x in player_cp.connected_points if not x.is_friendly_to(player_cp) x for x in cp.connected_points if x.captured != from_player
]: ]:
yield FrontLine(player_cp, enemy_cp) yield FrontLine(cp, connected_point, self)
def enemy_points(self) -> List[ControlPoint]: def enemy_points(self) -> List[ControlPoint]:
return list(self.control_points_for(player=False)) return list(self.control_points_for(player=False))
@@ -662,32 +553,71 @@ class ConflictTheater:
Returns a tuple of the two nearest opposing ControlPoints in theater. Returns a tuple of the two nearest opposing ControlPoints in theater.
(player_cp, enemy_cp) (player_cp, enemy_cp)
""" """
seen = set() all_cp_min_distances = {}
min_distance = math.inf for idx, control_point in enumerate(self.controlpoints):
closest_blue = None distances = {}
closest_red = None closest_distance = None
for blue_cp in self.player_points(): for i, cp in enumerate(self.controlpoints):
for red_cp in self.enemy_points(): if i != idx and cp.captured is not control_point.captured:
if (blue_cp, red_cp) in seen: dist = cp.position.distance_to_point(control_point.position)
continue if not closest_distance:
seen.add((blue_cp, red_cp)) closest_distance = dist
seen.add((red_cp, blue_cp)) distances[cp.id] = dist
if dist < closest_distance:
distances[cp.id] = dist
closest_cp_id = min(distances, key=distances.get) # type: ignore
dist = red_cp.position.distance_to_point(blue_cp.position) all_cp_min_distances[(control_point.id, closest_cp_id)] = distances[
if dist < min_distance: closest_cp_id
closest_red = red_cp ]
closest_blue = blue_cp closest_opposing_cps = [
min_distance = dist self.find_control_point_by_id(i)
for i in min(
assert closest_blue is not None all_cp_min_distances, key=all_cp_min_distances.get
assert closest_red is not None ) # type: ignore
return closest_blue, closest_red ] # type: List[ControlPoint]
assert len(closest_opposing_cps) == 2
if closest_opposing_cps[0].captured:
return cast(Tuple[ControlPoint, ControlPoint], tuple(closest_opposing_cps))
else:
return cast(
Tuple[ControlPoint, ControlPoint], tuple(reversed(closest_opposing_cps))
)
def find_control_point_by_id(self, id: int) -> ControlPoint: def find_control_point_by_id(self, id: int) -> ControlPoint:
for i in self.controlpoints: for i in self.controlpoints:
if i.id == id: if i.id == id:
return i return i
raise KeyError(f"Cannot find ControlPoint with ID {id}") raise RuntimeError(f"Cannot find ControlPoint with ID {id}")
def add_json_cp(self, theater, p: dict) -> ControlPoint:
cp: ControlPoint
if p["type"] == "airbase":
airbase = theater.terrain.airports[p["id"]]
if "size" in p.keys():
size = p["size"]
else:
size = SIZE_REGULAR
if "importance" in p.keys():
importance = p["importance"]
else:
importance = IMPORTANCE_MEDIUM
cp = Airfield(airbase, size, importance)
elif p["type"] == "carrier":
cp = Carrier("carrier", Point(p["x"], p["y"]), p["id"])
else:
cp = Lha("lha", Point(p["x"], p["y"]), p["id"])
if "captured_invert" in p.keys():
cp.captured_invert = p["captured_invert"]
else:
cp.captured_invert = False
return cp
@staticmethod @staticmethod
def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater: def from_json(directory: Path, data: Dict[str, Any]) -> ConflictTheater:
@@ -703,27 +633,28 @@ class ConflictTheater:
t = theater() t = theater()
miz = data.get("miz", None) miz = data.get("miz", None)
if miz is None: if miz is not None:
raise RuntimeError(
"Old format (non-miz) campaigns are no longer supported."
)
with logged_duration("Importing miz data"):
MizCampaignLoader(directory / miz, t).populate_theater() MizCampaignLoader(directory / miz, t).populate_theater()
return t
cps = {}
for p in data["player_points"]:
cp = t.add_json_cp(theater, p)
cp.captured = True
cps[p["id"]] = cp
t.add_controlpoint(cp)
for p in data["enemy_points"]:
cp = t.add_json_cp(theater, p)
cps[p["id"]] = cp
t.add_controlpoint(cp)
for l in data["links"]:
cps[l[0]].connect(cps[l[1]])
cps[l[1]].connect(cps[l[0]])
return t return t
@property
def projection_parameters(self) -> TransverseMercator:
raise NotImplementedError
def point_to_ll(self, point: Point) -> LatLon:
lat, lon = self.point_to_ll_transformer.transform(point.x, point.y)
return LatLon(lat, lon)
def ll_to_point(self, ll: LatLon) -> Point:
x, y = self.ll_to_point_transformer.transform(ll.latitude, ll.longitude)
return Point(x, y)
class CaucasusTheater(ConflictTheater): class CaucasusTheater(ConflictTheater):
terrain = caucasus.Caucasus() terrain = caucasus.Caucasus()
@@ -741,19 +672,13 @@ class CaucasusTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
@property
def projection_parameters(self) -> TransverseMercator:
from .caucasus import PARAMETERS
return PARAMETERS
class PersianGulfTheater(ConflictTheater): class PersianGulfTheater(ConflictTheater):
terrain = persiangulf.PersianGulf() terrain = persiangulf.PersianGulf()
overview_image = "persiangulf.gif" overview_image = "persiangulf.gif"
reference_points = ( reference_points = (
ReferencePoint(persiangulf.Jiroft.position, Point(1692, 1343)), ReferencePoint(persiangulf.Jiroft_Airport.position, Point(1692, 1343)),
ReferencePoint(persiangulf.Liwa_AFB.position, Point(358, 3238)), ReferencePoint(persiangulf.Liwa_Airbase.position, Point(358, 3238)),
) )
landmap = load_landmap("resources\\gulflandmap.p") landmap = load_landmap("resources\\gulflandmap.p")
daytime_map = { daytime_map = {
@@ -763,12 +688,6 @@ class PersianGulfTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
@property
def projection_parameters(self) -> TransverseMercator:
from .persiangulf import PARAMETERS
return PARAMETERS
class NevadaTheater(ConflictTheater): class NevadaTheater(ConflictTheater):
terrain = nevada.Nevada() terrain = nevada.Nevada()
@@ -785,12 +704,6 @@ class NevadaTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
@property
def projection_parameters(self) -> TransverseMercator:
from .nevada import PARAMETERS
return PARAMETERS
class NormandyTheater(ConflictTheater): class NormandyTheater(ConflictTheater):
terrain = normandy.Normandy() terrain = normandy.Normandy()
@@ -807,12 +720,6 @@ class NormandyTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
@property
def projection_parameters(self) -> TransverseMercator:
from .normandy import PARAMETERS
return PARAMETERS
class TheChannelTheater(ConflictTheater): class TheChannelTheater(ConflictTheater):
terrain = thechannel.TheChannel() terrain = thechannel.TheChannel()
@@ -829,12 +736,6 @@ class TheChannelTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
@property
def projection_parameters(self) -> TransverseMercator:
from .thechannel import PARAMETERS
return PARAMETERS
class SyriaTheater(ConflictTheater): class SyriaTheater(ConflictTheater):
terrain = syria.Syria() terrain = syria.Syria()
@@ -851,8 +752,212 @@ class SyriaTheater(ConflictTheater):
"night": (0, 5), "night": (0, 5),
} }
@property
def projection_parameters(self) -> TransverseMercator:
from .syria import PARAMETERS
return PARAMETERS @dataclass
class ComplexFrontLine:
"""
Stores data necessary for building a multi-segment frontline.
"points" should be ordered from closest to farthest distance originating from start_cp.position
"""
start_cp: ControlPoint
points: List[Point]
@dataclass
class FrontLineSegment:
"""
Describes a line segment of a FrontLine
"""
point_a: Point
point_b: Point
@property
def attack_heading(self) -> Numeric:
"""The heading of the frontline segment from player to enemy control point"""
return self.point_a.heading_between_point(self.point_b)
@property
def attack_distance(self) -> Numeric:
"""Length of the segment"""
return self.point_a.distance_to_point(self.point_b)
class FrontLine(MissionTarget):
"""Defines a front line location between two control points.
Front lines are the area where ground combat happens.
Overwrites the entirety of MissionTarget __init__ method to allow for
dynamic position calculation.
"""
def __init__(
self,
control_point_a: ControlPoint,
control_point_b: ControlPoint,
theater: ConflictTheater,
) -> None:
self.control_point_a = control_point_a
self.control_point_b = control_point_b
self.segments: List[FrontLineSegment] = []
self.theater = theater
self._build_segments()
self.name = f"Front line {control_point_a}/{control_point_b}"
def is_friendly(self, to_player: bool) -> bool:
"""Returns True if the objective is in friendly territory."""
return False
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
yield from [
FlightType.CAS,
# TODO: FlightType.TROOP_TRANSPORT
# TODO: FlightType.EVAC
]
yield from super().mission_types(for_player)
@property
def position(self):
"""
The position where the conflict should occur
according to the current strength of each control point.
"""
return self.point_from_a(self._position_distance)
@property
def control_points(self) -> Tuple[ControlPoint, ControlPoint]:
"""Returns a tuple of the two control points."""
return self.control_point_a, self.control_point_b
@property
def attack_distance(self):
"""The total distance of all segments"""
return sum(i.attack_distance for i in self.segments)
@property
def attack_heading(self):
"""The heading of the active attack segment from player to enemy control point"""
return self.active_segment.attack_heading
@property
def active_segment(self) -> FrontLineSegment:
"""The FrontLine segment where there can be an active conflict"""
if self._position_distance <= self.segments[0].attack_distance:
return self.segments[0]
remaining_dist = self._position_distance
for segment in self.segments:
if remaining_dist <= segment.attack_distance:
return segment
else:
remaining_dist -= segment.attack_distance
logging.error(
"Frontline attack distance is greater than the sum of its segments"
)
return self.segments[0]
def point_from_a(self, distance: Numeric) -> Point:
"""
Returns a point {distance} away from control_point_a along the frontline segments.
"""
if distance < self.segments[0].attack_distance:
return self.control_point_a.position.point_from_heading(
self.segments[0].attack_heading, distance
)
remaining_dist = distance
for segment in self.segments:
if remaining_dist < segment.attack_distance:
return segment.point_a.point_from_heading(
segment.attack_heading, remaining_dist
)
else:
remaining_dist -= segment.attack_distance
@property
def _position_distance(self) -> float:
"""
The distance from point "a" where the conflict should occur
according to the current strength of each control point
"""
total_strength = (
self.control_point_a.base.strength + self.control_point_b.base.strength
)
if self.control_point_a.base.strength == 0:
return self._adjust_for_min_dist(0)
if self.control_point_b.base.strength == 0:
return self._adjust_for_min_dist(self.attack_distance)
strength_pct = self.control_point_a.base.strength / total_strength
return self._adjust_for_min_dist(strength_pct * self.attack_distance)
def _adjust_for_min_dist(self, distance: Numeric) -> Numeric:
"""
Ensures the frontline conflict is never located within the minimum distance
constant of either end control point.
"""
if (distance > self.attack_distance / 2) and (
distance + FRONTLINE_MIN_CP_DISTANCE > self.attack_distance
):
distance = self.attack_distance - FRONTLINE_MIN_CP_DISTANCE
elif (distance < self.attack_distance / 2) and (
distance < FRONTLINE_MIN_CP_DISTANCE
):
distance = FRONTLINE_MIN_CP_DISTANCE
return distance
def _build_segments(self) -> None:
"""Create line segments for the frontline"""
control_point_ids = "|".join(
[str(self.control_point_a.id), str(self.control_point_b.id)]
) # from_cp.id|to_cp.id
reversed_cp_ids = "|".join(
[str(self.control_point_b.id), str(self.control_point_a.id)]
)
complex_frontlines = self.theater.frontline_data
if (complex_frontlines) and (
(control_point_ids in complex_frontlines)
or (reversed_cp_ids in complex_frontlines)
):
# The frontline segments must be stored in the correct order for the distance algorithms to work.
# The points in the frontline are ordered from the id before the | to the id after.
# First, check if control point id pair matches in order, and create segments if a match is found.
if control_point_ids in complex_frontlines:
point_pairs = pairwise(complex_frontlines[control_point_ids].points)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# Check the reverse order and build in reverse if found.
elif reversed_cp_ids in complex_frontlines:
point_pairs = pairwise(
reversed(complex_frontlines[reversed_cp_ids].points)
)
for i in point_pairs:
self.segments.append(FrontLineSegment(i[0], i[1]))
# If no complex frontline has been configured, fall back to the old straight line method.
else:
self.segments.append(
FrontLineSegment(
self.control_point_a.position, self.control_point_b.position
)
)
@staticmethod
def load_json_frontlines(
theater: ConflictTheater,
) -> Optional[Dict[str, ComplexFrontLine]]:
"""Load complex frontlines from json"""
try:
path = Path(f"resources/frontlines/{theater.terrain.name.lower()}.json")
with open(path, "r") as file:
logging.debug(f"Loading frontline from {path}...")
data = json.load(file)
return {
frontline: ComplexFrontLine(
data[frontline]["start_cp"],
[Point(i[0], i[1]) for i in data[frontline]["points"]],
)
for frontline in data
}
except OSError:
logging.warning(
f"Unable to load preset frontlines for {theater.terrain.name}"
)
return None

View File

@@ -3,25 +3,13 @@ from __future__ import annotations
import heapq import heapq
import itertools import itertools
import logging import logging
import random
import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum, unique, auto, IntEnum from enum import Enum
from functools import total_ordering, cached_property from functools import total_ordering
from typing import ( from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
Any,
Dict,
Iterator,
List,
Optional,
Set,
TYPE_CHECKING,
Type,
Union,
Sequence,
Iterable,
Tuple,
)
from dcs.mapping import Point from dcs.mapping import Point
from dcs.ships import ( from dcs.ships import (
@@ -31,20 +19,22 @@ from dcs.ships import (
Type_071_Amphibious_Transport_Dock, Type_071_Amphibious_Transport_Dock,
) )
from dcs.terrain.terrain import Airport, ParkingSlot from dcs.terrain.terrain import Airport, ParkingSlot
from dcs.unit import Unit from dcs.unittype import FlyingType
from dcs.unittype import FlyingType, VehicleType
from game import db from game import db
from game.point_with_heading import PointWithHeading
from game.scenery_group import SceneryGroup
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner_db import TYPE_SHORAD
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
from gen.runways import RunwayAssigner, RunwayData from gen.runways import RunwayAssigner, RunwayData
from .base import Base from .base import Base
from .missiontarget import MissionTarget from .missiontarget import MissionTarget
from .theatergroundobject import ( from .theatergroundobject import (
BaseDefenseGroundObject,
EwrGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
SamGroundObject,
TheaterGroundObject, TheaterGroundObject,
VehicleGroupGroundObject,
) )
from ..db import PRICES from ..db import PRICES
from ..utils import nautical_miles from ..utils import nautical_miles
@@ -53,10 +43,6 @@ from ..weather import Conditions
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from ..transfers import PendingTransfers
FREE_FRONTLINE_UNIT_SUPPLY: int = 15
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION: int = 12
class ControlPointType(Enum): class ControlPointType(Enum):
@@ -73,133 +59,102 @@ class ControlPointType(Enum):
OFF_MAP = 6 OFF_MAP = 6
class LocationType(Enum):
BaseAirDefense = "base air defense"
Coastal = "coastal defense"
Ewr = "EWR"
Garrison = "garrison"
MissileSite = "missile site"
OffshoreStrikeTarget = "offshore strike target"
Sam = "SAM"
Ship = "ship"
Shorad = "SHORAD"
StrikeTarget = "strike target"
@dataclass @dataclass
class PresetLocations: class PresetLocations:
"""Defines the preset locations loaded from the campaign mission file.""" """Defines the preset locations loaded from the campaign mission file."""
#: Locations used by non-carrier ships that will be spawned unless the faction has #: Locations used for spawning ground defenses for bases.
#: no navy or the player has disabled ship generation for the owning side. base_garrisons: List[Point] = field(default_factory=list)
ships: List[PointWithHeading] = field(default_factory=list)
#: Locations used by coastal defenses that are generated if the faction is capable. #: Locations used for spawning air defenses for bases. Used by SAMs, AAA,
coastal_defenses: List[PointWithHeading] = field(default_factory=list) #: and SHORADs.
base_air_defense: List[Point] = field(default_factory=list)
#: Locations used by EWRs.
ewrs: List[Point] = field(default_factory=list)
#: Locations used by non-carrier ships. Carriers and LHAs are not random.
ships: List[Point] = field(default_factory=list)
#: Locations used by coastal defenses.
coastal_defenses: List[Point] = field(default_factory=list)
#: Locations used by ground based strike objectives. #: Locations used by ground based strike objectives.
strike_locations: List[PointWithHeading] = field(default_factory=list) strike_locations: List[Point] = field(default_factory=list)
#: Locations used by offshore strike objectives. #: Locations used by offshore strike objectives.
offshore_strike_locations: List[PointWithHeading] = field(default_factory=list) offshore_strike_locations: List[Point] = field(default_factory=list)
#: Locations used by missile sites like scuds and V-2s that are generated if the #: Locations used by missile sites like scuds and V-2s.
#: faction is capable. missile_sites: List[Point] = field(default_factory=list)
missile_sites: List[PointWithHeading] = field(default_factory=list)
#: Locations of long range SAMs. #: Locations of long range SAMs which should always be spawned.
long_range_sams: List[PointWithHeading] = field(default_factory=list) required_long_range_sams: List[Point] = field(default_factory=list)
#: Locations of medium range SAMs. #: Locations of medium range SAMs which should always be spawned.
medium_range_sams: List[PointWithHeading] = field(default_factory=list) required_medium_range_sams: List[Point] = field(default_factory=list)
#: Locations of short range SAMs. @staticmethod
short_range_sams: List[PointWithHeading] = field(default_factory=list) def _random_from(points: List[Point]) -> Optional[Point]:
"""Finds, removes, and returns a random position from the given list."""
if not points:
return None
point = random.choice(points)
points.remove(point)
return point
#: Locations of AAA groups. def random_for(self, location_type: LocationType) -> Optional[Point]:
aaa: List[PointWithHeading] = field(default_factory=list) """Returns a position suitable for the given location type.
#: Locations of EWRs. The location, if found, will be claimed by the caller and not available
ewrs: List[PointWithHeading] = field(default_factory=list) to subsequent calls.
"""
#: Locations of map scenery to create zones for. if location_type == LocationType.BaseAirDefense:
scenery: List[SceneryGroup] = field(default_factory=list) return self._random_from(self.base_air_defense)
if location_type == LocationType.Coastal:
#: Locations of factories for producing ground units. return self._random_from(self.coastal_defenses)
factories: List[PointWithHeading] = field(default_factory=list) if location_type == LocationType.Ewr:
return self._random_from(self.ewrs)
#: Locations of ammo depots for controlling number of units on the front line at a if location_type == LocationType.Garrison:
#: control point. return self._random_from(self.base_garrisons)
ammunition_depots: List[PointWithHeading] = field(default_factory=list) if location_type == LocationType.MissileSite:
return self._random_from(self.missile_sites)
#: Locations of stationary armor groups. if location_type == LocationType.OffshoreStrikeTarget:
armor_groups: List[PointWithHeading] = field(default_factory=list) return self._random_from(self.offshore_strike_locations)
if location_type == LocationType.Sam:
return self._random_from(self.strike_locations)
if location_type == LocationType.Ship:
return self._random_from(self.ships)
if location_type == LocationType.Shorad:
return self._random_from(self.base_garrisons)
if location_type == LocationType.StrikeTarget:
return self._random_from(self.strike_locations)
logging.error(f"Unknown location type: {location_type}")
return None
@dataclass(frozen=True) @dataclass(frozen=True)
class AircraftAllocations: class PendingOccupancy:
present: dict[Type[FlyingType], int] present: int
ordered: dict[Type[FlyingType], int] ordered: int
transferring: dict[Type[FlyingType], int] transferring: int
@property
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
total += PRICES[unit_type] * count
for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count
for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count
return total
@property @property
def total(self) -> int: def total(self) -> int:
return self.total_present + self.total_ordered + self.total_transferring return self.present + self.ordered + self.transferring
@property
def total_present(self) -> int:
return sum(self.present.values())
@property
def total_ordered(self) -> int:
return sum(self.ordered.values())
@property
def total_transferring(self) -> int:
return sum(self.transferring.values())
@dataclass(frozen=True)
class GroundUnitAllocations:
present: dict[Type[VehicleType], int]
ordered: dict[Type[VehicleType], int]
transferring: dict[Type[VehicleType], int]
@property
def all(self) -> dict[Type[VehicleType], int]:
combined: dict[Type[VehicleType], int] = defaultdict(int)
for unit_type, count in itertools.chain(
self.present.items(), self.ordered.items(), self.transferring.items()
):
combined[unit_type] += count
return dict(combined)
@property
def total_value(self) -> int:
total: int = 0
for unit_type, count in self.present.items():
total += PRICES[unit_type] * count
for unit_type, count in self.ordered.items():
total += PRICES[unit_type] * count
for unit_type, count in self.transferring.items():
total += PRICES[unit_type] * count
return total
@cached_property
def total(self) -> int:
return self.total_present + self.total_ordered + self.total_transferring
@cached_property
def total_present(self) -> int:
return sum(self.present.values())
@cached_property
def total_ordered(self) -> int:
return sum(self.ordered.values())
@cached_property
def total_transferring(self) -> int:
return sum(self.transferring.values())
@dataclass @dataclass
@@ -263,13 +218,6 @@ class GroundUnitDestination:
return self.total_value < other.total_value return self.total_value < other.total_value
@unique
class ControlPointStatus(IntEnum):
Functional = auto()
Damaged = auto()
Destroyed = auto()
class ControlPoint(MissionTarget, ABC): class ControlPoint(MissionTarget, ABC):
position = None # type: Point position = None # type: Point
@@ -300,8 +248,8 @@ class ControlPoint(MissionTarget, ABC):
self.full_name = name self.full_name = name
self.at = at self.at = at
self.connected_objectives: List[TheaterGroundObject] = [] self.connected_objectives: List[TheaterGroundObject] = []
self.base_defenses: List[BaseDefenseGroundObject] = []
self.preset_locations = PresetLocations() self.preset_locations = PresetLocations()
self.helipads: List[PointWithHeading] = []
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.size = size self.size = size
@@ -311,15 +259,13 @@ class ControlPoint(MissionTarget, ABC):
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.has_frontline = has_frontline self.has_frontline = has_frontline
self.connected_points: List[ControlPoint] = [] self.connected_points: List[ControlPoint] = []
self.convoy_routes: Dict[ControlPoint, Tuple[Point, ...]] = {}
self.shipping_lanes: Dict[ControlPoint, Tuple[Point, ...]] = {}
self.base: Base = Base() self.base: Base = Base()
self.cptype = cptype self.cptype = cptype
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
self.stances: Dict[int, CombatStance] = {} self.stances: Dict[int, CombatStance] = {}
from ..unitdelivery import PendingUnitDeliveries from ..event import UnitsDeliveryEvent
self.pending_unit_deliveries = PendingUnitDeliveries(self) self.pending_unit_deliveries = UnitsDeliveryEvent(self)
self.target_position: Optional[Point] = None self.target_position: Optional[Point] = None
@@ -328,7 +274,7 @@ class ControlPoint(MissionTarget, ABC):
@property @property
def ground_objects(self) -> List[TheaterGroundObject]: def ground_objects(self) -> List[TheaterGroundObject]:
return list(self.connected_objectives) return list(itertools.chain(self.connected_objectives, self.base_defenses))
@property @property
@abstractmethod @abstractmethod
@@ -342,69 +288,6 @@ class ControlPoint(MissionTarget, ABC):
def is_global(self): def is_global(self):
return not self.connected_points return not self.connected_points
def transitive_connected_friendly_points(
self, seen: Optional[Set[ControlPoint]] = None
) -> List[ControlPoint]:
if seen is None:
seen = {self}
connected = []
for cp in self.connected_points:
if cp.captured != self.captured:
continue
if cp in seen:
continue
seen.add(cp)
connected.append(cp)
connected.extend(cp.transitive_connected_friendly_points(seen))
return connected
def transitive_friendly_shipping_destinations(
self, seen: Optional[Set[ControlPoint]] = None
) -> List[ControlPoint]:
if seen is None:
seen = {self}
connected = []
for cp in self.shipping_lanes:
if cp.captured != self.captured:
continue
if cp in seen:
continue
seen.add(cp)
connected.append(cp)
connected.extend(cp.transitive_friendly_shipping_destinations(seen))
return connected
@property
def has_factory(self) -> bool:
for tgo in self.connected_objectives:
if tgo.is_factory and not tgo.is_dead:
return True
return False
def can_recruit_ground_units(self, game: Game) -> bool:
"""Returns True if this control point is capable of recruiting ground units."""
if not self.can_deploy_ground_units:
return False
if game.turn == 0:
# Allow units to be recruited anywhere on turn 0 to avoid long delays to get
# everyone to the front line.
return True
return self.has_factory
def has_ground_unit_source(self, game: Game) -> bool:
"""Returns True if this control point has access to ground reinforcements."""
if not self.can_deploy_ground_units:
return False
for cp in game.theater.controlpoints:
if cp.is_friendly(self.captured) and cp.can_recruit_ground_units(game):
return True
return False
@property @property
def is_carrier(self): def is_carrier(self):
""" """
@@ -447,21 +330,10 @@ class ControlPoint(MissionTarget, ABC):
""" """
... ...
def convoy_origin_for(self, destination: ControlPoint) -> Point: # TODO: Should be Airbase specific.
return self.convoy_route_to(destination)[0] def connect(self, to: ControlPoint) -> None:
def convoy_route_to(self, destination: ControlPoint) -> Sequence[Point]:
return self.convoy_routes[destination]
def create_convoy_route(self, to: ControlPoint, waypoints: Iterable[Point]) -> None:
self.connected_points.append(to) self.connected_points.append(to)
self.stances[to.id] = CombatStance.DEFENSIVE self.stances[to.id] = CombatStance.DEFENSIVE
self.convoy_routes[to] = tuple(waypoints)
def create_shipping_lane(
self, to: ControlPoint, waypoints: Iterable[Point]
) -> None:
self.shipping_lanes[to] = tuple(waypoints)
@abstractmethod @abstractmethod
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
@@ -511,8 +383,22 @@ class ControlPoint(MissionTarget, ABC):
def is_friendly(self, to_player: bool) -> bool: def is_friendly(self, to_player: bool) -> bool:
return self.captured == to_player return self.captured == to_player
def is_friendly_to(self, control_point: ControlPoint) -> bool: # TODO: Should be Airbase specific.
return control_point.is_friendly(self.captured) def clear_base_defenses(self) -> None:
for base_defense in self.base_defenses:
if isinstance(base_defense, EwrGroundObject):
self.preset_locations.ewrs.append(base_defense.position)
elif isinstance(base_defense, SamGroundObject):
self.preset_locations.base_air_defense.append(base_defense.position)
elif isinstance(base_defense, VehicleGroupGroundObject):
self.preset_locations.base_garrisons.append(base_defense.position)
else:
logging.error(
"Could not determine preset location type for "
f"{base_defense}. Assuming garrison type."
)
self.preset_locations.base_garrisons.append(base_defense.position)
self.base_defenses = []
def capture_equipment(self, game: Game) -> None: def capture_equipment(self, game: Game) -> None:
total = self.base.total_armor_value total = self.base.total_armor_value
@@ -568,7 +454,7 @@ class ControlPoint(MissionTarget, ABC):
max_retreat_distance = nautical_miles(200) max_retreat_distance = nautical_miles(200)
# Skip the first airbase because that's the airbase we're retreating # Skip the first airbase because that's the airbase we're retreating
# from. # from.
airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] airfields = list(closest.airfields_within(max_retreat_distance))[1:]
for airbase in airfields: for airbase in airfields:
if not airbase.can_operate(airframe): if not airbase.can_operate(airframe):
continue continue
@@ -598,17 +484,11 @@ class ControlPoint(MissionTarget, ABC):
airframe, count = self.base.aircraft.popitem() airframe, count = self.base.aircraft.popitem()
self._retreat_air_units(game, airframe, count) self._retreat_air_units(game, airframe, count)
def depopulate_uncapturable_tgos(self) -> None:
for tgo in self.connected_objectives:
if not tgo.capturable:
tgo.clear()
# TODO: Should be Airbase specific. # TODO: Should be Airbase specific.
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
self.pending_unit_deliveries.refund_all(game) self.pending_unit_deliveries.refund_all(game)
self.retreat_ground_units(game) self.retreat_ground_units(game)
self.retreat_air_units(game) self.retreat_air_units(game)
self.depopulate_uncapturable_tgos()
if for_player: if for_player:
self.captured = True self.captured = True
@@ -617,29 +497,46 @@ class ControlPoint(MissionTarget, ABC):
self.base.set_strength_to_minimum() self.base.set_strength_to_minimum()
self.clear_base_defenses()
from .start_generator import BaseDefenseGenerator
BaseDefenseGenerator(game, self).generate()
@abstractmethod @abstractmethod
def can_operate(self, aircraft: Type[FlyingType]) -> bool: def can_operate(self, aircraft: Type[FlyingType]) -> bool:
... ...
def aircraft_transferring(self, game: Game) -> dict[Type[FlyingType], int]: def aircraft_transferring(self, game: Game) -> int:
if self.captured: if self.captured:
ato = game.blue_ato ato = game.blue_ato
else: else:
ato = game.red_ato ato = game.red_ato
transferring: defaultdict[Type[FlyingType], int] = defaultdict(int) total = 0
for package in ato.packages: for package in ato.packages:
for flight in package.flights: for flight in package.flights:
if flight.departure == flight.arrival: if flight.departure == flight.arrival:
continue continue
if flight.departure == self: if flight.departure == self:
transferring[flight.unit_type] -= flight.count total -= flight.count
elif flight.arrival == self: elif flight.arrival == self:
transferring[flight.unit_type] += flight.count total += flight.count
return transferring return total
def expected_aircraft_next_turn(self, game: Game) -> PendingOccupancy:
on_order = 0
for unit_bought in self.pending_unit_deliveries.units:
if issubclass(unit_bought, FlyingType):
on_order += self.pending_unit_deliveries.units[unit_bought]
return PendingOccupancy(
self.base.total_aircraft, on_order, self.aircraft_transferring(game)
)
def unclaimed_parking(self, game: Game) -> int: def unclaimed_parking(self, game: Game) -> int:
return self.total_aircraft_parking - self.allocated_aircraft(game).total return (
self.total_aircraft_parking - self.expected_aircraft_next_turn(game).total
)
@abstractmethod @abstractmethod
def active_runway( def active_runway(
@@ -689,34 +586,47 @@ class ControlPoint(MissionTarget, ABC):
u.position.x = u.position.x + delta.x u.position.x = u.position.x + delta.x
u.position.y = u.position.y + delta.y u.position.y = u.position.y + delta.y
def allocated_aircraft(self, game: Game) -> AircraftAllocations: @property
on_order = {} def pending_frontline_aa_deliveries_count(self):
for unit_bought, count in self.pending_unit_deliveries.units.items(): """
Get number of pending frontline aa units
"""
if self.pending_unit_deliveries:
return sum(
[
v
for k, v in self.pending_unit_deliveries.units.items()
if k in TYPE_SHORAD
]
)
else:
return 0
@property
def pending_deliveries_count(self):
"""
Get number of pending units
"""
if self.pending_unit_deliveries:
return sum([v for k, v in self.pending_unit_deliveries.units.items()])
else:
return 0
@property
def expected_ground_units_next_turn(self) -> PendingOccupancy:
on_order = 0
for unit_bought in self.pending_unit_deliveries.units:
if issubclass(unit_bought, FlyingType): if issubclass(unit_bought, FlyingType):
on_order[unit_bought] = count continue
if unit_bought in TYPE_SHORAD:
continue
on_order += self.pending_unit_deliveries.units[unit_bought]
return AircraftAllocations( return PendingOccupancy(
self.base.aircraft, on_order, self.aircraft_transferring(game) self.base.total_armor,
)
def allocated_ground_units(
self, transfers: PendingTransfers
) -> GroundUnitAllocations:
on_order = {}
for unit_bought, count in self.pending_unit_deliveries.units.items():
if issubclass(unit_bought, VehicleType):
on_order[unit_bought] = count
transferring: dict[Type[VehicleType], int] = defaultdict(int)
for transfer in transfers:
if transfer.destination == self:
for unit_type, count in transfer.units.items():
transferring[unit_type] += count
return GroundUnitAllocations(
self.base.armor,
on_order, on_order,
transferring, # Ground unit transfers not yet implemented.
transferring=0,
) )
@property @property
@@ -727,49 +637,6 @@ class ControlPoint(MissionTarget, ABC):
def has_active_frontline(self) -> bool: def has_active_frontline(self) -> bool:
return any(not c.is_friendly(self.captured) for c in self.connected_points) return any(not c.is_friendly(self.captured) for c in self.connected_points)
def front_is_active(self, other: ControlPoint) -> bool:
if other not in self.connected_points:
raise ValueError
return self.captured != other.captured
@property
def frontline_unit_count_limit(self) -> int:
return (
FREE_FRONTLINE_UNIT_SUPPLY
+ self.active_ammo_depots_count * AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION
)
@property
def active_ammo_depots_count(self) -> int:
"""Return the number of available ammo depots"""
return len(
[
obj
for obj in self.connected_objectives
if obj.category == "ammo" and not obj.is_dead
]
)
@property
def total_ammo_depots_count(self) -> int:
"""Return the number of ammo depots, including dead ones"""
return len([obj for obj in self.connected_objectives if obj.category == "ammo"])
@property
def strike_targets(self) -> List[Union[MissionTarget, Unit]]:
return []
@property
@abstractmethod
def category(self) -> str:
...
@property
@abstractmethod
def status(self) -> ControlPointStatus:
...
class Airfield(ControlPoint): class Airfield(ControlPoint):
def __init__( def __init__(
@@ -799,21 +666,18 @@ class Airfield(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if self.is_friendly(for_player):
yield from [
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
else:
yield from [ yield from [
FlightType.OCA_AIRCRAFT, FlightType.OCA_AIRCRAFT,
FlightType.OCA_RUNWAY, FlightType.OCA_RUNWAY,
] ]
yield from super().mission_types(for_player) yield from super().mission_types(for_player)
if self.is_friendly(for_player):
yield from [
FlightType.AEWC,
# TODO: FlightType.INTERCEPTION
# TODO: FlightType.LOGISTICS
]
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return len(self.airport.parking_slots) return len(self.airport.parking_slots)
@@ -850,19 +714,6 @@ class Airfield(ControlPoint):
def income_per_turn(self) -> int: def income_per_turn(self) -> int:
return 20 return 20
@property
def category(self) -> str:
return "airfield"
@property
def status(self) -> ControlPointStatus:
runway_staus = self.runway_status
if runway_staus.needs_repair:
return ControlPointStatus.Destroyed
elif runway_staus.damaged:
return ControlPointStatus.Damaged
return ControlPointStatus.Functional
class NavalControlPoint(ControlPoint, ABC): class NavalControlPoint(ControlPoint, ABC):
@property @property
@@ -887,24 +738,20 @@ class NavalControlPoint(ControlPoint, ABC):
def heading(self) -> int: def heading(self) -> int:
return 0 # TODO compute heading return 0 # TODO compute heading
def find_main_tgo(self) -> TheaterGroundObject:
for g in self.ground_objects:
if g.dcs_identifier in ["CARRIER", "LHA"]:
return g
raise RuntimeError(f"Found no carrier/LHA group for {self.name}")
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
# Necessary because it's possible for the carrier itself to have sunk # Necessary because it's possible for the carrier itself to have sunk
# while its escorts are still alive. # while its escorts are still alive.
for group in self.find_main_tgo().groups: for g in self.ground_objects:
for u in group.units: if g.dcs_identifier in ["CARRIER", "LHA"]:
if db.unit_type_from_name(u.type) in [ for group in g.groups:
CVN_74_John_C__Stennis, for u in group.units:
LHA_1_Tarawa, if db.unit_type_from_name(u.type) in [
CV_1143_5_Admiral_Kuznetsov, CVN_74_John_C__Stennis,
Type_071_Amphibious_Transport_Dock, LHA_1_Tarawa,
]: CV_1143_5_Admiral_Kuznetsov,
return True Type_071_Amphibious_Transport_Dock,
]:
return True
return False return False
def active_runway( def active_runway(
@@ -930,14 +777,6 @@ class NavalControlPoint(ControlPoint, ABC):
def can_deploy_ground_units(self) -> bool: def can_deploy_ground_units(self) -> bool:
return False return False
@property
def status(self) -> ControlPointStatus:
if not self.runway_is_operational():
return ControlPointStatus.Destroyed
if self.find_main_tgo().dead_units:
return ControlPointStatus.Damaged
return ControlPointStatus.Functional
class Carrier(NavalControlPoint): class Carrier(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
@@ -954,13 +793,6 @@ class Carrier(NavalControlPoint):
cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP,
) )
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType
yield from super().mission_types(for_player)
if self.is_friendly(for_player):
yield FlightType.AEWC
def capture(self, game: Game, for_player: bool) -> None: def capture(self, game: Game, for_player: bool) -> None:
raise RuntimeError("Carriers cannot be captured") raise RuntimeError("Carriers cannot be captured")
@@ -975,10 +807,6 @@ class Carrier(NavalControlPoint):
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 90 return 90
@property
def category(self) -> str:
return "cv"
class Lha(NavalControlPoint): class Lha(NavalControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
@@ -1009,10 +837,6 @@ class Lha(NavalControlPoint):
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
return 20 return 20
@property
def category(self) -> str:
return "lha"
class OffMapSpawn(ControlPoint): class OffMapSpawn(ControlPoint):
def runway_is_operational(self) -> bool: def runway_is_operational(self) -> bool:
@@ -1063,14 +887,6 @@ class OffMapSpawn(ControlPoint):
def can_deploy_ground_units(self) -> bool: def can_deploy_ground_units(self) -> bool:
return False return False
@property
def category(self) -> str:
return "offmap"
@property
def status(self) -> ControlPointStatus:
return ControlPointStatus.Functional
class Fob(ControlPoint): class Fob(ControlPoint):
def __init__(self, name: str, at: Point, cp_id: int): def __init__(self, name: str, at: Point, cp_id: int):
@@ -1104,10 +920,18 @@ class Fob(ControlPoint):
def mission_types(self, for_player: bool) -> Iterator[FlightType]: def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
if not self.is_friendly(for_player): if self.is_friendly(for_player):
yield FlightType.STRIKE yield from [
FlightType.BARCAP,
yield from super().mission_types(for_player) # TODO: FlightType.LOGISTICS
]
else:
yield from [
FlightType.STRIKE,
FlightType.SWEEP,
FlightType.ESCORT,
FlightType.SEAD,
]
@property @property
def total_aircraft_parking(self) -> int: def total_aircraft_parking(self) -> int:
@@ -1127,11 +951,3 @@ class Fob(ControlPoint):
@property @property
def income_per_turn(self) -> int: def income_per_turn(self) -> int:
return 10 return 10
@property
def category(self) -> str:
return "fob"
@property
def status(self) -> ControlPointStatus:
return ControlPointStatus.Functional

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from functools import singledispatchmethod from functools import singledispatchmethod
from typing import Optional, TYPE_CHECKING, Union, Iterable from typing import Optional, TYPE_CHECKING, Union
from dcs.mapping import Point as DcsPoint from dcs.mapping import Point as DcsPoint
from shapely.geometry import ( from shapely.geometry import (
@@ -13,10 +13,10 @@ from shapely.geometry import (
from shapely.geometry.base import BaseGeometry from shapely.geometry.base import BaseGeometry
from shapely.ops import nearest_points, unary_union from shapely.ops import nearest_points, unary_union
from game.theater import ControlPoint, MissionTarget from game.theater import ControlPoint
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from gen.flights.closestairfields import ObjectiveDistanceCache from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.flights.flight import Flight, FlightWaypoint from gen.flights.flight import Flight
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
@@ -26,12 +26,9 @@ ThreatPoly = Union[MultiPolygon, Polygon]
class ThreatZones: class ThreatZones:
def __init__( def __init__(self, airbases: ThreatPoly, air_defenses: ThreatPoly) -> None:
self, airbases: ThreatPoly, air_defenses: ThreatPoly, radar_sam_threats
) -> None:
self.airbases = airbases self.airbases = airbases
self.air_defenses = air_defenses self.air_defenses = air_defenses
self.radar_sam_threats = radar_sam_threats
self.all = unary_union([airbases, air_defenses]) self.all = unary_union([airbases, air_defenses])
def closest_boundary(self, point: DcsPoint) -> DcsPoint: def closest_boundary(self, point: DcsPoint) -> DcsPoint:
@@ -40,10 +37,6 @@ class ThreatZones:
) )
return DcsPoint(boundary.x, boundary.y) return DcsPoint(boundary.x, boundary.y)
def distance_to_threat(self, point: DcsPoint) -> Distance:
boundary = self.closest_boundary(point)
return meters(boundary.distance_to_point(point))
@singledispatchmethod @singledispatchmethod
def threatened(self, position) -> bool: def threatened(self, position) -> bool:
raise NotImplementedError raise NotImplementedError
@@ -75,13 +68,6 @@ class ThreatZones:
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points)) LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
) )
def waypoints_threatened_by_aircraft(
self, waypoints: Iterable[FlightWaypoint]
) -> bool:
return self.threatened_by_aircraft(
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
)
@singledispatchmethod @singledispatchmethod
def threatened_by_air_defense(self, target) -> bool: def threatened_by_air_defense(self, target) -> bool:
raise NotImplementedError raise NotImplementedError
@@ -96,39 +82,12 @@ class ThreatZones:
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points)) LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
) )
@threatened_by_air_defense.register
def _threatened_by_air_defense_mission_target(self, target: MissionTarget) -> bool:
return self.threatened_by_air_defense(
self.dcs_to_shapely_point(target.position)
)
@singledispatchmethod
def threatened_by_radar_sam(self, target) -> bool:
raise NotImplementedError
@threatened_by_radar_sam.register
def _threatened_by_radar_sam_geom(self, position: BaseGeometry) -> bool:
return self.radar_sam_threats.intersects(position)
@threatened_by_radar_sam.register
def _threatened_by_radar_sam_flight(self, flight: Flight) -> bool:
return self.threatened_by_radar_sam(
LineString((self.dcs_to_shapely_point(p.position) for p in flight.points))
)
def waypoints_threatened_by_radar_sam(
self, waypoints: Iterable[FlightWaypoint]
) -> bool:
return self.threatened_by_radar_sam(
LineString((self.dcs_to_shapely_point(p.position) for p in waypoints))
)
@classmethod @classmethod
def closest_enemy_airbase( def closest_enemy_airbase(
cls, location: ControlPoint, max_distance: Distance cls, location: ControlPoint, max_distance: Distance
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
airfields = ObjectiveDistanceCache.get_closest_airfields(location) airfields = ObjectiveDistanceCache.get_closest_airfields(location)
for airfield in airfields.all_airfields_within(max_distance): for airfield in airfields.airfields_within(max_distance):
if airfield.captured != location.captured: if airfield.captured != location.captured:
return airfield return airfield
return None return None
@@ -172,16 +131,15 @@ class ThreatZones:
zone belongs to the player, it is the zone that will be avoided by zone belongs to the player, it is the zone that will be avoided by
the enemy and vice versa. the enemy and vice versa.
""" """
air_threats = [] airbases = []
air_defenses = [] air_defenses = []
radar_sam_threats = []
for control_point in game.theater.controlpoints: for control_point in game.theater.controlpoints:
if control_point.captured != player: if control_point.captured != player:
continue continue
if control_point.runway_is_operational(): if control_point.runway_is_operational():
point = ShapelyPoint(control_point.position.x, control_point.position.y) point = ShapelyPoint(control_point.position.x, control_point.position.y)
cap_threat_range = cls.barcap_threat_range(game, control_point) cap_threat_range = cls.barcap_threat_range(game, control_point)
air_threats.append(point.buffer(cap_threat_range.meters)) airbases.append(point.buffer(cap_threat_range.meters))
for tgo in control_point.ground_objects: for tgo in control_point.ground_objects:
for group in tgo.groups: for group in tgo.groups:
@@ -192,16 +150,9 @@ class ThreatZones:
point = ShapelyPoint(tgo.position.x, tgo.position.y) point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters) threat_zone = point.buffer(threat_range.meters)
air_defenses.append(threat_zone) air_defenses.append(threat_zone)
radar_threat_range = tgo.threat_range(group, radar_only=True)
if radar_threat_range > nautical_miles(3):
point = ShapelyPoint(tgo.position.x, tgo.position.y)
threat_zone = point.buffer(threat_range.meters)
radar_sam_threats.append(threat_zone)
return cls( return cls(
airbases=unary_union(air_threats), airbases=unary_union(airbases), air_defenses=unary_union(air_defenses)
air_defenses=unary_union(air_defenses),
radar_sam_threats=unary_union(radar_sam_threats),
) )
@staticmethod @staticmethod

View File

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

View File

@@ -1,161 +0,0 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from typing import Dict, Optional, TYPE_CHECKING, Type
from dcs.unittype import UnitType, VehicleType
from game.theater import ControlPoint
from .db import PRICES
from .theater.transitnetwork import (
NoPathError,
TransitNetwork,
)
from .transfers import TransferOrder
if TYPE_CHECKING:
from .game import Game
@dataclass(frozen=True)
class GroundUnitSource:
control_point: ControlPoint
class PendingUnitDeliveries:
def __init__(self, destination: ControlPoint) -> None:
self.destination = destination
# Maps unit type to order quantity.
self.units: Dict[Type[UnitType], int] = defaultdict(int)
def __str__(self) -> str:
return f"Pending delivery to {self.destination}"
def order(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] += v
def sell(self, units: Dict[Type[UnitType], int]) -> None:
for k, v in units.items():
self.units[k] -= v
def refund_all(self, game: Game) -> None:
self.refund(game, self.units)
self.units = defaultdict(int)
def refund(self, game: Game, units: Dict[Type[UnitType], int]) -> None:
for unit_type, count in units.items():
try:
price = PRICES[unit_type]
except KeyError:
logging.error(f"Could not refund {unit_type.id}, price unknown")
continue
logging.info(f"Refunding {count} {unit_type.id} at {self.destination.name}")
game.adjust_budget(price * count, player=self.destination.captured)
def pending_orders(self, unit_type: Type[UnitType]) -> int:
pending_units = self.units.get(unit_type)
if pending_units is None:
pending_units = 0
return pending_units
def available_next_turn(self, unit_type: Type[UnitType]) -> int:
current_units = self.destination.base.total_units_of_type(unit_type)
return self.pending_orders(unit_type) + current_units
def process(self, game: Game) -> None:
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
game.message(
f"{self.destination.name} lost its source for ground unit "
"reinforcements. Refunding purchase price."
)
self.refund_all(game)
return
bought_units: Dict[Type[UnitType], int] = {}
units_needing_transfer: Dict[Type[VehicleType], int] = {}
sold_units: Dict[Type[UnitType], int] = {}
for unit_type, count in self.units.items():
coalition = "Ally" if self.destination.captured else "Enemy"
name = unit_type.id
if (
issubclass(unit_type, VehicleType)
and self.destination != ground_unit_source
):
source = ground_unit_source
d = units_needing_transfer
else:
source = self.destination
d = bought_units
if count >= 0:
d[unit_type] = count
game.message(
f"{coalition} reinforcements: {name} x {count} at {source}"
)
else:
sold_units[unit_type] = -count
game.message(f"{coalition} sold: {name} x {-count} at {source}")
self.units = defaultdict(int)
self.destination.base.commision_units(bought_units)
self.destination.base.commit_losses(sold_units)
if units_needing_transfer:
ground_unit_source.base.commision_units(units_needing_transfer)
self.create_transfer(game, ground_unit_source, units_needing_transfer)
def create_transfer(
self, game: Game, source: ControlPoint, units: Dict[Type[VehicleType], int]
) -> None:
game.transfers.new_transfer(TransferOrder(source, self.destination, units))
def find_ground_unit_source(self, game: Game) -> Optional[ControlPoint]:
# This is running *after* the turn counter has been incremented, so this is the
# reaction to turn 0. On turn zero we allow units to be recruited anywhere for
# delivery on turn 1 so that turn 1 always starts with units on the front line.
if game.turn == 1:
return self.destination
# Fast path if the destination is a valid source.
if self.destination.can_recruit_ground_units(game):
return self.destination
try:
return self.find_ground_unit_source_in_network(
game.transit_network_for(self.destination.captured), game
)
except NoPathError:
return None
def find_ground_unit_source_in_network(
self, network: TransitNetwork, game: Game
) -> Optional[ControlPoint]:
sources = []
for control_point in game.theater.control_points_for(self.destination.captured):
if control_point.can_recruit_ground_units(
game
) and network.has_path_between(self.destination, control_point):
sources.append(control_point)
if not sources:
return None
# Fast path to skip the distance calculation if we have only one option.
if len(sources) == 1:
return sources[0]
closest = sources[0]
_, cost = network.shortest_path_with_cost(self.destination, closest)
for source in sources:
_, new_cost = network.shortest_path_with_cost(self.destination, source)
if new_cost < cost:
closest = source
cost = new_cost
return closest

View File

@@ -1,6 +1,4 @@
"""Maps generated units back to their Liberation types.""" """Maps generated units back to their Liberation types."""
import itertools
import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Dict, Optional, Type from typing import Dict, Optional, Type
@@ -9,19 +7,11 @@ from dcs.unitgroup import FlyingGroup, Group, VehicleGroup
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from game import db from game import db
from game.squadrons import Pilot
from game.theater import Airfield, ControlPoint, TheaterGroundObject from game.theater import Airfield, ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import BuildingGroundObject, SceneryGroundObject from game.theater.theatergroundobject import BuildingGroundObject
from game.transfers import CargoShip, Convoy, TransferOrder
from gen.flights.flight import Flight from gen.flights.flight import Flight
@dataclass(frozen=True)
class FlyingUnit:
flight: Flight
pilot: Pilot
@dataclass(frozen=True) @dataclass(frozen=True)
class FrontLineUnit: class FrontLineUnit:
unit_type: Type[VehicleType] unit_type: Type[VehicleType]
@@ -35,18 +25,6 @@ class GroundObjectUnit:
unit: Unit unit: Unit
@dataclass(frozen=True)
class ConvoyUnit:
unit_type: Type[VehicleType]
convoy: Convoy
@dataclass(frozen=True)
class AirliftUnits:
cargo: tuple[Type[VehicleType], ...]
transfer: TransferOrder
@dataclass(frozen=True) @dataclass(frozen=True)
class Building: class Building:
ground_object: BuildingGroundObject ground_object: BuildingGroundObject
@@ -54,29 +32,22 @@ class Building:
class UnitMap: class UnitMap:
def __init__(self) -> None: def __init__(self) -> None:
self.aircraft: Dict[str, FlyingUnit] = {} self.aircraft: Dict[str, Flight] = {}
self.airfields: Dict[str, Airfield] = {} self.airfields: Dict[str, Airfield] = {}
self.front_line_units: Dict[str, FrontLineUnit] = {} self.front_line_units: Dict[str, FrontLineUnit] = {}
self.ground_object_units: Dict[str, GroundObjectUnit] = {} self.ground_object_units: Dict[str, GroundObjectUnit] = {}
self.buildings: Dict[str, Building] = {} self.buildings: Dict[str, Building] = {}
self.convoys: Dict[str, ConvoyUnit] = {}
self.cargo_ships: Dict[str, CargoShip] = {}
self.airlifts: Dict[str, AirliftUnits] = {}
def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None: def add_aircraft(self, group: FlyingGroup, flight: Flight) -> None:
for pilot, unit in zip(flight.roster.pilots, group.units): for unit in group.units:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
name = str(unit.name) name = str(unit.name)
if name in self.aircraft: if name in self.aircraft:
raise RuntimeError(f"Duplicate unit name: {name}") raise RuntimeError(f"Duplicate unit name: {name}")
if pilot is None: self.aircraft[name] = flight
raise ValueError(f"{name} has no pilot assigned")
self.aircraft[name] = FlyingUnit(flight, pilot)
if flight.cargo is not None:
self.add_airlift_units(group, flight.cargo)
def flight(self, unit_name: str) -> Optional[FlyingUnit]: def flight(self, unit_name: str) -> Optional[Flight]:
return self.aircraft.get(unit_name, None) return self.aircraft.get(unit_name, None)
def add_airfield(self, airfield: Airfield) -> None: def add_airfield(self, airfield: Airfield) -> None:
@@ -142,65 +113,6 @@ class UnitMap:
def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]: def ground_object_unit(self, name: str) -> Optional[GroundObjectUnit]:
return self.ground_object_units.get(name, None) return self.ground_object_units.get(name, None)
def add_convoy_units(self, group: Group, convoy: Convoy) -> None:
for unit in group.units:
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.convoys:
raise RuntimeError(f"Duplicate convoy unit: {name}")
unit_type = db.unit_type_from_name(unit.type)
if unit_type is None:
raise RuntimeError(f"Unknown unit type: {unit.type}")
if not issubclass(unit_type, VehicleType):
raise RuntimeError(
f"{name} is a {unit_type.__name__}, expected a VehicleType"
)
self.convoys[name] = ConvoyUnit(unit_type, convoy)
def convoy_unit(self, name: str) -> Optional[ConvoyUnit]:
return self.convoys.get(name, None)
def add_cargo_ship(self, group: Group, ship: CargoShip) -> None:
if len(group.units) > 1:
# Cargo ship "groups" are single units. Killing the one ship kills the whole
# transfer. If we ever want to add escorts or create multiple cargo ships in
# a convoy of ships that logic needs to change.
raise ValueError("Expected cargo ship to be a single unit group.")
unit = group.units[0]
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(unit.name)
if name in self.cargo_ships:
raise RuntimeError(f"Duplicate cargo ship: {name}")
self.cargo_ships[name] = ship
def cargo_ship(self, name: str) -> Optional[CargoShip]:
return self.cargo_ships.get(name, None)
def add_airlift_units(self, group: FlyingGroup, transfer: TransferOrder) -> None:
capacity_each = math.ceil(transfer.size / len(group.units))
for idx, transport in enumerate(group.units):
# Slice the units in groups based on the capacity of each unit. Cargo is
# assigned arbitrarily to units in the order of the group. The last unit in
# the group will receive a partial load if there is not enough cargo to fill
# every transport.
base_idx = idx * capacity_each
cargo = tuple(
itertools.islice(
transfer.iter_units(), base_idx, base_idx + capacity_each
)
)
# The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__.
name = str(transport.name)
if name in self.airlifts:
raise RuntimeError(f"Duplicate airlift unit: {name}")
self.airlifts[name] = AirliftUnits(cargo, transfer)
def airlift_unit(self, name: str) -> Optional[AirliftUnits]:
return self.airlifts.get(name, None)
def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None: def add_building(self, ground_object: BuildingGroundObject, group: Group) -> None:
# The actual name is a String (the pydcs translatable string), which # The actual name is a String (the pydcs translatable string), which
# doesn't define __eq__. # doesn't define __eq__.
@@ -224,15 +136,5 @@ class UnitMap:
raise RuntimeError(f"Duplicate TGO unit: {name}") raise RuntimeError(f"Duplicate TGO unit: {name}")
self.buildings[name] = Building(ground_object) self.buildings[name] = Building(ground_object)
def add_scenery(self, ground_object: SceneryGroundObject) -> None:
name = str(ground_object.map_object_id)
if name in self.buildings:
raise RuntimeError(
f"Duplicate TGO unit: {name}. TriggerZone name: "
f"{ground_object.dcs_identifier}"
)
self.buildings[name] = Building(ground_object)
def building_or_fortification(self, name: str) -> Optional[Building]: def building_or_fortification(self, name: str) -> Optional[Building]:
return self.buildings.get(name, None) return self.buildings.get(name, None)

View File

@@ -1,6 +1,5 @@
from __future__ import annotations from __future__ import annotations
import itertools
import math import math
from dataclasses import dataclass from dataclasses import dataclass
from typing import Union from typing import Union
@@ -179,13 +178,3 @@ def mach(value: float, altitude: Distance) -> Speed:
SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5) SPEED_OF_SOUND_AT_SEA_LEVEL = knots(661.5)
def pairwise(iterable):
"""
itertools recipe
s -> (s0,s1), (s1,s2), (s2, s3), ...
"""
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)

View File

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

View File

@@ -3,12 +3,11 @@ from __future__ import annotations
import datetime import datetime
import logging import logging
import random import random
from dataclasses import dataclass, field from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from dcs.cloud_presets import Clouds as PydcsClouds from dcs.weather import Weather as PydcsWeather, Wind
from dcs.weather import CloudPreset, Weather as PydcsWeather, Wind
from game.settings import Settings from game.settings import Settings
from game.utils import Distance, meters from game.utils import Distance, meters
@@ -37,23 +36,6 @@ class Clouds:
density: int density: int
thickness: int thickness: int
precipitation: PydcsWeather.Preceptions precipitation: PydcsWeather.Preceptions
preset: Optional[CloudPreset] = field(default=None)
@classmethod
def random_preset(cls, rain: bool) -> Clouds:
clouds = (p.value for p in PydcsClouds)
if rain:
presets = [p for p in clouds if "Rain" in p.name]
else:
presets = [p for p in clouds if "Rain" not in p.name]
preset = random.choice(presets)
return Clouds(
base=random.randint(preset.min_base, preset.max_base),
density=0,
thickness=0,
precipitation=PydcsWeather.Preceptions.None_,
preset=preset,
)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -119,11 +101,12 @@ class ClearSkies(Weather):
class Cloudy(Weather): class Cloudy(Weather):
def generate_clouds(self) -> Optional[Clouds]: def generate_clouds(self) -> Optional[Clouds]:
return Clouds.random_preset(rain=False) return Clouds(
base=self.random_cloud_base(),
def generate_fog(self) -> Optional[Fog]: density=random.randint(1, 8),
# DCS 2.7 says to not use fog with the cloud presets. thickness=self.random_cloud_thickness(),
return None precipitation=PydcsWeather.Preceptions.None_,
)
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
return self.random_wind(0, 4) return self.random_wind(0, 4)
@@ -131,11 +114,12 @@ class Cloudy(Weather):
class Raining(Weather): class Raining(Weather):
def generate_clouds(self) -> Optional[Clouds]: def generate_clouds(self) -> Optional[Clouds]:
return Clouds.random_preset(rain=True) return Clouds(
base=self.random_cloud_base(),
def generate_fog(self) -> Optional[Fog]: density=random.randint(5, 8),
# DCS 2.7 says to not use fog with the cloud presets. thickness=self.random_cloud_thickness(),
return None precipitation=PydcsWeather.Preceptions.Rain,
)
def generate_wind(self) -> WindConditions: def generate_wind(self) -> WindConditions:
return self.random_wind(0, 6) return self.random_wind(0, 6)

View File

@@ -5,7 +5,7 @@ import random
from dataclasses import dataclass from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union, Iterable from typing import Dict, List, Optional, TYPE_CHECKING, Type, Union
from dcs import helicopters from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup from dcs.action import AITaskPush, ActivateGroup
@@ -41,8 +41,6 @@ from dcs.planes import (
) )
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.task import ( from dcs.task import (
AWACS,
AWACSTaskAction,
AntishipStrike, AntishipStrike,
AttackGroup, AttackGroup,
Bombing, Bombing,
@@ -61,26 +59,24 @@ from dcs.task import (
OptReactOnThreat, OptReactOnThreat,
OptRestrictJettison, OptRestrictJettison,
OrbitAction, OrbitAction,
PinpointStrike,
RunwayAttack, RunwayAttack,
SEAD,
StartCommand, StartCommand,
Targets, Targets,
Transport, Task,
WeaponType, WeaponType,
TargetType,
) )
from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import Event, TriggerOnce, TriggerRule from dcs.triggers import Event, TriggerOnce, TriggerRule
from dcs.unit import Unit, Skill
from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup from dcs.unitgroup import FlyingGroup, ShipGroup, StaticGroup
from dcs.unittype import FlyingType, UnitType from dcs.unittype import FlyingType, UnitType
from game import db from game import db
from game.data.cap_capabilities_db import GUNFIGHTERS from game.data.cap_capabilities_db import GUNFIGHTERS
from game.data.weapons import Pylon from game.data.weapons import Pylon, Weapon
from game.db import GUN_RELIANT_AIRFRAMES
from game.factions.faction import Faction from game.factions.faction import Faction
from game.settings import Settings from game.settings import Settings
from game.squadrons import Pilot, Squadron
from game.theater.controlpoint import ( from game.theater.controlpoint import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -88,13 +84,13 @@ from game.theater.controlpoint import (
NavalControlPoint, NavalControlPoint,
OffMapSpawn, OffMapSpawn,
) )
from game.theater.missiontarget import MissionTarget
from game.theater.theatergroundobject import TheaterGroundObject from game.theater.theatergroundobject import TheaterGroundObject
from game.transfers import MultiGroupTransport
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import Distance, meters, nautical_miles from game.utils import Distance, meters, nautical_miles
from gen.airsupportgen import AirSupport
from gen.ato import AirTaskingOrder, Package from gen.ato import AirTaskingOrder, Package
from gen.callsigns import create_group_callsign_from_unit from gen.callsigns import create_group_callsign_from_unit
from gen.conflictgen import FRONTLINE_LENGTH
from gen.flights.flight import ( from gen.flights.flight import (
Flight, Flight,
FlightType, FlightType,
@@ -103,10 +99,7 @@ from gen.flights.flight import (
) )
from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio from gen.radios import MHz, Radio, RadioFrequency, RadioRegistry, get_radio
from gen.runways import RunwayData from gen.runways import RunwayData
from .airsupportgen import AirSupport, AwacsInfo
from .callsigns import callsign_for_support_unit
from .flights.flightplan import ( from .flights.flightplan import (
AwacsFlightPlan,
CasFlightPlan, CasFlightPlan,
LoiterFlightPlan, LoiterFlightPlan,
PatrollingFlightPlan, PatrollingFlightPlan,
@@ -141,7 +134,6 @@ TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_SHIP, FlightWaypointType.TARGET_SHIP,
) )
# TODO: Get radio information for all the special cases. # TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency: def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
if unit_type in helicopter_map.values() and unit_type != UH_1H: if unit_type in helicopter_map.values() and unit_type != UH_1H:
@@ -326,7 +318,6 @@ class FlightData:
intra_flight_channel: RadioFrequency, intra_flight_channel: RadioFrequency,
bingo_fuel: Optional[int], bingo_fuel: Optional[int],
joker_fuel: Optional[int], joker_fuel: Optional[int],
custom_name: Optional[str],
) -> None: ) -> None:
self.package = package self.package = package
self.country = country self.country = country
@@ -344,7 +335,6 @@ class FlightData:
self.bingo_fuel = bingo_fuel self.bingo_fuel = bingo_fuel
self.joker_fuel = joker_fuel self.joker_fuel = joker_fuel
self.callsign = create_group_callsign_from_unit(self.units[0]) self.callsign = create_group_callsign_from_unit(self.units[0])
self.custom_name = custom_name
@property @property
def client_units(self) -> List[FlyingUnit]: def client_units(self) -> List[FlyingUnit]:
@@ -665,16 +655,10 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
channel_allocator=None, channel_allocator=None,
channel_namer=SCR522ChannelNamer, channel_namer=SCR522ChannelNamer,
), ),
"JAS39Gripen": AircraftData(
inter_flight_radio=get_radio("R&S Series 6000"),
intra_flight_radio=get_radio("R&S Series 6000"),
channel_allocator=None,
),
} }
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"] AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"] AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"] AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["JAS39Gripen_AG"] = AIRCRAFT_DATA["JAS39Gripen"]
class AircraftConflictGenerator: class AircraftConflictGenerator:
@@ -685,7 +669,6 @@ class AircraftConflictGenerator:
game: Game, game: Game,
radio_registry: RadioRegistry, radio_registry: RadioRegistry,
unit_map: UnitMap, unit_map: UnitMap,
air_support: AirSupport,
) -> None: ) -> None:
self.m = mission self.m = mission
self.game = game self.game = game
@@ -693,7 +676,6 @@ class AircraftConflictGenerator:
self.radio_registry = radio_registry self.radio_registry = radio_registry
self.unit_map = unit_map self.unit_map = unit_map
self.flights: List[FlightData] = [] self.flights: List[FlightData] = []
self.air_support = air_support
@cached_property @cached_property
def use_client(self) -> bool: def use_client(self) -> bool:
@@ -733,90 +715,74 @@ class AircraftConflictGenerator:
return StartType.Cold return StartType.Cold
return StartType.Warm return StartType.Warm
def skill_level_for(
self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool
) -> Skill:
if blue:
base_skill = Skill(self.game.settings.player_skill)
else:
base_skill = Skill(self.game.settings.enemy_skill)
if pilot is None:
logging.error(f"Cannot determine skill level: {unit.name} has not pilot")
return base_skill
levels = [
Skill.Average,
Skill.Good,
Skill.High,
Skill.Excellent,
]
current_level = levels.index(base_skill)
missions_for_skill_increase = 4
increase = pilot.record.missions_flown // missions_for_skill_increase
new_level = min(current_level + increase, len(levels) - 1)
return levels[new_level]
def set_skill(self, unit: FlyingUnit, pilot: Optional[Pilot], blue: bool) -> None:
if pilot is None or not pilot.player:
unit.skill = self.skill_level_for(unit, pilot, blue)
return
if self.use_client:
unit.set_client()
else:
unit.set_player()
@staticmethod
def livery_from_db(flight: Flight) -> Optional[str]:
return db.PLANE_LIVERY_OVERRIDES.get(flight.unit_type)
def livery_from_faction(self, flight: Flight) -> Optional[str]:
faction = self.game.faction_for(player=flight.departure.captured)
if (choices := faction.liveries_overrides.get(flight.unit_type)) is not None:
return random.choice(choices)
return None
@staticmethod
def livery_from_squadron(flight: Flight) -> Optional[str]:
return flight.squadron.livery
def livery_for(self, flight: Flight) -> Optional[str]:
if (livery := self.livery_from_squadron(flight)) is not None:
return livery
if (livery := self.livery_from_faction(flight)) is not None:
return livery
if (livery := self.livery_from_db(flight)) is not None:
return livery
return None
def _setup_livery(self, flight: Flight, group: FlyingGroup) -> None:
livery = self.livery_for(flight)
if livery is None:
return
for unit in group.units:
unit.livery_id = livery
def _setup_group( def _setup_group(
self, self,
group: FlyingGroup, group: FlyingGroup,
for_task: Type[Task],
package: Package, package: Package,
flight: Flight, flight: Flight,
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
did_load_loadout = False
unit_type = group.units[0].unit_type unit_type = group.units[0].unit_type
self._setup_payload(flight, group) if unit_type in db.PLANE_PAYLOAD_OVERRIDES:
self._setup_livery(flight, group) # Clear pylons
for p in group.units:
p.pylons.clear()
# Now load loadout
if for_task in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][for_task]
group.load_loadout(payload_name)
if not group.units[0].pylons and for_task == RunwayAttack:
if PinpointStrike in db.PLANE_PAYLOAD_OVERRIDES[unit_type]:
logging.warning(
'No loadout for "Runway Attack" for the {}, defaulting to Strike loadout'.format(
str(unit_type)
)
)
payload_name = db.PLANE_PAYLOAD_OVERRIDES[unit_type][
PinpointStrike
]
group.load_loadout(payload_name)
did_load_loadout = True
logging.info(
"Loaded overridden payload for {} - {} for task {}".format(
unit_type, payload_name, for_task
)
)
if not did_load_loadout:
group.load_task_default_loadout(for_task)
if unit_type in db.PLANE_LIVERY_OVERRIDES:
for unit_instance in group.units:
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
# Override livery by faction file data
if flight.from_cp.captured:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
if unit_type in faction.liveries_overrides:
livery = random.choice(faction.liveries_overrides[unit_type])
for unit_instance in group.units:
unit_instance.livery_id = livery
for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
if self.use_client:
unit.set_client()
else:
unit.set_player()
for unit, pilot in zip(group.units, flight.roster.pilots):
player = pilot is not None and pilot.player
self.set_skill(unit, pilot, blue=flight.departure.captured)
# Do not generate player group with late activation. # Do not generate player group with late activation.
if player and group.late_activation: if group.late_activation:
group.late_activation = False group.late_activation = False
# Set up F-14 Client to have pre-stored alignment # Set up F-14 Client to have pre-stored alignement
if unit_type is F_14B: if unit_type is F_14B:
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True) unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
@@ -824,10 +790,7 @@ class AircraftConflictGenerator:
OptReactOnThreat(OptReactOnThreat.Values.EvadeFire) OptReactOnThreat(OptReactOnThreat.Values.EvadeFire)
) )
if flight.flight_type == FlightType.AEWC: channel = self.get_intra_flight_channel(unit_type)
channel = self.radio_registry.alloc_uhf()
else:
channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz) group.set_frequency(channel.mhz)
divert = None divert = None
@@ -837,7 +800,7 @@ class AircraftConflictGenerator:
self.flights.append( self.flights.append(
FlightData( FlightData(
package=package, package=package,
country=self.game.faction_for(player=flight.departure.captured).country, country=faction.country,
flight_type=flight.flight_type, flight_type=flight.flight_type,
units=group.units, units=group.units,
size=len(group.units), size=len(group.units),
@@ -856,7 +819,6 @@ class AircraftConflictGenerator:
intra_flight_channel=channel, intra_flight_channel=channel,
bingo_fuel=flight.flight_plan.bingo_fuel, bingo_fuel=flight.flight_plan.bingo_fuel,
joker_fuel=flight.flight_plan.joker_fuel, joker_fuel=flight.flight_plan.joker_fuel,
custom_name=flight.custom_name,
) )
) )
@@ -864,21 +826,6 @@ class AircraftConflictGenerator:
if unit_type in [Su_33, C_101EB, C_101CC]: if unit_type in [Su_33, C_101EB, C_101CC]:
self.set_reduced_fuel(flight, group, unit_type) self.set_reduced_fuel(flight, group, unit_type)
if isinstance(flight.flight_plan, AwacsFlightPlan):
callsign = callsign_for_support_unit(group)
self.air_support.awacs.append(
AwacsInfo(
group_name=str(group.name),
callsign=callsign,
freq=channel,
depature_location=flight.departure.name,
end_time=flight.flight_plan.mission_departure_time,
start_time=flight.flight_plan.mission_start_time,
blue=flight.departure.captured,
)
)
def _generate_at_airport( def _generate_at_airport(
self, self,
name: str, name: str,
@@ -1001,20 +948,39 @@ class AircraftConflictGenerator:
else: else:
assert False assert False
def _setup_payload(self, flight: Flight, group: FlyingGroup) -> None: @staticmethod
def _setup_custom_payload(flight: Flight, group: FlyingGroup) -> None:
if not flight.use_custom_loadout:
return
logging.info("Custom loadout for flight : " + flight.__repr__())
for p in group.units: for p in group.units:
p.pylons.clear() p.pylons.clear()
loadout = flight.loadout for pylon_number, weapon in flight.loadout.items():
if self.game.settings.restrict_weapons_by_date:
loadout = loadout.degrade_for_date(flight.unit_type, self.game.date)
for pylon_number, weapon in loadout.pylons.items():
if weapon is None: if weapon is None:
continue continue
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number) pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
pylon.equip(group, weapon) pylon.equip(group, weapon)
def _degrade_payload_to_era(self, flight: Flight, group: FlyingGroup) -> None:
loadout = dict(group.units[0].pylons)
for pylon_number, clsid in loadout.items():
weapon = Weapon.from_clsid(clsid["CLSID"])
if weapon is None:
logging.error(f"Could not find weapon for clsid {clsid}")
continue
if not weapon.available_on(self.game.date):
pylon = Pylon.for_aircraft(flight.unit_type, pylon_number)
for fallback in weapon.fallbacks:
if not pylon.can_equip(fallback):
continue
if not fallback.available_on(self.game.date):
continue
pylon.equip(group, fallback)
break
def clear_parking_slots(self) -> None: def clear_parking_slots(self) -> None:
for cp in self.game.theater.controlpoints: for cp in self.game.theater.controlpoints:
for parking_slot in cp.parking_slots: for parking_slot in cp.parking_slots:
@@ -1073,7 +1039,7 @@ class AircraftConflictGenerator:
flight = Flight( flight = Flight(
Package(control_point), Package(control_point),
faction.country, faction.country,
self.game.air_wing_for(control_point.captured).squadron_for(aircraft), aircraft,
1, 1,
FlightType.BARCAP, FlightType.BARCAP,
"Cold", "Cold",
@@ -1208,23 +1174,12 @@ class AircraftConflictGenerator:
raise RuntimeError(f"No reduced fuel case for type {unit_type}") raise RuntimeError(f"No reduced fuel case for type {unit_type}")
@staticmethod @staticmethod
def flight_always_keeps_gun(flight: Flight) -> bool:
# Never take bullets from players. They're smart enough to know when to use it
# and when to RTB.
if flight.client_count > 0:
return True
return flight.unit_type in GUN_RELIANT_AIRFRAMES
def configure_behavior( def configure_behavior(
self,
flight: Flight,
group: FlyingGroup, group: FlyingGroup,
react_on_threat: Optional[OptReactOnThreat.Values] = None, react_on_threat: Optional[OptReactOnThreat.Values] = None,
roe: Optional[OptROE.Values] = None, roe: Optional[OptROE.Values] = None,
rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None, rtb_winchester: Optional[OptRTBOnOutOfAmmo.Values] = None,
restrict_jettison: Optional[bool] = None, restrict_jettison: Optional[bool] = None,
mission_uses_gun: bool = True,
) -> None: ) -> None:
group.points[0].tasks.clear() group.points[0].tasks.clear()
if react_on_threat is not None: if react_on_threat is not None:
@@ -1236,17 +1191,6 @@ class AircraftConflictGenerator:
if rtb_winchester is not None: if rtb_winchester is not None:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester)) group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
# Confiscate the bullets of AI missions that do not rely on the gun. There is no
# "all but gun" RTB winchester option, so air to ground missions with mixed
# weapon types will insist on using all of their bullets after running out of
# missiles and bombs. Take away their bullets so they don't strafe a Tor.
#
# Exceptions are made for player flights and for airframes where the gun is
# essential like the A-10 or warbirds.
if not mission_uses_gun and not self.flight_always_keeps_gun(flight):
for unit in group.units:
unit.gun = 0
group.points[0].tasks.append(OptRTBOnBingoFuel(True)) group.points[0].tasks.append(OptRTBOnBingoFuel(True))
# Do not restrict afterburner. # 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 # 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
@@ -1265,14 +1209,14 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = CAP.name group.task = CAP.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, CAP, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS: if flight.unit_type not in GUNFIGHTERS:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else: else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
self.configure_behavior(flight, group, rtb_winchester=ammo_type) self.configure_behavior(group, rtb_winchester=ammo_type)
def configure_sweep( def configure_sweep(
self, self,
@@ -1282,14 +1226,14 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = FighterSweep.name group.task = FighterSweep.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, FighterSweep, package, flight, dynamic_runways)
if flight.unit_type not in GUNFIGHTERS: if flight.unit_type not in GUNFIGHTERS:
ammo_type = OptRTBOnOutOfAmmo.Values.AAM ammo_type = OptRTBOnOutOfAmmo.Values.AAM
else: else:
ammo_type = OptRTBOnOutOfAmmo.Values.Cannon ammo_type = OptRTBOnOutOfAmmo.Values.Cannon
self.configure_behavior(flight, group, rtb_winchester=ammo_type) self.configure_behavior(group, rtb_winchester=ammo_type)
def configure_cas( def configure_cas(
self, self,
@@ -1299,9 +1243,8 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = CAS.name group.task = CAS.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, CAS, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
@@ -1316,22 +1259,14 @@ class AircraftConflictGenerator:
flight: Flight, flight: Flight,
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
# Only CAS and SEAD are capable of the Attack Group task. SEAD is arguably more group.task = SEAD.name
# appropriate but it has an extremely limited list of capable aircraft, whereas self._setup_group(group, SEAD, package, flight, dynamic_runways)
# CAS has a much wider selection of units.
#
# Note that the only effect that the DCS task type has is in determining which
# waypoint actions the group may perform.
group.task = CAS.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
rtb_winchester=OptRTBOnOutOfAmmo.Values.All, rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
restrict_jettison=True, restrict_jettison=True,
mission_uses_gun=False,
) )
def configure_sead( def configure_sead(
@@ -1341,21 +1276,14 @@ class AircraftConflictGenerator:
flight: Flight, flight: Flight,
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
# CAS is able to perform all the same tasks as SEAD using a superset of the group.task = SEAD.name
# available aircraft, and F-14s are not able to be SEAD despite having TALDs. self._setup_group(group, SEAD, package, flight, dynamic_runways)
# https://forums.eagle.ru/topic/272112-cannot-assign-f-14-to-sead/
group.task = CAS.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
# ASM includes ARMs and TALDs (among other things, but those are the useful
# weapons for SEAD).
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM, rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
restrict_jettison=True, restrict_jettison=True,
mission_uses_gun=False,
) )
def configure_strike( def configure_strike(
@@ -1366,14 +1294,12 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = GroundAttack.name group.task = GroundAttack.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, GroundAttack, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True, restrict_jettison=True,
mission_uses_gun=False,
) )
def configure_anti_ship( def configure_anti_ship(
@@ -1384,14 +1310,12 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = AntishipStrike.name group.task = AntishipStrike.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, AntishipStrike, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True, restrict_jettison=True,
mission_uses_gun=False,
) )
def configure_runway_attack( def configure_runway_attack(
@@ -1402,14 +1326,12 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = RunwayAttack.name group.task = RunwayAttack.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, RunwayAttack, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True, restrict_jettison=True,
mission_uses_gun=False,
) )
def configure_oca_strike( def configure_oca_strike(
@@ -1420,43 +1342,14 @@ class AircraftConflictGenerator:
dynamic_runways: Dict[str, RunwayData], dynamic_runways: Dict[str, RunwayData],
) -> None: ) -> None:
group.task = CAS.name group.task = CAS.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, CAS, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight,
group, group,
react_on_threat=OptReactOnThreat.Values.EvadeFire, react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire, roe=OptROE.Values.OpenFire,
restrict_jettison=True, restrict_jettison=True,
) )
def configure_awacs(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = AWACS.name
if not isinstance(flight.flight_plan, AwacsFlightPlan):
logging.error(
f"Cannot configure AEW&C tasks for {flight} because it does not have an AEW&C flight plan."
)
return
self._setup_group(group, package, flight, dynamic_runways)
# Awacs task action
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
)
group.points[0].tasks.append(AWACSTaskAction())
def configure_escort( def configure_escort(
self, self,
group: FlyingGroup, group: FlyingGroup,
@@ -1468,54 +1361,14 @@ class AircraftConflictGenerator:
# Search Then Engage task, which we have to use instead of the Escort # Search Then Engage task, which we have to use instead of the Escort
# task for the reasons explained in JoinPointBuilder. # task for the reasons explained in JoinPointBuilder.
group.task = CAP.name group.task = CAP.name
self._setup_group(group, package, flight, dynamic_runways) self._setup_group(group, CAP, package, flight, dynamic_runways)
self.configure_behavior( self.configure_behavior(
flight, group, roe=OptROE.Values.OpenFire, restrict_jettison=True group, roe=OptROE.Values.OpenFire, restrict_jettison=True
)
def configure_sead_escort(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
# 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
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
roe=OptROE.Values.OpenFire,
# ASM includes ARMs and TALDs (among other things, but those are the useful
# weapons for SEAD).
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
restrict_jettison=True,
mission_uses_gun=False,
)
def configure_transport(
self,
group: FlyingGroup,
package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData],
) -> None:
group.task = Transport.name
self._setup_group(group, package, flight, dynamic_runways)
self.configure_behavior(
flight,
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.WeaponHold,
restrict_jettison=True,
) )
def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None: def configure_unknown_task(self, group: FlyingGroup, flight: Flight) -> None:
logging.error(f"Unhandled flight type: {flight.flight_type}") logging.error(f"Unhandled flight type: {flight.flight_type}")
self.configure_behavior(flight, group) self.configure_behavior(group)
def setup_flight_group( def setup_flight_group(
self, self,
@@ -1533,16 +1386,12 @@ class AircraftConflictGenerator:
self.configure_cap(group, package, flight, dynamic_runways) self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type == FlightType.SWEEP: elif flight_type == FlightType.SWEEP:
self.configure_sweep(group, package, flight, dynamic_runways) self.configure_sweep(group, package, flight, dynamic_runways)
elif flight_type == FlightType.AEWC:
self.configure_awacs(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]: elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways) self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type == FlightType.DEAD: elif flight_type == FlightType.DEAD:
self.configure_dead(group, package, flight, dynamic_runways) self.configure_dead(group, package, flight, dynamic_runways)
elif flight_type == FlightType.SEAD: elif flight_type == FlightType.SEAD:
self.configure_sead(group, package, flight, dynamic_runways) self.configure_sead(group, package, flight, dynamic_runways)
elif flight_type == FlightType.SEAD_ESCORT:
self.configure_sead_escort(group, package, flight, dynamic_runways)
elif flight_type == FlightType.STRIKE: elif flight_type == FlightType.STRIKE:
self.configure_strike(group, package, flight, dynamic_runways) self.configure_strike(group, package, flight, dynamic_runways)
elif flight_type == FlightType.ANTISHIP: elif flight_type == FlightType.ANTISHIP:
@@ -1553,8 +1402,6 @@ class AircraftConflictGenerator:
self.configure_runway_attack(group, package, flight, dynamic_runways) self.configure_runway_attack(group, package, flight, dynamic_runways)
elif flight_type == FlightType.OCA_AIRCRAFT: elif flight_type == FlightType.OCA_AIRCRAFT:
self.configure_oca_strike(group, package, flight, dynamic_runways) self.configure_oca_strike(group, package, flight, dynamic_runways)
elif flight_type == FlightType.TRANSPORT:
self.configure_transport(group, package, flight, dynamic_runways)
else: else:
self.configure_unknown_task(group, flight) self.configure_unknown_task(group, flight)
@@ -1608,6 +1455,9 @@ class AircraftConflictGenerator:
# Set here rather than when the FlightData is created so they waypoints # Set here rather than when the FlightData is created so they waypoints
# have their TOTs set. # have their TOTs set.
self.flights[-1].waypoints = [takeoff_point] + flight.points self.flights[-1].waypoints = [takeoff_point] + flight.points
self._setup_custom_payload(flight, group)
if self.game.settings.restrict_weapons_by_date:
self._degrade_payload_to_era(flight, group)
def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool: def should_delay_flight(self, flight: Flight, start_time: timedelta) -> bool:
if start_time.total_seconds() <= 0: if start_time.total_seconds() <= 0:
@@ -1687,7 +1537,7 @@ class PydcsWaypointBuilder:
waypoint = self.group.add_waypoint( waypoint = self.group.add_waypoint(
Point(self.waypoint.x, self.waypoint.y), Point(self.waypoint.x, self.waypoint.y),
self.waypoint.alt.meters, self.waypoint.alt.meters,
name=self.waypoint.name, name=self.mission.string(self.waypoint.name),
) )
if self.waypoint.flyover: if self.waypoint.flyover:
@@ -1719,7 +1569,6 @@ class PydcsWaypointBuilder:
mission: Mission, mission: Mission,
) -> PydcsWaypointBuilder: ) -> PydcsWaypointBuilder:
builders = { builders = {
FlightWaypointType.DROP_OFF: CargoStopBuilder,
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder, FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder, FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder, FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
@@ -1733,7 +1582,6 @@ class PydcsWaypointBuilder:
FlightWaypointType.LOITER: HoldPointBuilder, FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL: RaceTrackEndBuilder, FlightWaypointType.PATROL: RaceTrackEndBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder, FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.PICKUP: CargoStopBuilder,
} }
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder) builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission) return builder(waypoint, group, package, flight, mission)
@@ -1749,9 +1597,7 @@ class PydcsWaypointBuilder:
else: else:
return False return False
def register_special_waypoints( def register_special_waypoints(self, targets) -> None:
self, targets: Iterable[Union[MissionTarget, Unit]]
) -> None:
"""Create special target waypoints for various aircraft""" """Create special target waypoints for various aircraft"""
for i, t in enumerate(targets): for i, t in enumerate(targets):
if self.group.units[0].unit_type == JF_17 and i < 4: if self.group.units[0].unit_type == JF_17 and i < 4:
@@ -1789,33 +1635,25 @@ class BaiIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
# TODO: Add common "UnitGroupTarget" base type. target_group = self.package.target
group_names = [] if isinstance(target_group, TheaterGroundObject):
target = self.package.target tgroup = self.mission.find_group(target_group.group_name)
if isinstance(target, TheaterGroundObject): if tgroup is not None:
for group in target.groups: task = AttackGroup(tgroup.id, weapon_type=WeaponType.Auto)
group_names.append(group.name) task.params["attackQtyLimit"] = False
elif isinstance(target, MultiGroupTransport): task.params["directionEnabled"] = False
group_names.append(target.name) task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
else:
logging.error(
"Could not find group for BAI mission %s", target_group.group_name
)
else: else:
logging.error( logging.error(
"Unexpected target type for BAI mission: %s", "Unexpected target type for BAI mission: %s",
target.__class__.__name__, target_group.__class__.__name__,
) )
return waypoint
for group_name in group_names:
group = self.mission.find_group(group_name)
if group is None:
logging.error("Could not find group for BAI mission %s", group_name)
continue
task = AttackGroup(group.id, weapon_type=WeaponType.Auto)
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint return waypoint
@@ -1852,29 +1690,23 @@ class CasIngressBuilder(PydcsWaypointBuilder):
class DeadIngressBuilder(PydcsWaypointBuilder): class DeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
task = AttackGroup(tgroup.id, weapon_type=WeaponType.Guided)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
else:
logging.error(
f"Could not find group for DEAD mission {target_group.group_name}"
)
self.register_special_waypoints(self.waypoint.targets) self.register_special_waypoints(self.waypoint.targets)
target = self.package.target
if not isinstance(target, TheaterGroundObject):
logging.error(
"Unexpected target type for DEAD mission: %s",
target.__class__.__name__,
)
return waypoint
for group in target.groups:
miz_group = self.mission.find_group(group.name)
if miz_group is None:
logging.error(f"Could not find group for DEAD mission {group.name}")
continue
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Auto)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint return waypoint
@@ -1926,29 +1758,25 @@ class OcaRunwayIngressBuilder(PydcsWaypointBuilder):
class SeadIngressBuilder(PydcsWaypointBuilder): class SeadIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name)
if tgroup is not None:
waypoint.add_task(
EngageTargetsInZone(
position=tgroup.position,
radius=int(nautical_miles(30).meters),
targets=[
Targets.All.GroundUnits.AirDefence,
],
)
)
else:
logging.error(
f"Could not find group for DEAD mission {target_group.group_name}"
)
self.register_special_waypoints(self.waypoint.targets) self.register_special_waypoints(self.waypoint.targets)
target = self.package.target
if not isinstance(target, TheaterGroundObject):
logging.error(
"Unexpected target type for SEAD mission: %s",
target.__class__.__name__,
)
return waypoint
for group in target.groups:
miz_group = self.mission.find_group(group.name)
if miz_group is None:
logging.error(f"Could not find group for SEAD mission {group.name}")
continue
task = AttackGroup(miz_group.id, weapon_type=WeaponType.Guided)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
task.params["directionEnabled"] = False
task.params["altitudeEnabled"] = False
task.params["groupAttack"] = True
waypoint.tasks.append(task)
return waypoint return waypoint
@@ -1972,11 +1800,12 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
center.y += target.position.y center.y += target.position.y
center.x /= len(targets) center.x /= len(targets)
center.y /= len(targets) center.y /= len(targets)
bombing = Bombing(center, weapon_type=WeaponType.Bombs) bombing = Bombing(center)
bombing.params["expend"] = "All" bombing.params["expend"] = "All"
bombing.params["attackQtyLimit"] = False bombing.params["attackQtyLimit"] = False
bombing.params["directionEnabled"] = False bombing.params["directionEnabled"] = False
bombing.params["altitudeEnabled"] = False bombing.params["altitudeEnabled"] = False
bombing.params["weaponType"] = WeaponType.Bombs.value
bombing.params["groupAttack"] = True bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing) waypoint.tasks.append(bombing)
return waypoint return waypoint
@@ -1984,15 +1813,29 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
def build_strike(self) -> MovingPoint: def build_strike(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
for target in self.waypoint.targets: for target in self.waypoint.targets:
bombing = Bombing(target.position, weapon_type=WeaponType.Auto)
# If there is only one target, drop all ordnance in one pass.
if len(self.waypoint.targets) == 1:
bombing.params["expend"] = "All"
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
# Register special waypoints targets = [target]
self.register_special_waypoints(self.waypoint.targets) # If the target type is a group of units,
# then target each unit in the group with a Bombing task on their position
# (It is not perfect, we should have an engage Group task instead,
# but we don't have the group ref in the model there)
# TODO : for building group, engage all the buildings as well
if isinstance(target, TheaterGroundObject):
if len(target.units) > 0:
targets = target.units
for t in targets:
bombing = Bombing(t.position)
# If there is only one target, drop all ordnance in one pass
if len(self.waypoint.targets) == 1 and len(targets) == 1:
bombing.params["expend"] = "All"
bombing.params["weaponType"] = WeaponType.Auto.value
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
print(bombing)
# Register special waypoints
self.register_special_waypoints(targets)
return waypoint return waypoint
@@ -2011,10 +1854,7 @@ class SweepIngressBuilder(PydcsWaypointBuilder):
waypoint.tasks.append( waypoint.tasks.append(
EngageTargets( EngageTargets(
max_distance=int(nautical_miles(50).meters), max_distance=int(nautical_miles(50).meters),
targets=[ targets=[Targets.All.Air.Planes.Fighters],
Targets.All.Air.Planes.Fighters,
Targets.All.Air.Planes.MultiroleFighters,
],
) )
) )
@@ -2025,23 +1865,11 @@ class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()
if self.flight.flight_type == FlightType.ESCORT: if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks( self.configure_escort_tasks(waypoint)
waypoint,
[
Targets.All.Air.Planes.Fighters,
Targets.All.Air.Planes.MultiroleFighters,
],
)
elif self.flight.flight_type == FlightType.SEAD_ESCORT:
self.configure_escort_tasks(
waypoint, [Targets.All.GroundUnits.AirDefence.AAA.SAMRelated]
)
return waypoint return waypoint
@staticmethod @staticmethod
def configure_escort_tasks( def configure_escort_tasks(waypoint: MovingPoint) -> None:
waypoint: MovingPoint, target_types: List[Type[TargetType]]
) -> None:
# Ideally we would use the escort mission type and escort task to have # 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 # the AI automatically but the AI only escorts AI flights while they are
# traveling between waypoints. When an AI flight performs an attack # traveling between waypoints. When an AI flight performs an attack
@@ -2067,13 +1895,13 @@ class JoinPointBuilder(PydcsWaypointBuilder):
# for the target area that is set to end on a flag flip that occurs when # for the target area that is set to end on a flag flip that occurs when
# the strike aircraft finish their attack task. # the strike aircraft finish their attack task.
# #
# https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior # https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/250183-task-follow-and-escort-temporarily-aborted
waypoint.add_task( waypoint.add_task(
ControlledTask( ControlledTask(
EngageTargets( EngageTargets(
# TODO: From doctrine. # TODO: From doctrine.
max_distance=int(nautical_miles(30).meters), max_distance=int(nautical_miles(30).meters),
targets=target_types, targets=[Targets.All.Air.Planes.Fighters],
) )
) )
) )
@@ -2091,15 +1919,6 @@ class LandingPointBuilder(PydcsWaypointBuilder):
return waypoint return waypoint
class CargoStopBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
waypoint.type = "LandingReFuAr"
waypoint.action = PointAction.LandingReFuAr
waypoint.landing_refuel_rearm_time = 2 # Minutes.
return waypoint
class RaceTrackBuilder(PydcsWaypointBuilder): class RaceTrackBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint: def build(self) -> MovingPoint:
waypoint = super().build() waypoint = super().build()

View File

@@ -383,8 +383,8 @@ AIRFIELD_DATA = {
"31": ("IVZ", MHz(108, 750)), "31": ("IVZ", MHz(108, 750)),
}, },
), ),
# PERSIAN GULF MAP # TODO : PERSIAN GULF MAP
"Liwa AFB": AirfieldData( "Liwa Airbase": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMLW", icao="OMLW",
elevation=400, elevation=400,
@@ -394,7 +394,7 @@ AIRFIELD_DATA = {
vor=("OMLW", MHz(117, 400)), vor=("OMLW", MHz(117, 400)),
atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(119, 300), MHz(250, 950)), atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(119, 300), MHz(250, 950)),
), ),
"Al Dhafra AFB": AirfieldData( "Al Dhafra AB": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMAM", icao="OMAM",
elevation=52, elevation=52,
@@ -402,63 +402,63 @@ AIRFIELD_DATA = {
tacan=TacanChannel(96, TacanBand.X), tacan=TacanChannel(96, TacanBand.X),
tacan_callsign="MA", tacan_callsign="MA",
vor=("MA", MHz(114, 900)), vor=("MA", MHz(114, 900)),
atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(126, 500), MHz(251, 100)), atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(126, 500), MHz(251, 000)),
ils={ ils={
"13": ("MMA", MHz(111, 100)), "13": ("MMA", MHz(111, 100)),
"31": ("IMA", MHz(109, 100)), "31": ("IMA", MHz(109, 100)),
}, },
), ),
"Al-Bateen": AirfieldData( "Al-Bateen Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMAD", icao="OMAD",
elevation=11, elevation=11,
runway_length=6808, runway_length=6808,
vor=("ALB", MHz(114, 0)), vor=("ALB", MHz(114, 0)),
atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 900), MHz(250, 600)), atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(119, 900), MHz(250, 550)),
), ),
"Sas Al Nakheel": AirfieldData( "Sas Al Nakheel Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMNK", icao="OMNK",
elevation=9, elevation=9,
runway_length=5387, runway_length=5387,
vor=("SAS", MHz(128, 930)), vor=("SAS", MHz(128, 930)),
atc=AtcData(MHz(4, 0), MHz(38, 900), MHz(128, 900), MHz(250, 450)), atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(128, 900), MHz(250, 450)),
), ),
"Abu Dhabi Intl": AirfieldData( "Abu Dhabi International Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMAA", icao="OMAA",
elevation=91, elevation=91,
runway_length=12817, runway_length=12817,
vor=("ADV", MHz(114, 250)), vor=("ADV", MHz(114, 250)),
atc=AtcData(MHz(4, 50), MHz(39, 0), MHz(119, 200), MHz(250, 550)), atc=AtcData(MHz(4, 000), MHz(38, 900), MHz(119, 200), MHz(250, 500)),
), ),
"Al Ain Intl": AirfieldData( "Al Ain International Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMAL", icao="OMAL",
elevation=813, elevation=813,
runway_length=11267, runway_length=11267,
vor=("ALN", MHz(112, 600)), vor=("ALN", MHz(112, 600)),
atc=AtcData(MHz(4, 125), MHz(39, 150), MHz(119, 850), MHz(250, 700)), atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 850), MHz(250, 650)),
), ),
"Al Maktoum Intl": AirfieldData( "Al Maktoum Intl": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMDW", icao="OMDW",
elevation=123, elevation=123,
runway_length=11500, runway_length=11500,
atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(118, 600), MHz(251, 200)), atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(118, 650), MHz(251, 100)),
ils={ ils={
"30": ("IJWA", MHz(109, 750)), "30": ("IJWA", MHz(109, 750)),
"12": ("IMA", MHz(111, 750)), "12": ("IMA", MHz(111, 750)),
}, },
), ),
"Al Minhad AFB": AirfieldData( "Al Minhad Intl": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMDM", icao="OMDM",
elevation=190, elevation=190,
runway_length=11865, runway_length=11865,
tacan=TacanChannel(99, TacanBand.X), tacan=TacanChannel(99, TacanBand.X),
tacan_callsign="MIN", tacan_callsign="MIN",
atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(118, 550), MHz(250, 100)), atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(121, 800), MHz(250, 100)),
ils={ ils={
"27": ("IMNR", MHz(110, 750)), "27": ("IMNR", MHz(110, 750)),
"9": ("IMNW", MHz(110, 700)), "9": ("IMNW", MHz(110, 700)),
@@ -469,7 +469,7 @@ AIRFIELD_DATA = {
icao="OMDB", icao="OMDB",
elevation=16, elevation=16,
runway_length=11018, runway_length=11018,
atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(118, 750), MHz(251, 150)), atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(118, 750), MHz(251, 50)),
ils={ ils={
"30": ("IDBL", MHz(110, 900)), "30": ("IDBL", MHz(110, 900)),
"12": ("IDBR", MHz(110, 100)), "12": ("IDBR", MHz(110, 100)),
@@ -480,7 +480,7 @@ AIRFIELD_DATA = {
icao="OMSJ", icao="OMSJ",
elevation=98, elevation=98,
runway_length=10535, runway_length=10535,
atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(118, 600), MHz(250, 200)), atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(118, 600), MHz(252, 200)),
ils={ ils={
"30": ("ISHW", MHz(111, 950)), "30": ("ISHW", MHz(111, 950)),
"12": ("ISRE", MHz(108, 550)), "12": ("ISRE", MHz(108, 550)),
@@ -492,18 +492,18 @@ AIRFIELD_DATA = {
elevation=60, elevation=60,
runway_length=9437, runway_length=9437,
vor=("FJV", MHz(113, 800)), vor=("FJV", MHz(113, 800)),
atc=AtcData(MHz(4, 375), MHz(39, 650), MHz(124, 600), MHz(251, 250)), atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(124, 600), MHz(251, 150)),
ils={ ils={
"29": ("IFJR", MHz(111, 500)), "29": ("IFJR", MHz(111, 500)),
}, },
), ),
"Ras Al Khaimah Intl": AirfieldData( "Ras AL Khaimah": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OMRK", icao="OMRK",
elevation=70, elevation=70,
runway_length=8406, runway_length=8406,
vor=("OMRK", MHz(113, 600)), vor=("OMRK", MHz(113, 600)),
atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(121, 600), MHz(250, 900)), atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(121, 600), MHz(250, 800)),
), ),
"Khasab": AirfieldData( "Khasab": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
@@ -516,11 +516,7 @@ AIRFIELD_DATA = {
}, },
), ),
"Sir Abu Nuayr": AirfieldData( "Sir Abu Nuayr": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf", icao="OMSN", elevation=25, runway_length=2229
icao="OMSN",
elevation=25,
runway_length=2229,
atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(118, 0), MHz(250, 800)),
), ),
"Sirri Island": AirfieldData( "Sirri Island": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
@@ -530,7 +526,7 @@ AIRFIELD_DATA = {
vor=("SIR", MHz(113, 750)), vor=("SIR", MHz(113, 750)),
atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(135, 50), MHz(250, 250)), atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(135, 50), MHz(250, 250)),
), ),
"Abu Musa Island": AirfieldData( "Abu Musa Island Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OIBA", icao="OIBA",
elevation=16, elevation=16,
@@ -559,13 +555,13 @@ AIRFIELD_DATA = {
vor=("KHM", MHz(117, 100)), vor=("KHM", MHz(117, 100)),
atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)), atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)),
), ),
"Bandar-e-Jask": AirfieldData( "Bandar-e-Jask airfield": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OIZJ", icao="OIZJ",
elevation=26, elevation=26,
runway_length=6842, runway_length=6842,
vor=("KHM", MHz(116, 300)), vor=("KHM", MHz(116, 300)),
atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(118, 150), MHz(250, 500)), atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(118, 50), MHz(250, 150)),
), ),
"Bandar Lengeh": AirfieldData( "Bandar Lengeh": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
@@ -573,26 +569,26 @@ AIRFIELD_DATA = {
elevation=80, elevation=80,
runway_length=7625, runway_length=7625,
vor=("LEN", MHz(114, 800)), vor=("LEN", MHz(114, 800)),
atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(121, 700), MHz(251, 50)), atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 700), MHz(250, 950)),
), ),
"Kish Intl": AirfieldData( "Kish International Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OIBK", icao="OIBK",
elevation=114, elevation=114,
runway_length=10617, runway_length=10617,
tacan=TacanChannel(112, TacanBand.X), tacan=TacanChannel(112, TacanBand.X),
tacan_callsign="KIH", tacan_callsign="KIH",
atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(121, 650), MHz(250, 650)), atc=AtcData(MHz(4, 50), MHz(39, 000), MHz(121, 650), MHz(250, 600)),
), ),
"Lavan Island": AirfieldData( "Lavan Island Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OIBV", icao="OIBV",
elevation=75, elevation=75,
runway_length=8234, runway_length=8234,
vor=("LVA", MHz(116, 850)), vor=("LVA", MHz(116, 850)),
atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(128, 550), MHz(250, 750)), atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(128, 550), MHz(250, 700)),
), ),
"Lar": AirfieldData( "Lar Airbase": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OISL", icao="OISL",
elevation=2635, elevation=2635,
@@ -607,7 +603,7 @@ AIRFIELD_DATA = {
runway_length=7300, runway_length=7300,
tacan=TacanChannel(47, TacanBand.X), tacan=TacanChannel(47, TacanBand.X),
tacan_callsign="HDR", tacan_callsign="HDR",
atc=AtcData(MHz(4, 400), MHz(39, 700), MHz(123, 150), MHz(251, 300)), atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(123, 150), MHz(251, 200)),
ils={ ils={
"8": ("IBHD", MHz(108, 900)), "8": ("IBHD", MHz(108, 900)),
}, },
@@ -620,19 +616,19 @@ AIRFIELD_DATA = {
tacan=TacanChannel(78, TacanBand.X), tacan=TacanChannel(78, TacanBand.X),
tacan_callsign="BND", tacan_callsign="BND",
vor=("BND", MHz(117, 200)), vor=("BND", MHz(117, 200)),
atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(118, 100), MHz(251, 0)), atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(118, 100), MHz(250, 900)),
ils={ ils={
"21": ("IBND", MHz(109, 900)), "21": ("IBND", MHz(333, 800)),
}, },
), ),
"Jiroft": AirfieldData( "Jiroft Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OIKJ", icao="OIKJ",
elevation=2664, elevation=2664,
runway_length=9160, runway_length=9160,
atc=AtcData(MHz(4, 125), MHz(39, 120), MHz(136, 0), MHz(250, 750)), atc=AtcData(MHz(4, 125), MHz(39, 120), MHz(136, 0), MHz(250, 750)),
), ),
"Kerman": AirfieldData( "Kerman Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OIKK", icao="OIKK",
elevation=5746, elevation=5746,
@@ -640,9 +636,9 @@ AIRFIELD_DATA = {
tacan=TacanChannel(97, TacanBand.X), tacan=TacanChannel(97, TacanBand.X),
tacan_callsign="KER", tacan_callsign="KER",
vor=("KER", MHz(112, 0)), vor=("KER", MHz(112, 0)),
atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(118, 250), MHz(250, 300)), atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(118, 250), MHz(250, 300)),
), ),
"Shiraz Intl": AirfieldData( "Shiraz International Airport": AirfieldData(
theater="Persian Gulf", theater="Persian Gulf",
icao="OISS", icao="OISS",
elevation=4878, elevation=4878,
@@ -650,7 +646,7 @@ AIRFIELD_DATA = {
tacan=TacanChannel(94, TacanBand.X), tacan=TacanChannel(94, TacanBand.X),
tacan_callsign="SYZ1", tacan_callsign="SYZ1",
vor=("SYZ", MHz(112, 0)), vor=("SYZ", MHz(112, 0)),
atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(121, 900), MHz(250, 350)), atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(121, 900), MHz(250, 350)),
), ),
# Syria Map # Syria Map
"Adana Sakirpasa": AirfieldData( "Adana Sakirpasa": AirfieldData(
@@ -659,7 +655,7 @@ AIRFIELD_DATA = {
elevation=55, elevation=55,
runway_length=8115, runway_length=8115,
vor=("ADA", MHz(112, 700)), vor=("ADA", MHz(112, 700)),
atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(121, 100), MHz(251, 0)), atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 100), MHz(250, 900)),
ils={ ils={
"05": ("IADA", MHz(108, 700)), "05": ("IADA", MHz(108, 700)),
}, },
@@ -672,9 +668,9 @@ AIRFIELD_DATA = {
tacan=TacanChannel(21, TacanBand.X), tacan=TacanChannel(21, TacanBand.X),
tacan_callsign="DAN", tacan_callsign="DAN",
vor=("DAN", MHz(108, 400)), vor=("DAN", MHz(108, 400)),
atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(122, 100), MHz(360, 100)), atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(129, 400), MHz(360, 100)),
ils={ ils={
"05": ("IDAN", MHz(109, 300)), "50": ("IDAN", MHz(109, 300)),
"23": ("DANM", MHz(111, 700)), "23": ("DANM", MHz(111, 700)),
}, },
), ),
@@ -683,7 +679,7 @@ AIRFIELD_DATA = {
icao="OS71", icao="OS71",
elevation=1614, elevation=1614,
runway_length=4648, runway_length=4648,
atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(120, 600), MHz(250, 800)), atc=AtcData(MHz(4, 125), MHz(39, 150), MHz(120, 600), MHz(250, 700)),
), ),
"Hatay": AirfieldData( "Hatay": AirfieldData(
theater="Syria", theater="Syria",
@@ -691,7 +687,7 @@ AIRFIELD_DATA = {
elevation=253, elevation=253,
runway_length=9052, runway_length=9052,
vor=("HTY", MHz(112, 500)), vor=("HTY", MHz(112, 500)),
atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(128, 500), MHz(250, 250)), atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(128, 500), MHz(250, 150)),
ils={ ils={
"22": ("IHTY", MHz(108, 150)), "22": ("IHTY", MHz(108, 150)),
"04": ("IHAT", MHz(108, 900)), "04": ("IHAT", MHz(108, 900)),
@@ -702,21 +698,25 @@ AIRFIELD_DATA = {
icao="OS66", icao="OS66",
elevation=1200, elevation=1200,
runway_length=6662, runway_length=6662,
atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(120, 500), MHz(251, 100)), atc=AtcData(MHz(4, 275), MHz(39, 450), MHz(120, 500), MHz(251)),
), ),
"Aleppo": AirfieldData( "Aleppo": AirfieldData(
theater="Syria", theater="Syria",
icao="OSAP", icao="OSAP",
elevation=1253, elevation=1253,
runway_length=8332, runway_length=8332,
atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(119, 100), MHz(250, 850)), atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(119, 100), MHz(250, 750)),
ils={
"50": ("IDAN", MHz(109, 300)),
"23": ("DANM", MHz(111, 700)),
},
), ),
"Jirah": AirfieldData( "Jirah": AirfieldData(
theater="Syria", theater="Syria",
icao="OS62", icao="OS62",
elevation=1170, elevation=1170,
runway_length=9090, runway_length=9090,
atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(118, 100), MHz(250, 300)), atc=AtcData(MHz(3, 875), MHz(38, 650), MHz(118, 100), MHz(250, 200)),
), ),
"Taftanaz": AirfieldData( "Taftanaz": AirfieldData(
theater="Syria", theater="Syria",
@@ -729,14 +729,14 @@ AIRFIELD_DATA = {
icao="OS59", icao="OS59",
elevation=1083, elevation=1083,
runway_length=9036, runway_length=9036,
atc=AtcData(MHz(4, 500), MHz(39, 900), MHz(122, 800), MHz(251, 450)), atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(118, 500), MHz(251, 150)),
), ),
"Abu al-Dahur": AirfieldData( "Abu al-Dahur": AirfieldData(
theater="Syria", theater="Syria",
icao="OS57", icao="OS57",
elevation=820, elevation=820,
runway_length=8728, runway_length=8728,
atc=AtcData(MHz(4, 0), MHz(38, 900), MHz(122, 200), MHz(250, 450)), atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(122, 200), MHz(250, 350)),
), ),
"Bassel Al-Assad": AirfieldData( "Bassel Al-Assad": AirfieldData(
theater="Syria", theater="Syria",
@@ -744,7 +744,7 @@ AIRFIELD_DATA = {
elevation=93, elevation=93,
runway_length=7305, runway_length=7305,
vor=("LTK", MHz(114, 800)), vor=("LTK", MHz(114, 800)),
atc=AtcData(MHz(4, 50), MHz(39, 0), MHz(118, 100), MHz(250, 550)), atc=AtcData(MHz(4), MHz(38, 900), MHz(118, 100), MHz(250, 450)),
ils={ ils={
"17": ("IBA", MHz(109, 100)), "17": ("IBA", MHz(109, 100)),
}, },
@@ -754,28 +754,28 @@ AIRFIELD_DATA = {
icao="OS58", icao="OS58",
elevation=983, elevation=983,
runway_length=7957, runway_length=7957,
atc=AtcData(MHz(3, 850), MHz(38, 600), MHz(118, 50), MHz(250, 200)), atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(118, 50), MHz(250, 100)),
), ),
"Rene Mouawad": AirfieldData( "Rene Mouawad": AirfieldData(
theater="Syria", theater="Syria",
icao="OLKA", icao="OLKA",
elevation=14, elevation=14,
runway_length=8614, runway_length=8614,
atc=AtcData(MHz(4, 375), MHz(39, 650), MHz(121, 0), MHz(251, 200)), atc=AtcData(MHz(4, 325), MHz(39, 550), MHz(129, 500), MHz(251, 100)),
), ),
"Al Quasayr": AirfieldData( "Al Quasayr": AirfieldData(
theater="Syria", theater="Syria",
icao="OS70", icao="OS70",
elevation=1729, elevation=1729,
runway_length=8585, runway_length=8585,
atc=AtcData(MHz(4, 550), MHz(40, 0), MHz(119, 200), MHz(251, 550)), atc=AtcData(MHz(4, 400), MHz(39, 700), MHz(119, 200), MHz(251, 250)),
), ),
"Palmyra": AirfieldData( "Palmyra": AirfieldData(
theater="Syria", theater="Syria",
icao="OSPR", icao="OSPR",
elevation=1267, elevation=1267,
runway_length=8704, runway_length=8704,
atc=AtcData(MHz(4, 225), MHz(39, 350), MHz(121, 900), MHz(250, 900)), atc=AtcData(MHz(4, 175), MHz(39, 250), MHz(121, 900), MHz(250, 800)),
), ),
"Wujah Al Hajar": AirfieldData( "Wujah Al Hajar": AirfieldData(
theater="Syria", theater="Syria",
@@ -783,14 +783,14 @@ AIRFIELD_DATA = {
elevation=619, elevation=619,
runway_length=4717, runway_length=4717,
vor=("CAK", MHz(116, 200)), vor=("CAK", MHz(116, 200)),
atc=AtcData(MHz(4, 575), MHz(40, 50), MHz(121, 500), MHz(251, 600)), atc=AtcData(MHz(4, 425), MHz(39, 750), MHz(121, 500), MHz(251, 300)),
), ),
"An Nasiriyah": AirfieldData( "An Nasiriyah": AirfieldData(
theater="Syria", theater="Syria",
icao="OS64", icao="OS64",
elevation=2746, elevation=2746,
runway_length=8172, runway_length=8172,
atc=AtcData(MHz(4, 600), MHz(40, 100), MHz(122, 300), MHz(251, 650)), atc=AtcData(MHz(4, 450), MHz(39, 800), MHz(122, 300), MHz(251, 350)),
), ),
"Rayak": AirfieldData( "Rayak": AirfieldData(
theater="Syria", theater="Syria",
@@ -798,7 +798,7 @@ AIRFIELD_DATA = {
elevation=2934, elevation=2934,
runway_length=8699, runway_length=8699,
vor=("HTY", MHz(124, 400)), vor=("HTY", MHz(124, 400)),
atc=AtcData(MHz(4, 350), MHz(39, 600), MHz(124, 400), MHz(251, 150)), atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(124, 400), MHz(251, 50)),
), ),
"Beirut-Rafic Hariri": AirfieldData( "Beirut-Rafic Hariri": AirfieldData(
theater="Syria", theater="Syria",
@@ -806,7 +806,7 @@ AIRFIELD_DATA = {
elevation=39, elevation=39,
runway_length=9463, runway_length=9463,
vor=("KAD", MHz(112, 600)), vor=("KAD", MHz(112, 600)),
atc=AtcData(MHz(4, 675), MHz(40, 250), MHz(118, 900), MHz(251, 800)), atc=AtcData(MHz(4, 475), MHz(39, 850), MHz(118, 900), MHz(251, 400)),
ils={ ils={
"17": ("BIL", MHz(109, 500)), "17": ("BIL", MHz(109, 500)),
}, },
@@ -816,32 +816,32 @@ AIRFIELD_DATA = {
icao="OS61", icao="OS61",
elevation=2066, elevation=2066,
runway_length=8902, runway_length=8902,
atc=AtcData(MHz(4, 750), MHz(40, 400), MHz(120, 300), MHz(251, 950)), atc=AtcData(MHz(4, 550), MHz(40), MHz(120, 300), MHz(251, 550)),
), ),
"Marj as Sultan North": AirfieldData( "Marj as Sultan North": AirfieldData(
theater="Syria", theater="Syria",
elevation=2007, elevation=2007,
runway_length=268, runway_length=268,
atc=AtcData(MHz(4, 75), MHz(38, 50), MHz(122, 700), MHz(250, 600)), atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(122, 700), MHz(250, 500)),
), ),
"Marj as Sultan South": AirfieldData( "Marj as Sultan South": AirfieldData(
theater="Syria", theater="Syria",
elevation=2007, elevation=2007,
runway_length=166, runway_length=166,
atc=AtcData(MHz(4, 725), MHz(40, 350), MHz(122, 900), MHz(251, 900)), atc=AtcData(MHz(4, 525), MHz(39, 950), MHz(122, 900), MHz(251, 500)),
), ),
"Mezzeh": AirfieldData( "Mezzeh": AirfieldData(
theater="Syria", theater="Syria",
icao="OS67", icao="OS67",
elevation=2355, elevation=2355,
runway_length=7522, runway_length=7522,
atc=AtcData(MHz(4, 150), MHz(39, 200), MHz(120, 700), MHz(250, 750)), atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(120, 700), MHz(250, 650)),
), ),
"Qabr as Sitt": AirfieldData( "Qabr as Sitt": AirfieldData(
theater="Syria", theater="Syria",
elevation=2134, elevation=2134,
runway_length=489, runway_length=489,
atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(122, 600), MHz(250, 950)), atc=AtcData(MHz(4, 200), MHz(39, 300), MHz(122, 600), MHz(250, 850)),
), ),
"Damascus": AirfieldData( "Damascus": AirfieldData(
theater="Syria", theater="Syria",
@@ -849,7 +849,7 @@ AIRFIELD_DATA = {
elevation=2007, elevation=2007,
runway_length=11423, runway_length=11423,
vor=("DAM", MHz(116)), vor=("DAM", MHz(116)),
atc=AtcData(MHz(4, 700), MHz(40, 300), MHz(118, 500), MHz(251, 850)), atc=AtcData(MHz(4, 500), MHz(39, 900), MHz(118, 500), MHz(251, 450)),
ils={ ils={
"24": ("IDA", MHz(109, 900)), "24": ("IDA", MHz(109, 900)),
}, },
@@ -859,42 +859,42 @@ AIRFIELD_DATA = {
icao="OS63", icao="OS63",
elevation=2160, elevation=2160,
runway_length=7576, runway_length=7576,
atc=AtcData(MHz(4, 100), MHz(39, 100), MHz(120, 800), MHz(250, 6550)), atc=AtcData(MHz(4, 50), MHz(39), MHz(120, 800), MHz(250, 550)),
), ),
"Kiryat Shmona": AirfieldData( "Kiryat Shmona": AirfieldData(
theater="Syria", theater="Syria",
icao="LLKS", icao="LLKS",
elevation=328, elevation=328,
runway_length=3258, runway_length=3258,
atc=AtcData(MHz(4, 25), MHz(38, 950), MHz(118, 400), MHz(250, 500)), atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(118, 400), MHz(250, 400)),
), ),
"Khalkhalah": AirfieldData( "Khalkhalah": AirfieldData(
theater="Syria", theater="Syria",
icao="OS69", icao="OS69",
elevation=2337, elevation=2337,
runway_length=8248, runway_length=8248,
atc=AtcData(MHz(3, 950), MHz(38, 800), MHz(122, 500), MHz(250, 350)), atc=AtcData(MHz(3, 900), MHz(38, 700), MHz(122, 500), MHz(250, 250)),
), ),
"Haifa": AirfieldData( "Haifa": AirfieldData(
theater="Syria", theater="Syria",
icao="LLHA", icao="LLHA",
elevation=19, elevation=19,
runway_length=3253, runway_length=3253,
atc=AtcData(MHz(3, 825), MHz(38, 550), MHz(127, 800), MHz(250, 150)), atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(127, 800), MHz(250, 50)),
), ),
"Ramat David": AirfieldData( "Ramat David": AirfieldData(
theater="Syria", theater="Syria",
icao="LLRD", icao="LLRD",
elevation=105, elevation=105,
runway_length=7037, runway_length=7037,
atc=AtcData(MHz(4, 300), MHz(39, 500), MHz(118, 600), MHz(251, 50)), atc=AtcData(MHz(4, 250), MHz(39, 400), MHz(118, 600), MHz(250, 950)),
), ),
"Megiddo": AirfieldData( "Megiddo": AirfieldData(
theater="Syria", theater="Syria",
icao="LLMG", icao="LLMG",
elevation=180, elevation=180,
runway_length=6098, runway_length=6098,
atc=AtcData(MHz(4, 125), MHz(39, 150), MHz(119, 900), MHz(250, 700)), atc=AtcData(MHz(4, 75), MHz(39, 50), MHz(119, 900), MHz(250, 600)),
), ),
"Eyn Shemer": AirfieldData( "Eyn Shemer": AirfieldData(
theater="Syria", theater="Syria",
@@ -908,66 +908,7 @@ AIRFIELD_DATA = {
icao="OJMF", icao="OJMF",
elevation=2204, elevation=2204,
runway_length=8595, runway_length=8595,
atc=AtcData(MHz(3, 975), MHz(38, 850), MHz(118, 300), MHz(250, 400)), atc=AtcData(MHz(3, 925), MHz(38, 750), MHz(118, 300), MHz(250, 300)),
),
"Tha'lah": AirfieldData(
theater="Syria",
icao="OS60",
elevation=2381,
runway_length=8025,
atc=AtcData(MHz(4, 650), MHz(40, 200), MHz(122, 400), MHz(251, 750)),
),
"Shayrat": AirfieldData(
theater="Syria",
icao="OS60",
elevation=2637,
runway_length=8553,
atc=AtcData(MHz(4, 450), MHz(39, 800), MHz(122, 200), MHz(251, 350)),
),
"Tiyas": AirfieldData(
theater="Syria",
icao="OS72",
elevation=1797,
runway_length=9420,
atc=AtcData(MHz(4, 525), MHz(39, 950), MHz(120, 500), MHz(251, 500)),
),
"Rosh Pina": AirfieldData(
theater="Syria",
icao="LLIB",
elevation=865,
runway_length=2711,
atc=AtcData(MHz(4, 400), MHz(39, 700), MHz(118, 450), MHz(251, 250)),
),
"Sayqal": AirfieldData(
theater="Syria",
icao="OS68",
elevation=2273,
runway_length=8536,
atc=AtcData(MHz(4, 425), MHz(39, 750), MHz(120, 400), MHz(251, 300)),
),
"H4": AirfieldData(
theater="Syria",
icao="OJHR",
elevation=2257,
runway_length=7179,
atc=AtcData(MHz(3, 800), MHz(38, 500), MHz(120, 400), MHz(250, 100)),
),
"Naqoura": AirfieldData(
theater="Syria",
icao="",
elevation=378,
runway_length=0,
atc=AtcData(MHz(4, 625), MHz(40, 150), MHz(122, 000), MHz(251, 700)),
),
"Gaziantep": AirfieldData(
theater="Syria",
icao="LTAJ",
elevation=2287,
runway_length=8871,
atc=AtcData(MHz(3, 775), MHz(38, 450), MHz(120, 100), MHz(250, 50)),
ils={
"28": ("IGNP", MHz(109, 10)),
},
), ),
# NTTR # NTTR
"Mina Airport 3Q0": AirfieldData( "Mina Airport 3Q0": AirfieldData(
@@ -1363,73 +1304,55 @@ AIRFIELD_DATA = {
"Detling": AirfieldData( "Detling": AirfieldData(
theater="Channel", theater="Channel",
elevation=623, elevation=623,
runway_length=3482, runway_length=2557,
atc=AtcData(MHz(4, 50), MHz(118, 600), MHz(39, 0), MHz(250, 600)), atc=AtcData(MHz(3, 950), MHz(118, 400), MHz(38, 800), MHz(250, 400)),
), ),
"High Halden": AirfieldData( "High Halden": AirfieldData(
theater="Channel", theater="Channel",
elevation=104, elevation=104,
runway_length=3296, runway_length=3296,
atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)), atc=AtcData(MHz(3, 750), MHz(118, 800), MHz(38, 400), MHz(250, 0)),
), ),
"Lympne": AirfieldData( "Lympne": AirfieldData(
theater="Channel", theater="Channel",
elevation=351, elevation=351,
runway_length=3054, runway_length=2548,
atc=AtcData(MHz(4, 25), MHz(118, 550), MHz(38, 950), MHz(250, 550)), atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)),
), ),
"Hawkinge": AirfieldData( "Hawkinge": AirfieldData(
theater="Channel", theater="Channel",
elevation=524, elevation=524,
runway_length=3013, runway_length=3013,
atc=AtcData(MHz(4, 0), MHz(118, 500), MHz(38, 900), MHz(250, 500)), atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)),
), ),
"Manston": AirfieldData( "Manston": AirfieldData(
theater="Channel", theater="Channel",
elevation=160, elevation=160,
runway_length=8626, runway_length=8626,
atc=AtcData(MHz(3, 975), MHz(118, 250), MHz(38, 650), MHz(250, 250)), atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)),
), ),
"Dunkirk Mardyck": AirfieldData( "Dunkirk Mardyck": AirfieldData(
theater="Channel", theater="Channel",
elevation=16, elevation=16,
runway_length=1737, runway_length=1737,
atc=AtcData(MHz(3, 950), MHz(118, 450), MHz(38, 850), MHz(250, 450)), atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
), ),
"Saint Omer Longuenesse": AirfieldData( "Saint Omer Longuenesse": AirfieldData(
theater="Channel", theater="Channel",
elevation=219, elevation=219,
runway_length=1929, runway_length=1929,
atc=AtcData(MHz(3, 925), MHz(118, 350), MHz(38, 750), MHz(250, 350)), atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)),
), ),
"Merville Calonne": AirfieldData( "Merville Calonne": AirfieldData(
theater="Channel", theater="Channel",
elevation=52, elevation=52,
runway_length=7580, runway_length=7580,
atc=AtcData(MHz(3, 900), MHz(118, 300), MHz(38, 700), MHz(250, 300)), atc=AtcData(MHz(3, 800), MHz(118, 100), MHz(38, 500), MHz(250, 100)),
), ),
"Abbeville Drucat": AirfieldData( "Abbeville Drucat": AirfieldData(
theater="Channel", theater="Channel",
elevation=183, elevation=183,
runway_length=4726, runway_length=4726,
atc=AtcData(MHz(3, 875), MHz(118, 250), MHz(38, 650), MHz(250, 250)),
),
"Eastchurch": AirfieldData(
theater="Channel",
elevation=30,
runway_length=2983,
atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)), atc=AtcData(MHz(3, 775), MHz(118, 50), MHz(38, 450), MHz(250, 50)),
), ),
"Headcorn": AirfieldData(
theater="Channel",
elevation=114,
runway_length=3680,
atc=AtcData(MHz(3, 825), MHz(118, 150), MHz(38, 550), MHz(250, 150)),
),
"Biggin Hill": AirfieldData(
theater="Channel",
elevation=552,
runway_length=3953,
atc=AtcData(MHz(3, 850), MHz(118, 200), MHz(38, 600), MHz(250, 200)),
),
} }

View File

@@ -1,7 +1,6 @@
import logging import logging
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import timedelta from typing import List, Type, Tuple
from typing import List, Type, Tuple, Optional
from dcs.mission import Mission, StartType from dcs.mission import Mission, StartType
from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135 from dcs.planes import IL_78M, KC130, KC135MPRS, KC_135
@@ -22,7 +21,6 @@ from .conflictgen import Conflict
from .radios import RadioFrequency, RadioRegistry from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
TANKER_DISTANCE = 15000 TANKER_DISTANCE = 15000
TANKER_ALT = 4572 TANKER_ALT = 4572
TANKER_HEADING_OFFSET = 45 TANKER_HEADING_OFFSET = 45
@@ -35,25 +33,20 @@ AWACS_ALT = 13000
class AwacsInfo: class AwacsInfo:
"""AWACS information for the kneeboard.""" """AWACS information for the kneeboard."""
group_name: str dcsGroupName: str
callsign: str callsign: str
freq: RadioFrequency freq: RadioFrequency
depature_location: Optional[str]
start_time: Optional[timedelta]
end_time: Optional[timedelta]
blue: bool
@dataclass @dataclass
class TankerInfo: class TankerInfo:
"""Tanker information for the kneeboard.""" """Tanker information for the kneeboard."""
group_name: str dcsGroupName: str
callsign: str callsign: str
variant: str variant: str
freq: RadioFrequency freq: RadioFrequency
tacan: TacanChannel tacan: TacanChannel
blue: bool
@dataclass @dataclass
@@ -94,9 +87,9 @@ class AirSupportConflictGenerator:
def generate(self): def generate(self):
player_cp = ( player_cp = (
self.conflict.blue_cp self.conflict.from_cp
if self.conflict.blue_cp.captured if self.conflict.from_cp.captured
else self.conflict.red_cp else self.conflict.to_cp
) )
fallback_tanker_number = 0 fallback_tanker_number = 0
@@ -109,8 +102,8 @@ class AirSupportConflictGenerator:
freq = self.radio_registry.alloc_uhf() freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = ( tanker_heading = (
self.conflict.red_cp.position.heading_between_point( self.conflict.to_cp.position.heading_between_point(
self.conflict.blue_cp.position self.conflict.from_cp.position
) )
+ TANKER_HEADING_OFFSET * i + TANKER_HEADING_OFFSET * i
) )
@@ -167,52 +160,40 @@ class AirSupportConflictGenerator:
tanker_group.points[0].tasks.append(SetImmortalCommand(True)) tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append( self.air_support.tankers.append(
TankerInfo( TankerInfo(str(tanker_group.name), callsign, variant, freq, tacan)
str(tanker_group.name),
callsign,
variant,
freq,
tacan,
blue=True,
)
) )
if not self.game.settings.disable_legacy_aewc: possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
possible_awacs = db.find_unittype(AWACS, self.conflict.attackers_side)
if len(possible_awacs) > 0: if len(possible_awacs) > 0:
awacs_unit = possible_awacs[0] awacs_unit = possible_awacs[0]
freq = self.radio_registry.alloc_uhf() freq = self.radio_registry.alloc_uhf()
awacs_flight = self.mission.awacs_flight( awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country), country=self.mission.country(self.game.player_country),
name=namegen.next_awacs_name( name=namegen.next_awacs_name(
self.mission.country(self.game.player_country) self.mission.country(self.game.player_country)
), ),
plane_type=awacs_unit, plane_type=awacs_unit,
altitude=AWACS_ALT, altitude=AWACS_ALT,
airport=None, airport=None,
position=self.conflict.position.random_point_within( position=self.conflict.position.random_point_within(
AWACS_DISTANCE, AWACS_DISTANCE AWACS_DISTANCE, AWACS_DISTANCE
), ),
frequency=freq.mhz, frequency=freq.mhz,
start_type=StartType.Warm, start_type=StartType.Warm,
)
awacs_flight.set_frequency(freq.mhz)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(
AwacsInfo(
str(awacs_flight.name),
callsign_for_support_unit(awacs_flight),
freq,
) )
awacs_flight.set_frequency(freq.mhz) )
else:
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True)) logging.warning("No AWACS for faction")
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.awacs.append(
AwacsInfo(
group_name=str(awacs_flight.name),
callsign=callsign_for_support_unit(awacs_flight),
freq=freq,
depature_location=None,
start_time=None,
end_time=None,
blue=True,
)
)
else:
logging.warning("No AWACS for faction")

View File

@@ -68,12 +68,11 @@ INFANTRY_GROUP_SIZE = 5
class JtacInfo: class JtacInfo:
"""JTAC information.""" """JTAC information."""
group_name: str dcsGroupName: str
unit_name: str unit_name: str
callsign: str callsign: str
region: str region: str
code: str code: str
blue: bool
# TODO: Radio info? Type? # TODO: Radio info? Type?
@@ -135,10 +134,10 @@ class GroundConflictGenerator:
def generate(self): def generate(self):
position = Conflict.frontline_position( position = Conflict.frontline_position(
self.conflict.front_line, self.game.theater self.conflict.from_cp, self.conflict.to_cp, self.game.theater
) )
frontline_vector = Conflict.frontline_vector( frontline_vector = Conflict.frontline_vector(
self.conflict.front_line, self.game.theater self.conflict.from_cp, self.conflict.to_cp, self.game.theater
) )
# Create player groups at random position # Create player groups at random position
@@ -157,21 +156,21 @@ class GroundConflictGenerator:
player_groups, player_groups,
enemy_groups, enemy_groups,
self.conflict.heading + 90, self.conflict.heading + 90,
self.conflict.blue_cp, self.conflict.from_cp,
self.conflict.red_cp, self.conflict.to_cp,
) )
self.plan_action_for_groups( self.plan_action_for_groups(
self.enemy_stance, self.enemy_stance,
enemy_groups, enemy_groups,
player_groups, player_groups,
self.conflict.heading - 90, self.conflict.heading - 90,
self.conflict.red_cp, self.conflict.to_cp,
self.conflict.blue_cp, self.conflict.from_cp,
) )
# Add JTAC # Add JTAC
if self.game.player_faction.has_jtac: if self.game.player_faction.has_jtac:
n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id) n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.jtacs) code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper utype = MQ_9_Reaper
@@ -192,19 +191,12 @@ class GroundConflictGenerator:
OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle) OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle)
) )
frontline = ( frontline = (
f"Frontline {self.conflict.blue_cp.name}/{self.conflict.red_cp.name}" f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
) )
# Note: Will need to change if we ever add ground based JTAC. # Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac) callsign = callsign_for_support_unit(jtac)
self.jtacs.append( self.jtacs.append(
JtacInfo( JtacInfo(str(jtac.name), n, callsign, frontline, str(code))
str(jtac.name),
n,
callsign,
frontline,
str(code),
blue=True,
)
) )
def gen_infantry_group_for_group( def gen_infantry_group_for_group(
@@ -221,9 +213,9 @@ class GroundConflictGenerator:
logging.warning("Could not find infantry position") logging.warning("Could not find infantry position")
return return
if side == self.conflict.attackers_country: if side == self.conflict.attackers_country:
cp = self.conflict.blue_cp cp = self.conflict.from_cp
else: else:
cp = self.conflict.red_cp cp = self.conflict.to_cp
if is_player: if is_player:
faction = self.game.player_name faction = self.game.player_name
@@ -790,9 +782,9 @@ class GroundConflictGenerator:
) -> VehicleGroup: ) -> VehicleGroup:
if side == self.conflict.attackers_country: if side == self.conflict.attackers_country:
cp = self.conflict.blue_cp cp = self.conflict.from_cp
else: else:
cp = self.conflict.red_cp cp = self.conflict.to_cp
logging.info("armorgen: {} for {}".format(unit, side.id)) logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group( group = self.mission.vehicle_group(

View File

@@ -8,8 +8,6 @@ example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
the single CAP flight. the single CAP flight.
""" """
from __future__ import annotations
import logging import logging
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
@@ -67,10 +65,6 @@ class Package:
waypoints: Optional[PackageWaypoints] = field(default=None) waypoints: Optional[PackageWaypoints] = field(default=None)
@property
def has_players(self) -> bool:
return any(flight.client_count for flight in self.flights)
@property @property
def formation_speed(self) -> Optional[Speed]: def formation_speed(self) -> Optional[Speed]:
"""The speed of the package when in formation. """The speed of the package when in formation.
@@ -178,11 +172,9 @@ class Package:
FlightType.OCA_RUNWAY, FlightType.OCA_RUNWAY,
FlightType.BAI, FlightType.BAI,
FlightType.DEAD, FlightType.DEAD,
FlightType.TRANSPORT,
FlightType.SEAD, FlightType.SEAD,
FlightType.TARCAP, FlightType.TARCAP,
FlightType.BARCAP, FlightType.BARCAP,
FlightType.AEWC,
FlightType.SWEEP, FlightType.SWEEP,
FlightType.ESCORT, FlightType.ESCORT,
] ]

View File

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

View File

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

View File

@@ -1,31 +0,0 @@
import logging
import random
from game import db
from gen.coastal.silkworm import SilkwormGenerator
COASTAL_MAP = {
"SilkwormGenerator": SilkwormGenerator,
}
def generate_coastal_group(game, ground_object, faction_name: str):
"""
This generate a coastal defenses group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction_name]
if len(faction.coastal_defenses) > 0:
generators = faction.coastal_defenses
if len(generators) > 0:
gen = random.choice(generators)
if gen in COASTAL_MAP.keys():
generator = COASTAL_MAP[gen](game, ground_object, faction)
generator.generate()
return generator.get_generated_group()
else:
logging.info(
"Unable to generate missile group, generator : "
+ str(gen)
+ "does not exists"
)
return None

View File

@@ -1,58 +0,0 @@
from dcs.vehicles import MissilesSS, Unarmed, AirDefence
from gen.sam.group_generator import GroupGenerator
class SilkwormGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(SilkwormGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
positions = self.get_circular_position(5, launcher_distance=120, coverage=180)
self.add_unit(
MissilesSS.AShM_Silkworm_SR,
"SR#0",
self.position.x,
self.position.y,
self.heading,
)
# Launchers
for i, p in enumerate(positions):
self.add_unit(
MissilesSS.AShM_SS_N_2_Silkworm,
"Missile#" + str(i),
p[0],
p[1],
self.heading,
)
# Commander
self.add_unit(
Unarmed.Truck_KAMAZ_43101,
"KAMAZ#0",
self.position.x - 35,
self.position.y - 20,
self.heading,
)
# Shorad
self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish,
"SHILKA#0",
self.position.x - 55,
self.position.y - 38,
self.heading,
)
# Shorad 2
self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL,
"STRELA#0",
self.position.x + 200,
self.position.y + 15,
90,
)

View File

@@ -17,7 +17,8 @@ class Conflict:
def __init__( def __init__(
self, self,
theater: ConflictTheater, theater: ConflictTheater,
front_line: FrontLine, from_cp: ControlPoint,
to_cp: ControlPoint,
attackers_side: str, attackers_side: str,
defenders_side: str, defenders_side: str,
attackers_country: Country, attackers_country: Country,
@@ -32,28 +33,22 @@ class Conflict:
self.attackers_country = attackers_country self.attackers_country = attackers_country
self.defenders_country = defenders_country self.defenders_country = defenders_country
self.front_line = front_line self.from_cp = from_cp
self.to_cp = to_cp
self.theater = theater self.theater = theater
self.position = position self.position = position
self.heading = heading self.heading = heading
self.size = size self.size = size
@property
def blue_cp(self) -> ControlPoint:
return self.front_line.blue_cp
@property
def red_cp(self) -> ControlPoint:
return self.front_line.red_cp
@classmethod @classmethod
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool: def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline return from_cp.has_frontline and to_cp.has_frontline
@classmethod @classmethod
def frontline_position( def frontline_position(
cls, frontline: FrontLine, theater: ConflictTheater cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
) -> Tuple[Point, int]: ) -> Tuple[Point, int]:
frontline = FrontLine(from_cp, to_cp, theater)
attack_heading = frontline.attack_heading attack_heading = frontline.attack_heading
position = cls.find_ground_position( position = cls.find_ground_position(
frontline.position, frontline.position,
@@ -65,12 +60,12 @@ class Conflict:
@classmethod @classmethod
def frontline_vector( def frontline_vector(
cls, front_line: FrontLine, theater: ConflictTheater cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater
) -> Tuple[Point, int, int]: ) -> Tuple[Point, int, int]:
""" """
Returns a vector for a valid frontline location avoiding exclusion zones. Returns a vector for a valid frontline location avoiding exclusion zones.
""" """
center_position, heading = cls.frontline_position(front_line, theater) center_position, heading = cls.frontline_position(from_cp, to_cp, theater)
left_heading = heading_sum(heading, -90) left_heading = heading_sum(heading, -90)
right_heading = heading_sum(heading, 90) right_heading = heading_sum(heading, 90)
left_position = cls.extend_ground_position( left_position = cls.extend_ground_position(
@@ -89,16 +84,18 @@ class Conflict:
defender_name: str, defender_name: str,
attacker: Country, attacker: Country,
defender: Country, defender: Country,
front_line: FrontLine, from_cp: ControlPoint,
to_cp: ControlPoint,
theater: ConflictTheater, theater: ConflictTheater,
): ):
assert cls.has_frontline_between(front_line.blue_cp, front_line.red_cp) assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(front_line, theater) position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
conflict = cls( conflict = cls(
position=position, position=position,
heading=heading, heading=heading,
theater=theater, theater=theater,
front_line=front_line, from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name, attackers_side=attacker_name,
defenders_side=defender_name, defenders_side=defender_name,
attackers_country=attacker, attackers_country=attacker,

View File

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

View File

@@ -17,7 +17,6 @@ class EnvironmentGenerator:
self.mission.weather.clouds_thickness = clouds.thickness self.mission.weather.clouds_thickness = clouds.thickness
self.mission.weather.clouds_density = clouds.density self.mission.weather.clouds_density = clouds.density
self.mission.weather.clouds_iprecptns = clouds.precipitation self.mission.weather.clouds_iprecptns = clouds.precipitation
self.mission.weather.clouds_preset = clouds.preset
def set_fog(self, fog: Optional[Fog]) -> None: def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None: if fog is None:

View File

@@ -2,122 +2,50 @@ import random
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.ships import DDG_Arleigh_Burke_IIa, CG_Ticonderoga
class CarrierGroupGenerator(ShipGroupGenerator): class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self): def generate(self):
# Carrier Strike Group 8 # Add carrier
if self.faction.carrier_names[0] == "Carrier Strike Group 8": if len(self.faction.aircraft_carrier) > 0:
carrier_type = random.choice(self.faction.aircraft_carrier) carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit( self.add_unit(
carrier_type, carrier_type, "Carrier", self.position.x, self.position.y, self.heading
"CVN-75 Harry S. Truman",
self.position.x,
self.position.y,
self.heading,
) )
# Add Arleigh Burke escort
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Ramage",
self.position.x + 6482,
self.position.y + 6667,
self.heading,
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Mitscher",
self.position.x - 7963,
self.position.y + 7037,
self.heading,
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Forrest Sherman",
self.position.x - 7408,
self.position.y - 7408,
self.heading,
)
self.add_unit(
DDG_Arleigh_Burke_IIa,
"USS Lassen",
self.position.x + 8704,
self.position.y - 6296,
self.heading,
)
# Add Ticonderoga escort
if self.heading >= 180:
self.add_unit(
CG_Ticonderoga,
"USS Hué City",
self.position.x + 2222,
self.position.y - 3333,
self.heading,
)
else:
self.add_unit(
CG_Ticonderoga,
"USS Hué City",
self.position.x - 3333,
self.position.y + 2222,
self.heading,
)
self.get_generated_group().points[0].speed = 20
##################################################################################################
# Add carrier for normal generation
else: else:
if len(self.faction.aircraft_carrier) > 0: return
carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(
carrier_type,
"Carrier",
self.position.x,
self.position.y,
self.heading,
)
else:
return
# Add destroyers escort # Add destroyers escort
if len(self.faction.destroyers) > 0: if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers) dd_type = random.choice(self.faction.destroyers)
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD1", "DD1",
self.position.x + 2500, self.position.x + 2500,
self.position.y + 4500, self.position.y + 4500,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD2", "DD2",
self.position.x + 2500, self.position.x + 2500,
self.position.y - 4500, self.position.y - 4500,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD3", "DD3",
self.position.x + 4500, self.position.x + 4500,
self.position.y + 8500, self.position.y + 8500,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD4", "DD4",
self.position.x + 4500, self.position.x + 4500,
self.position.y - 8500, self.position.y - 8500,
self.heading, self.heading,
) )
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20

View File

@@ -6,7 +6,7 @@ from game.theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType from dcs.unittype import ShipType
from dcs.ships import FFG_Oliver_Hazzard_Perry, DDG_Arleigh_Burke_IIa from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
if TYPE_CHECKING: if TYPE_CHECKING:
from game.game import Game from game.game import Game
@@ -46,7 +46,7 @@ class OliverHazardPerryGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(OliverHazardPerryGroupGenerator, self).__init__( super(OliverHazardPerryGroupGenerator, self).__init__(
game, ground_object, faction, FFG_Oliver_Hazzard_Perry game, ground_object, faction, Oliver_Hazzard_Perry_class
) )
@@ -55,5 +55,5 @@ class ArleighBurkeGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(ArleighBurkeGroupGenerator, self).__init__( super(ArleighBurkeGroupGenerator, self).__init__(
game, ground_object, faction, DDG_Arleigh_Burke_IIa game, ground_object, faction, USS_Arleigh_Burke_IIa
) )

View File

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

View File

@@ -3,13 +3,13 @@ import random
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from dcs.ships import ( from dcs.ships import (
Corvette_1124_4_Grisha, FFL_1124_4_Grisha,
Corvette_1241_1_Molniya, FSG_1241_1MP_Molniya,
Frigate_11540_Neustrashimy, FFG_11540_Neustrashimy,
Frigate_1135M_Rezky, FF_1135M_Rezky,
Cruiser_1164_Moskva, CG_1164_Moskva,
SSK_877V_Kilo, SSK_877,
SSK_641B_Tango, SSK_641B,
) )
from gen.fleet.dd_group import DDGroupGenerator from gen.fleet.dd_group import DDGroupGenerator
@@ -37,9 +37,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
include_frigate = True include_frigate = True
if include_frigate: if include_frigate:
frigate_type = random.choice( frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
[Corvette_1124_4_Grisha, Corvette_1241_1_Molniya]
)
self.add_unit( self.add_unit(
frigate_type, frigate_type,
"FF1", "FF1",
@@ -56,7 +54,7 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
) )
if include_dd: if include_dd:
dd_type = random.choice([Frigate_11540_Neustrashimy, Frigate_1135M_Rezky]) dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
self.add_unit( self.add_unit(
dd_type, dd_type,
"DD1", "DD1",
@@ -74,13 +72,9 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
if include_cc: if include_cc:
# Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster. # Only include the Moskva for now, the Pyotry Velikiy is an unkillable monster.
# See https://github.com/dcs-liberation/dcs_liberation/issues/567 # See https://github.com/Khopa/dcs_liberation/issues/567
self.add_unit( self.add_unit(
Cruiser_1164_Moskva, CG_1164_Moskva, "CC1", self.position.x, self.position.y, self.heading
"CC1",
self.position.x,
self.position.y,
self.heading,
) )
self.get_generated_group().points[0].speed = 20 self.get_generated_group().points[0].speed = 20
@@ -91,7 +85,7 @@ class GrishaGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(GrishaGroupGenerator, self).__init__( super(GrishaGroupGenerator, self).__init__(
game, ground_object, faction, Corvette_1124_4_Grisha game, ground_object, faction, FFL_1124_4_Grisha
) )
@@ -100,7 +94,7 @@ class MolniyaGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(MolniyaGroupGenerator, self).__init__( super(MolniyaGroupGenerator, self).__init__(
game, ground_object, faction, Corvette_1241_1_Molniya game, ground_object, faction, FSG_1241_1MP_Molniya
) )
@@ -109,7 +103,7 @@ class KiloSubGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(KiloSubGroupGenerator, self).__init__( super(KiloSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_877V_Kilo game, ground_object, faction, SSK_877
) )
@@ -118,5 +112,5 @@ class TangoSubGroupGenerator(DDGroupGenerator):
self, game: Game, ground_object: TheaterGroundObject, faction: Faction self, game: Game, ground_object: TheaterGroundObject, faction: Faction
): ):
super(TangoSubGroupGenerator, self).__init__( super(TangoSubGroupGenerator, self).__init__(
game, ground_object, faction, SSK_641B_Tango game, ground_object, faction, SSK_641B
) )

View File

@@ -1,6 +1,6 @@
import random import random
from dcs.ships import Boat_Schnellboot_type_S130 from dcs.ships import Schnellboot_type_S130
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
@@ -10,7 +10,7 @@ class SchnellbootGroupGenerator(ShipGroupGenerator):
for i in range(random.randint(2, 4)): for i in range(random.randint(2, 4)):
self.add_unit( self.add_unit(
Boat_Schnellboot_type_S130, Schnellboot_type_S130,
"Schnellboot" + str(i), "Schnellboot" + str(i),
self.position.x + i * random.randint(100, 250), self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100), self.position.y + (random.randint(100, 200) - 100),

View File

@@ -8,7 +8,6 @@ from gen.fleet.dd_group import (
ArleighBurkeGroupGenerator, ArleighBurkeGroupGenerator,
OliverHazardPerryGroupGenerator, OliverHazardPerryGroupGenerator,
) )
from gen.fleet.lacombattanteII import LaCombattanteIIGroupGenerator
from gen.fleet.lha_group import LHAGroupGenerator from gen.fleet.lha_group import LHAGroupGenerator
from gen.fleet.ru_dd_group import ( from gen.fleet.ru_dd_group import (
RussianNavyGroupGenerator, RussianNavyGroupGenerator,
@@ -35,7 +34,6 @@ SHIP_MAP = {
"KiloSubGroupGenerator": KiloSubGroupGenerator, "KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator, "TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator, "Type54GroupGenerator": Type54GroupGenerator,
"LaCombattanteIIGroupGenerator": LaCombattanteIIGroupGenerator,
} }
@@ -49,7 +47,6 @@ def generate_ship_group(game, ground_object, faction_name: str):
gen = random.choice(faction.navy_generators) gen = random.choice(faction.navy_generators)
if gen in SHIP_MAP.keys(): if gen in SHIP_MAP.keys():
generator = SHIP_MAP[gen](game, ground_object, faction) generator = SHIP_MAP[gen](game, ground_object, faction)
print(generator.position)
generator.generate() generator.generate()
return generator.get_generated_group() return generator.get_generated_group()
else: else:

View File

@@ -1,6 +1,6 @@
import random import random
from dcs.ships import U_boat_VIIC_U_flak from dcs.ships import Uboat_VIIC_U_flak
from gen.sam.group_generator import ShipGroupGenerator from gen.sam.group_generator import ShipGroupGenerator
@@ -10,7 +10,7 @@ class UBoatGroupGenerator(ShipGroupGenerator):
for i in range(random.randint(1, 4)): for i in range(random.randint(1, 4)):
self.add_unit( self.add_unit(
U_boat_VIIC_U_flak, Uboat_VIIC_U_flak,
"Uboat" + str(i), "Uboat" + str(i),
self.position.x + i * random.randint(100, 250), self.position.x + i * random.randint(100, 250),
self.position.y + (random.randint(100, 200) - 100), self.position.y + (random.randint(100, 200) - 100),

View File

@@ -17,17 +17,12 @@ from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Tuple, Tuple,
Type, Type,
TypeVar,
Union,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game.factions.faction import Faction
from game.infos.information import Information from game.infos.information import Information
from game.procurement import AircraftProcurementRequest from game.procurement import AircraftProcurementRequest
from game.profiling import logged_duration, MultiEventTracer
from game.squadrons import AirWing, Squadron
from game.theater import ( from game.theater import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -44,8 +39,8 @@ from game.theater.theatergroundobject import (
NavalGroundObject, NavalGroundObject,
VehicleGroupGroundObject, VehicleGroupGroundObject,
) )
from game.transfers import CargoShip, Convoy from game.utils import Distance, nautical_miles
from game.utils import Distance, nautical_miles, meters from gen import Conflict
from gen.ato import Package from gen.ato import Package
from gen.flights.ai_flight_planner_db import aircraft_for_task from gen.flights.ai_flight_planner_db import aircraft_for_task
from gen.flights.closestairfields import ( from gen.flights.closestairfields import (
@@ -113,8 +108,6 @@ class ProposedMission:
#: The proposed flights that are required for the mission. #: The proposed flights that are required for the mission.
flights: List[ProposedFlight] flights: List[ProposedFlight]
asap: bool = field(default=False)
def __str__(self) -> str: def __str__(self) -> str:
flights = ", ".join([str(f) for f in self.flights]) flights = ", ".join([str(f) for f in self.flights])
return f"{self.location.name}: {flights}" return f"{self.location.name}: {flights}"
@@ -125,19 +118,17 @@ class AircraftAllocator:
def __init__( def __init__(
self, self,
air_wing: AirWing,
closest_airfields: ClosestAirfields, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory, global_inventory: GlobalAircraftInventory,
is_player: bool, is_player: bool,
) -> None: ) -> None:
self.air_wing = air_wing
self.closest_airfields = closest_airfields self.closest_airfields = closest_airfields
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.is_player = is_player self.is_player = is_player
def find_squadron_for_flight( def find_aircraft_for_flight(
self, flight: ProposedFlight self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, Squadron]]: ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
"""Finds aircraft suitable for the given mission. """Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the Searches for aircraft capable of performing the given mission within the
@@ -156,47 +147,28 @@ class AircraftAllocator:
on subsequent calls. If the found aircraft are not used, the caller is on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory. responsible for returning them to the inventory.
""" """
return self.find_aircraft_for_task(flight, flight.task) return self.find_aircraft_of_type(flight, aircraft_for_task(flight.task))
def find_aircraft_for_task( def find_aircraft_of_type(
self, flight: ProposedFlight, task: FlightType self,
) -> Optional[Tuple[ControlPoint, Squadron]]: flight: ProposedFlight,
types = aircraft_for_task(task) types: List[Type[FlyingType]],
airfields_in_range = self.closest_airfields.operational_airfields_within( ) -> Optional[Tuple[ControlPoint, Type[FlyingType]]]:
airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance flight.max_distance
) )
# Prefer using squadrons with pilots first
best_understaffed: Optional[Tuple[ControlPoint, Squadron]] = None
for airfield in airfields_in_range: for airfield in airfields_in_range:
if not airfield.is_friendly(self.is_player): if not airfield.is_friendly(self.is_player):
continue continue
inventory = self.global_inventory.for_control_point(airfield) inventory = self.global_inventory.for_control_point(airfield)
for aircraft in types: for aircraft, available in inventory.all_aircraft:
if not airfield.can_operate(aircraft): if not airfield.can_operate(aircraft):
continue continue
if inventory.available(aircraft) < flight.num_aircraft: if aircraft in types and available >= flight.num_aircraft:
continue inventory.remove_aircraft(aircraft, flight.num_aircraft)
# Valid location with enough aircraft available. Find a squadron to fit return airfield, aircraft
# the role.
for squadron in self.air_wing.squadrons_for(aircraft):
if task not in squadron.auto_assignable_mission_types:
continue
if len(squadron.available_pilots) >= flight.num_aircraft:
inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, squadron
# A compatible squadron that doesn't have enough pilots. Remember it return None
# as a fallback in case we find no better choices.
if best_understaffed is None:
best_understaffed = airfield, squadron
if best_understaffed is not None:
airfield, squadron = best_understaffed
self.global_inventory.for_control_point(airfield).remove_aircraft(
squadron.aircraft, flight.num_aircraft
)
return best_understaffed
class PackageBuilder: class PackageBuilder:
@@ -207,18 +179,16 @@ class PackageBuilder:
location: MissionTarget, location: MissionTarget,
closest_airfields: ClosestAirfields, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory, global_inventory: GlobalAircraftInventory,
air_wing: AirWing,
is_player: bool, is_player: bool,
package_country: str, package_country: str,
start_type: str, start_type: str,
asap: bool,
) -> None: ) -> None:
self.closest_airfields = closest_airfields self.closest_airfields = closest_airfields
self.is_player = is_player self.is_player = is_player
self.package_country = package_country self.package_country = package_country
self.package = Package(location, auto_asap=asap) self.package = Package(location)
self.allocator = AircraftAllocator( self.allocator = AircraftAllocator(
air_wing, closest_airfields, global_inventory, is_player closest_airfields, global_inventory, is_player
) )
self.global_inventory = global_inventory self.global_inventory = global_inventory
self.start_type = start_type self.start_type = start_type
@@ -231,10 +201,10 @@ class PackageBuilder:
caller should return any previously planned flights to the inventory caller should return any previously planned flights to the inventory
using release_planned_aircraft. using release_planned_aircraft.
""" """
assignment = self.allocator.find_squadron_for_flight(plan) assignment = self.allocator.find_aircraft_for_flight(plan)
if assignment is None: if assignment is None:
return False return False
airfield, squadron = assignment airfield, aircraft = assignment
if isinstance(airfield, OffMapSpawn): if isinstance(airfield, OffMapSpawn):
start_type = "In Flight" start_type = "In Flight"
else: else:
@@ -243,13 +213,13 @@ class PackageBuilder:
flight = Flight( flight = Flight(
self.package, self.package,
self.package_country, self.package_country,
squadron, aircraft,
plan.num_aircraft, plan.num_aircraft,
plan.task, plan.task,
start_type, start_type,
departure=airfield, departure=airfield,
arrival=airfield, arrival=airfield,
divert=self.find_divert_field(squadron.aircraft, airfield), divert=self.find_divert_field(aircraft, airfield),
) )
self.package.add_flight(flight) self.package.add_flight(flight)
return True return True
@@ -258,9 +228,7 @@ class PackageBuilder:
self, aircraft: Type[FlyingType], arrival: ControlPoint self, aircraft: Type[FlyingType], arrival: ControlPoint
) -> Optional[ControlPoint]: ) -> Optional[ControlPoint]:
divert_limit = nautical_miles(150) divert_limit = nautical_miles(150)
for airfield in self.closest_airfields.operational_airfields_within( for airfield in self.closest_airfields.airfields_within(divert_limit):
divert_limit
):
if airfield.captured != self.is_player: if airfield.captured != self.is_player:
continue continue
if airfield == arrival: if airfield == arrival:
@@ -281,13 +249,9 @@ class PackageBuilder:
flights = list(self.package.flights) flights = list(self.package.flights)
for flight in flights: for flight in flights:
self.global_inventory.return_from_flight(flight) self.global_inventory.return_from_flight(flight)
flight.clear_roster()
self.package.remove_flight(flight) self.package.remove_flight(flight)
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
class ObjectiveFinder: class ObjectiveFinder:
"""Identifies potential objectives for the mission planner.""" """Identifies potential objectives for the mission planner."""
@@ -299,53 +263,41 @@ class ObjectiveFinder:
self.game = game self.game = game
self.is_player = is_player self.is_player = is_player
def enemy_air_defenses(self) -> Iterator[tuple[TheaterGroundObject, Distance]]: def enemy_sams(self) -> Iterator[TheaterGroundObject]:
"""Iterates over all enemy SAM sites.""" """Iterates over all enemy SAM sites."""
doctrine = self.game.faction_for(self.is_player).doctrine # Control points might have the same ground object several times, for
threat_zones = self.game.threat_zone_for(not self.is_player) # some reason.
found_targets: Set[str] = set()
for cp in self.enemy_control_points(): for cp in self.enemy_control_points():
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
is_ewr = isinstance(ground_object, EwrGroundObject)
is_sam = isinstance(ground_object, SamGroundObject)
if not is_ewr and not is_sam:
continue
if ground_object.is_dead: if ground_object.is_dead:
continue continue
if isinstance(ground_object, EwrGroundObject): if ground_object.name in found_targets:
if threat_zones.threatened_by_air_defense(ground_object):
# This is a very weak heuristic for determining whether the EWR
# is close enough to be worth targeting before a SAM that is
# covering it. Ingress distance corresponds to the beginning of
# the attack range and is sufficient for most standoff weapons,
# so treating the ingress distance as the threat distance sorts
# these EWRs such that they will be attacked before SAMs that do
# not threaten the ingress point, but after those that do.
target_range = doctrine.ingress_egress_distance
else:
# But if the EWR isn't covered then we should only be worrying
# about its detection range.
target_range = ground_object.max_detection_range()
elif isinstance(ground_object, SamGroundObject):
target_range = ground_object.max_threat_range()
else:
continue continue
yield ground_object, target_range if not ground_object.has_radar:
continue
def threatening_air_defenses(self) -> Iterator[TheaterGroundObject]: # TODO: Yield in order of most threatening.
# Need to sort in order of how close their defensive range comes
# to friendly assets. To do that we need to add effective range
# information to the database.
yield ground_object
found_targets.add(ground_object.name)
def threatening_sams(self) -> Iterator[MissionTarget]:
"""Iterates over enemy SAMs in threat range of friendly control points. """Iterates over enemy SAMs in threat range of friendly control points.
SAM sites are sorted by their closest proximity to any friendly control SAM sites are sorted by their closest proximity to any friendly control
point (airfield or fleet). point (airfield or fleet).
""" """
return self._targets_by_range(self.enemy_sams())
target_ranges: list[tuple[TheaterGroundObject, Distance]] = []
for target, threat_range in self.enemy_air_defenses():
ranges: list[Distance] = []
for cp in self.friendly_control_points():
ranges.append(meters(target.distance_to(cp)) - threat_range)
target_ranges.append((target, min(ranges)))
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
for target, _range in target_ranges:
yield target
def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]: def enemy_vehicle_groups(self) -> Iterator[VehicleGroupGroundObject]:
"""Iterates over all enemy vehicle groups.""" """Iterates over all enemy vehicle groups."""
@@ -387,9 +339,9 @@ class ObjectiveFinder:
return self._targets_by_range(self.enemy_ships()) return self._targets_by_range(self.enemy_ships())
def _targets_by_range( def _targets_by_range(
self, targets: Iterable[MissionTargetType] self, targets: Iterable[MissionTarget]
) -> Iterator[MissionTargetType]: ) -> Iterator[MissionTarget]:
target_ranges: List[Tuple[MissionTargetType, int]] = [] target_ranges: List[Tuple[MissionTarget, int]] = []
for target in targets: for target in targets:
ranges: List[int] = [] ranges: List[int] = []
for cp in self.friendly_control_points(): for cp in self.friendly_control_points():
@@ -435,7 +387,7 @@ class ObjectiveFinder:
is_building = isinstance(ground_object, BuildingGroundObject) is_building = isinstance(ground_object, BuildingGroundObject)
is_fob = isinstance(enemy_cp, Fob) is_fob = isinstance(enemy_cp, Fob)
if is_building and is_fob and ground_object.is_control_point: if is_building and is_fob and ground_object.airbase_group:
# This is the FOB structure itself. Can't be repaired or # This is the FOB structure itself. Can't be repaired or
# targeted by the player, so shouldn't be targetable by the # targeted by the player, so shouldn't be targetable by the
# AI. # AI.
@@ -456,7 +408,13 @@ class ObjectiveFinder:
def front_lines(self) -> Iterator[FrontLine]: def front_lines(self) -> Iterator[FrontLine]:
"""Iterates over all active front lines in the theater.""" """Iterates over all active front lines in the theater."""
yield from self.game.theater.conflicts() for cp in self.friendly_control_points():
for connected in cp.connected_points:
if connected.is_friendly(self.is_player):
continue
if Conflict.has_frontline_between(cp, connected):
yield FrontLine(cp, connected, self.game.theater)
def vulnerable_control_points(self) -> Iterator[ControlPoint]: def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs. """Iterates over friendly CPs that are vulnerable to enemy CPs.
@@ -469,10 +427,8 @@ class ObjectiveFinder:
# Off-map spawn locations don't need protection. # Off-map spawn locations don't need protection.
continue continue
airfields_in_proximity = self.closest_airfields_to(cp) airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = ( airfields_in_threat_range = airfields_in_proximity.airfields_within(
airfields_in_proximity.operational_airfields_within( self.AIRFIELD_THREAT_RANGE
self.AIRFIELD_THREAT_RANGE
)
) )
for airfield in airfields_in_threat_range: for airfield in airfields_in_threat_range:
if not airfield.is_friendly(self.is_player): if not airfield.is_friendly(self.is_player):
@@ -488,42 +444,12 @@ class ObjectiveFinder:
airfields.append(control_point) airfields.append(control_point)
return self._targets_by_range(airfields) return self._targets_by_range(airfields)
def convoys(self) -> Iterator[Convoy]:
for front_line in self.front_lines():
yield from self.game.transfers.convoys.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)
def cargo_ships(self) -> Iterator[CargoShip]:
for front_line in self.front_lines():
yield from self.game.transfers.cargo_ships.travelling_to(
front_line.control_point_hostile_to(self.is_player)
)
def friendly_control_points(self) -> Iterator[ControlPoint]: def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points.""" """Iterates over all friendly control points."""
return ( return (
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player) c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
) )
def farthest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is farthest from any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
farthest = None
max_distance = meters(0)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn):
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance > max_distance:
farthest = cp
max_distance = distance
if farthest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest
def enemy_control_points(self) -> Iterator[ControlPoint]: def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points.""" """Iterates over all enemy control points."""
return ( return (
@@ -581,7 +507,6 @@ class CoalitionMissionPlanner:
MAX_OCA_RANGE = nautical_miles(150) MAX_OCA_RANGE = nautical_miles(150)
MAX_SEAD_RANGE = nautical_miles(150) MAX_SEAD_RANGE = nautical_miles(150)
MAX_STRIKE_RANGE = nautical_miles(150) MAX_STRIKE_RANGE = nautical_miles(150)
MAX_AWEC_RANGE = nautical_miles(200)
def __init__(self, game: Game, is_player: bool) -> None: def __init__(self, game: Game, is_player: bool) -> None:
self.game = game self.game = game
@@ -589,25 +514,7 @@ class CoalitionMissionPlanner:
self.objective_finder = ObjectiveFinder(self.game, self.is_player) self.objective_finder = ObjectiveFinder(self.game, self.is_player)
self.ato = self.game.blue_ato if is_player else self.game.red_ato self.ato = self.game.blue_ato if is_player else self.game.red_ato
self.threat_zones = self.game.threat_zone_for(not self.is_player) self.threat_zones = self.game.threat_zone_for(not self.is_player)
self.procurement_requests = self.game.procurement_requests_for(self.is_player) self.procurement_requests: List[AircraftProcurementRequest] = []
self.faction = self.game.faction_for(self.is_player)
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
"""Returns True if it is possible for the air wing to plan this mission type.
Not all mission types can be fulfilled by all air wings. Many factions do not
have AEW&C aircraft, so they will never be able to plan those missions. It's
also possible for the player to exclude mission types from their squadron
designs.
"""
all_compatible = aircraft_for_task(mission_type)
for squadron in self.game.air_wing_for(self.is_player).iter_squadrons():
if (
squadron.aircraft in all_compatible
and mission_type in squadron.auto_assignable_mission_types
):
return True
return False
def critical_missions(self) -> Iterator[ProposedMission]: def critical_missions(self) -> Iterator[ProposedMission]:
"""Identifies the most important missions to plan this turn. """Identifies the most important missions to plan this turn.
@@ -619,29 +526,28 @@ class CoalitionMissionPlanner:
ensure that they can be planned again next turn even if all aircraft are ensure that they can be planned again next turn even if all aircraft are
eliminated this turn. eliminated this turn.
""" """
# Find farthest, friendly CP for AEWC.
yield ProposedMission(
self.objective_finder.farthest_friendly_control_point(),
[ProposedFlight(FlightType.AEWC, 1, self.MAX_AWEC_RANGE)],
# Supports all the early CAP flights, so should be in the air ASAP.
asap=True,
)
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP. # Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points(): for cp in self.objective_finder.vulnerable_control_points():
# Plan CAP in such a way, that it is established during the whole desired mission length # Plan three rounds of CAP to give ~90 minutes coverage. Spacing
for _ in range( # these out appropriately is done in stagger_missions.
0, yield ProposedMission(
int(self.game.settings.desired_player_mission_duration.total_seconds()), cp,
int(self.faction.doctrine.cap_duration.total_seconds()), [
): ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
yield ProposedMission( ],
cp, )
[ yield ProposedMission(
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE), cp,
], [
) ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
yield ProposedMission(
cp,
[
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
],
)
# Find front lines, plan CAS. # Find front lines, plan CAS.
for front_line in self.objective_finder.front_lines(): for front_line in self.objective_finder.front_lines():
@@ -649,23 +555,9 @@ class CoalitionMissionPlanner:
front_line, front_line,
[ [
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE), ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
# This is *not* an escort because front lines don't create a threat ProposedFlight(
# zone. Generating threat zones from front lines causes the front FlightType.TARCAP, 2, self.MAX_CAP_RANGE, EscortType.AirToAir
# line to push back BARCAPs as it gets closer to the base. While ),
# front lines do have the same problem of potentially pulling
# BARCAPs off bases to engage a front line TARCAP, that's probably
# the one time where we do want that.
#
# TODO: Use intercepts and extra TARCAPs to cover bases near fronts.
# We don't have intercept missions yet so this isn't something we
# can do today, but we should probably return to having the front
# line project a threat zone (so that strike missions will route
# around it) and instead *not plan* a BARCAP at bases near the
# front, since there isn't a place to put a barrier. Instead, the
# aircraft that would have been a BARCAP could be used as additional
# interceptors and TARCAPs which will defend the base but won't be
# trying to avoid front line contacts.
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
], ],
) )
@@ -677,76 +569,14 @@ class CoalitionMissionPlanner:
# or objects, plan DEAD. # or objects, plan DEAD.
# Find enemy SAM sites with ranges that extend to within 50 nmi of # Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD. # friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_air_defenses(): for sam in self.objective_finder.threatening_sams():
flights = [ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE)]
# Only include SEAD against SAMs that still have emitters. No need to
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
# working track radar.
#
# For SAMs without track radars and EWRs, we still want a SEAD escort if
# needed.
#
# Note that there is a quirk here: we should potentially be included a SEAD
# escort *and* SEAD when the target is a radar SAM but the flight path is
# also threatened by SAMs. We don't want to include a SEAD escort if the
# package is *only* threatened by the target though. Could be improved, but
# needs a decent refactor to the escort planning to do so.
if sam.has_live_radar_sam:
flights.append(ProposedFlight(FlightType.SEAD, 2, self.MAX_SEAD_RANGE))
else:
flights.append(
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.Sead
)
)
# TODO: Max escort range.
flights.append(
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
)
)
yield ProposedMission(sam, flights)
# These will only rarely get planned. When a convoy is travelling multiple legs,
# they're targetable after the first leg. The reason for this is that
# procurement happens *after* mission planning so that the missions that could
# not be filled will guide the procurement process. Procurement is the stage
# that convoys are created (because they're created to move ground units that
# were just purchased), so we haven't created any yet. Any incomplete transfers
# from the previous turn (multi-leg journeys) will still be present though so
# they can be targeted.
#
# Even after this is fixed, the player's convoys that were created through the
# UI will never be targeted on the first turn of their journey because the AI
# stops planning after the start of the turn. We could potentially fix this by
# moving opfor mission planning until the takeoff button is pushed.
for convoy in self.objective_finder.convoys():
yield ProposedMission( yield ProposedMission(
convoy, sam,
[ [
ProposedFlight(FlightType.BAI, 2, self.MAX_BAI_RANGE), ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range. # TODO: Max escort range.
ProposedFlight( ProposedFlight(
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir FlightType.ESCORT, 2, self.MAX_SEAD_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead
),
],
)
for ship in self.objective_finder.cargo_ships():
yield ProposedMission(
ship,
[
ProposedFlight(FlightType.ANTISHIP, 2, self.MAX_ANTISHIP_RANGE),
# TODO: Max escort range.
ProposedFlight(
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
),
ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_BAI_RANGE, EscortType.Sead
), ),
], ],
) )
@@ -776,7 +606,7 @@ class CoalitionMissionPlanner:
FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir FlightType.ESCORT, 2, self.MAX_BAI_RANGE, EscortType.AirToAir
), ),
ProposedFlight( ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
), ),
], ],
) )
@@ -798,7 +628,7 @@ class CoalitionMissionPlanner:
FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir FlightType.ESCORT, 2, self.MAX_OCA_RANGE, EscortType.AirToAir
), ),
ProposedFlight( ProposedFlight(
FlightType.SEAD_ESCORT, 2, self.MAX_OCA_RANGE, EscortType.Sead FlightType.SEAD, 2, self.MAX_OCA_RANGE, EscortType.Sead
), ),
] ]
) )
@@ -815,29 +645,20 @@ class CoalitionMissionPlanner:
FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE, EscortType.AirToAir
), ),
ProposedFlight( ProposedFlight(
FlightType.SEAD_ESCORT, FlightType.SEAD, 2, self.MAX_STRIKE_RANGE, EscortType.Sead
2,
self.MAX_STRIKE_RANGE,
EscortType.Sead,
), ),
], ],
) )
def plan_missions(self) -> None: def plan_missions(self) -> None:
"""Identifies and plans mission for the turn.""" """Identifies and plans mission for the turn."""
player = "Blue" if self.is_player else "Red" for proposed_mission in self.propose_missions():
with logged_duration(f"{player} mission identification and fulfillment"): self.plan_mission(proposed_mission)
with MultiEventTracer() as tracer:
for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission, tracer)
with logged_duration(f"{player} reserve mission planning"): for critical_mission in self.critical_missions():
with MultiEventTracer() as tracer: self.plan_mission(critical_mission, reserves=True)
for critical_mission in self.critical_missions():
self.plan_mission(critical_mission, tracer, reserves=True)
with logged_duration(f"{player} mission scheduling"): self.stagger_missions()
self.stagger_missions()
for cp in self.objective_finder.friendly_control_points(): for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp) inventory = self.game.aircraft_inventory.for_control_point(cp)
@@ -892,29 +713,27 @@ class CoalitionMissionPlanner:
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]: def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
threats = defaultdict(bool) threats = defaultdict(bool)
for flight in builder.package.flights: for flight in builder.package.flights:
if self.threat_zones.waypoints_threatened_by_aircraft( if self.threat_zones.threatened_by_aircraft(flight):
flight.flight_plan.escorted_waypoints()
):
threats[EscortType.AirToAir] = True threats[EscortType.AirToAir] = True
if self.threat_zones.waypoints_threatened_by_radar_sam( if self.threat_zones.threatened_by_air_defense(flight):
list(flight.flight_plan.escorted_waypoints())
):
threats[EscortType.Sead] = True threats[EscortType.Sead] = True
return threats return threats
def plan_mission( def plan_mission(self, mission: ProposedMission, reserves: bool = False) -> None:
self, mission: ProposedMission, tracer: MultiEventTracer, reserves: bool = False
) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO.""" """Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.is_player:
package_country = self.game.player_country
else:
package_country = self.game.enemy_country
builder = PackageBuilder( builder = PackageBuilder(
mission.location, mission.location,
self.objective_finder.closest_airfields_to(mission.location), self.objective_finder.closest_airfields_to(mission.location),
self.game.aircraft_inventory, self.game.aircraft_inventory,
self.game.air_wing_for(self.is_player),
self.is_player, self.is_player,
self.game.country_for(self.is_player), package_country,
self.game.settings.default_start_type, self.game.settings.default_start_type,
mission.asap,
) )
# Attempt to plan all the main elements of the mission first. Escorts # Attempt to plan all the main elements of the mission first. Escorts
@@ -923,20 +742,12 @@ class CoalitionMissionPlanner:
missing_types: Set[FlightType] = set() missing_types: Set[FlightType] = set()
escorts = [] escorts = []
for proposed_flight in mission.flights: for proposed_flight in mission.flights:
if not self.air_wing_can_plan(proposed_flight.task):
# This air wing can never plan this mission type because they do not
# have compatible aircraft or squadrons. Skip fulfillment so that we
# don't place the purchase request.
continue
if proposed_flight.escort_type is not None: if proposed_flight.escort_type is not None:
# Escorts are planned after the primary elements of the package. # Escorts are planned after the primary elements of the package.
# If the package does not need escorts they may be pruned. # If the package does not need escorts they may be pruned.
escorts.append(proposed_flight) escorts.append(proposed_flight)
continue continue
with tracer.trace("Flight planning"): self.plan_flight(mission, proposed_flight, builder, missing_types, reserves)
self.plan_flight(
mission, proposed_flight, builder, missing_types, reserves
)
if missing_types: if missing_types:
self.scrub_mission_missing_aircraft( self.scrub_mission_missing_aircraft(
@@ -944,12 +755,6 @@ class CoalitionMissionPlanner:
) )
return return
if not builder.package.flights:
# The non-escort part of this mission is unplannable by this faction. Scrub
# the mission and do not attempt planning escorts because there's no reason
# to buy them because this mission will never be planned.
return
# Create flight plans for the main flights of the package so we can # Create flight plans for the main flights of the package so we can
# determine threats. This is done *after* creating all of the flights # determine threats. This is done *after* creating all of the flights
# rather than as each flight is added because the flight plan for # rather than as each flight is added because the flight plan for
@@ -960,8 +765,7 @@ class CoalitionMissionPlanner:
self.game, builder.package, self.is_player self.game, builder.package, self.is_player
) )
for flight in builder.package.flights: for flight in builder.package.flights:
with tracer.trace("Flight plan population"): flight_plan_builder.populate_flight_plan(flight)
flight_plan_builder.populate_flight_plan(flight)
needed_escorts = self.check_needed_escorts(builder) needed_escorts = self.check_needed_escorts(builder)
for escort in escorts: for escort in escorts:
@@ -969,8 +773,7 @@ class CoalitionMissionPlanner:
# impossible. # impossible.
assert escort.escort_type is not None assert escort.escort_type is not None
if needed_escorts[escort.escort_type]: if needed_escorts[escort.escort_type]:
with tracer.trace("Flight planning"): self.plan_flight(mission, escort, builder, missing_types, reserves)
self.plan_flight(mission, escort, builder, missing_types, reserves)
# Check again for unavailable aircraft. If the escort was required and # Check again for unavailable aircraft. If the escort was required and
# none were found, scrub the mission. # none were found, scrub the mission.
@@ -990,13 +793,7 @@ class CoalitionMissionPlanner:
# Add flight plans for escorts. # Add flight plans for escorts.
for flight in package.flights: for flight in package.flights:
if not flight.flight_plan.waypoints: if not flight.flight_plan.waypoints:
with tracer.trace("Flight plan population"): flight_plan_builder.populate_flight_plan(flight)
flight_plan_builder.populate_flight_plan(flight)
if package.has_players and self.game.settings.auto_ato_player_missions_asap:
package.auto_asap = True
package.set_tot_asap()
self.ato.add_package(package) self.ato.add_package(package)
def stagger_missions(self) -> None: def stagger_missions(self) -> None:
@@ -1006,7 +803,7 @@ class CoalitionMissionPlanner:
interval = (latest - earliest) // count interval = (latest - earliest) // count
for time in range(earliest, latest, interval): for time in range(earliest, latest, interval):
error = random.randint(-margin, margin) error = random.randint(-margin, margin)
yield timedelta(seconds=max(0, time + error)) yield timedelta(minutes=max(0, time + error))
dca_types = { dca_types = {
FlightType.BARCAP, FlightType.BARCAP,
@@ -1019,12 +816,7 @@ class CoalitionMissionPlanner:
] ]
start_time = start_time_generator( start_time = start_time_generator(
count=len(non_dca_packages), count=len(non_dca_packages), earliest=5, latest=90, margin=5
earliest=5 * 60,
latest=int(
self.game.settings.desired_player_mission_duration.total_seconds()
),
margin=5 * 60,
) )
for package in self.ato.packages: for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot() tot = TotEstimator(package).earliest_tot()
@@ -1044,8 +836,6 @@ class CoalitionMissionPlanner:
logging.error(f"Could not determine mission end time for {package}") logging.error(f"Could not determine mission end time for {package}")
continue continue
previous_cap_end_time[package.target] = departure_time previous_cap_end_time[package.target] = departure_time
elif package.auto_asap:
package.set_tot_asap()
else: else:
# But other packages should be spread out a bit. Note that take # But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at # times are delayed, but all aircraft will become active at

View File

@@ -5,19 +5,15 @@ from dcs.helicopters import (
AH_1W, AH_1W,
AH_64A, AH_64A,
AH_64D, AH_64D,
CH_47D,
CH_53E,
Ka_50, Ka_50,
Mi_24V, Mi_24V,
Mi_26,
Mi_28N, Mi_28N,
Mi_8MT, Mi_8MT,
OH_58D, OH_58D,
SA342L, SA342L,
SA342M, SA342M,
SH_60B,
UH_1H, UH_1H,
UH_60A, SH_60B,
) )
from dcs.planes import ( from dcs.planes import (
AJS37, AJS37,
@@ -26,17 +22,11 @@ from dcs.planes import (
A_10C, A_10C,
A_10C_2, A_10C_2,
A_20G, A_20G,
A_50,
An_26B,
B_17G, B_17G,
B_1B, B_1B,
B_52H, B_52H,
Bf_109K_4, Bf_109K_4,
C_101CC, C_101CC,
C_130,
C_17A,
E_2C,
E_3A,
FA_18C_hornet, FA_18C_hornet,
FW_190A8, FW_190A8,
FW_190D9, FW_190D9,
@@ -50,12 +40,9 @@ from dcs.planes import (
F_4E, F_4E,
F_5E_3, F_5E_3,
F_86F_Sabre, F_86F_Sabre,
IL_76MD,
I_16,
JF_17, JF_17,
J_11A, J_11A,
Ju_88A4, Ju_88A4,
KJ_2000,
L_39ZA, L_39ZA,
MQ_9_Reaper, MQ_9_Reaper,
M_2000C, M_2000C,
@@ -67,6 +54,7 @@ from dcs.planes import (
MiG_27K, MiG_27K,
MiG_29A, MiG_29A,
MiG_29G, MiG_29G,
MiG_29K,
MiG_29S, MiG_29S,
MiG_31, MiG_31,
Mirage_2000_5, Mirage_2000_5,
@@ -96,17 +84,17 @@ from dcs.planes import (
Tu_95MS, Tu_95MS,
WingLoong_I, WingLoong_I,
I_16, I_16,
Yak_40,
) )
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from gen.flights.flight import FlightType from gen.flights.flight import FlightType
from pydcs_extensions.a4ec.a4ec import A_4E_C from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f22a.f22a import F_22A from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.mb339.mb339 import MB_339PAN from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M, Rafale_B
from pydcs_extensions.su57.su57 import Su_57 from pydcs_extensions.su57.su57 import Su_57
from pydcs_extensions.hercules.hercules import Hercules
# All aircraft lists are in priority order. Aircraft higher in the list will be # All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list. # preferred over those lower in the list.
@@ -123,25 +111,26 @@ CAP_CAPABLE = [
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
MiG_25PD, MiG_25PD,
Rafale_M,
Su_33, Su_33,
Su_30, Su_30,
Su_27, Su_27,
J_11A, J_11A,
F_15C, F_15C,
MiG_29S, MiG_29S,
MiG_29K,
MiG_29G, MiG_29G,
MiG_29A, MiG_29A,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
F_15E,
F_16A, F_16A,
F_4E, F_4E,
JAS39Gripen,
JF_17, JF_17,
MiG_23MLD, MiG_23MLD,
MiG_21Bis, MiG_21Bis,
Mirage_2000_5, Mirage_2000_5,
M_2000C, M_2000C,
F_15E,
F_5E_3, F_5E_3,
MiG_19P, MiG_19P,
A_4E_C, A_4E_C,
@@ -167,32 +156,30 @@ CAP_CAPABLE = [
CAS_CAPABLE = [ CAS_CAPABLE = [
A_10C_2, A_10C_2,
A_10C, A_10C,
Hercules, B_1B,
F_14B,
F_14A_135_GR,
Su_25TM, Su_25TM,
Su_25T, Su_25T,
Su_25, Su_25,
F_15E, F_15E,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
Rafale_A_S,
Rafale_B,
Tornado_GR4, Tornado_GR4,
Tornado_IDS, Tornado_IDS,
JAS39Gripen_AG,
JF_17, JF_17,
AV8BNA,
A_10A, A_10A,
B_1B,
A_4E_C, A_4E_C,
F_14B,
F_14A_135_GR,
AJS37, AJS37,
Su_24MR, Su_24MR,
Su_24M, Su_24M,
Su_17M4, Su_17M4,
F_4E, AV8BNA,
S_3B, S_3B,
Su_34, Su_34,
Su_30, Su_30,
MiG_19P,
MiG_29S, MiG_29S,
MiG_27K, MiG_27K,
MiG_29A, MiG_29A,
@@ -215,7 +202,6 @@ CAS_CAPABLE = [
MB_339PAN, MB_339PAN,
L_39ZA, L_39ZA,
A_20G, A_20G,
Ju_88A4,
P_47D_40, P_47D_40,
P_47D_30bl1, P_47D_30bl1,
P_47D_30, P_47D_30,
@@ -233,7 +219,7 @@ CAS_CAPABLE = [
] ]
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task. # Aircraft used for SEAD tasks
SEAD_CAPABLE = [ SEAD_CAPABLE = [
JF_17, JF_17,
F_16C_50, F_16C_50,
@@ -241,11 +227,10 @@ SEAD_CAPABLE = [
Tornado_IDS, Tornado_IDS,
Su_25T, Su_25T,
Su_25TM, Su_25TM,
Rafale_A_S,
Rafale_B,
F_4E, F_4E,
A_4E_C, A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA, AV8BNA,
Su_24M, Su_24M,
Su_17M4, Su_17M4,
@@ -253,21 +238,9 @@ SEAD_CAPABLE = [
Su_30, Su_30,
MiG_27K, MiG_27K,
Tornado_GR4, Tornado_GR4,
] F_117A,
B_17G,
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
DEAD_CAPABLE = [
AJS37,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
A_20G, A_20G,
Ju_88A4,
P_47D_40, P_47D_40,
P_47D_30bl1, P_47D_30bl1,
P_47D_30, P_47D_30,
@@ -278,6 +251,18 @@ DEAD_CAPABLE = [
Bf_109K_4, Bf_109K_4,
FW_190D9, FW_190D9,
FW_190A8, FW_190A8,
]
# Aircraft used for DEAD tasks
DEAD_CAPABLE = [
AJS37,
F_14B,
F_14A_135_GR,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
] + SEAD_CAPABLE ] + SEAD_CAPABLE
@@ -291,13 +276,14 @@ STRIKE_CAPABLE = [
Tu_22M3, Tu_22M3,
F_15E, F_15E,
AJS37, AJS37,
Rafale_A_S,
Rafale_B,
Tornado_GR4, Tornado_GR4,
F_16C_50, F_16C_50,
FA_18C_hornet, FA_18C_hornet,
F_16A, F_16A,
F_14B, F_14B,
F_14A_135_GR, F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS, Tornado_IDS,
Su_17M4, Su_17M4,
Su_24MR, Su_24MR,
@@ -310,10 +296,10 @@ STRIKE_CAPABLE = [
Su_30, Su_30,
Su_27, Su_27,
MiG_29S, MiG_29S,
MiG_29K,
MiG_29G, MiG_29G,
MiG_29A, MiG_29A,
JF_17, JF_17,
F_4E,
A_10C_2, A_10C_2,
A_10C, A_10C,
AV8BNA, AV8BNA,
@@ -330,7 +316,6 @@ STRIKE_CAPABLE = [
L_39ZA, L_39ZA,
B_17G, B_17G,
A_20G, A_20G,
Ju_88A4,
P_47D_40, P_47D_40,
P_47D_30bl1, P_47D_30bl1,
P_47D_30, P_47D_30,
@@ -348,7 +333,8 @@ ANTISHIP_CAPABLE = [
AJS37, AJS37,
Tu_22M3, Tu_22M3,
FA_18C_hornet, FA_18C_hornet,
JAS39Gripen_AG, Rafale_A_S,
Rafale_B,
Su_24M, Su_24M,
Su_17M4, Su_17M4,
JF_17, JF_17,
@@ -376,34 +362,16 @@ RUNWAY_ATTACK_CAPABLE = [
# For any aircraft that isn't necessarily directly involved in strike # For any aircraft that isn't necessarily directly involved in strike
# missions in a direct combat sense, but can transport objects and infantry. # missions in a direct combat sense, but can transport objects and infantry.
TRANSPORT_CAPABLE = [ TRANSPORT_CAPABLE = [
C_17A,
Hercules, Hercules,
C_130, Mi_8MT,
IL_76MD,
An_26B,
Yak_40,
CH_53E,
CH_47D,
SH_60B,
UH_60A,
UH_1H, UH_1H,
Mi_8MT,
Mi_8MT,
Mi_26,
] ]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I] DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
E_3A,
E_2C,
A_50,
KJ_2000,
]
def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]: def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP, FlightType.SWEEP) cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions: if task in cap_missions:
return CAP_CAPABLE return CAP_CAPABLE
elif task == FlightType.ANTISHIP: elif task == FlightType.ANTISHIP:
@@ -414,8 +382,6 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return CAS_CAPABLE return CAS_CAPABLE
elif task == FlightType.SEAD: elif task == FlightType.SEAD:
return SEAD_CAPABLE return SEAD_CAPABLE
elif task == FlightType.SEAD_ESCORT:
return SEAD_CAPABLE
elif task == FlightType.DEAD: elif task == FlightType.DEAD:
return DEAD_CAPABLE return DEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT: elif task == FlightType.OCA_AIRCRAFT:
@@ -426,18 +392,6 @@ def aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
return STRIKE_CAPABLE return STRIKE_CAPABLE
elif task == FlightType.ESCORT: elif task == FlightType.ESCORT:
return CAP_CAPABLE return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
else: else:
logging.error(f"Unplannable flight type: {task}") logging.error(f"Unplannable flight type: {task}")
return [] return []
def tasks_for_aircraft(aircraft: Type[FlyingType]) -> list[FlightType]:
tasks = []
for task in FlightType:
if aircraft in aircraft_for_task(task):
tasks.append(task)
return tasks

View File

@@ -18,7 +18,7 @@ class ClosestAirfields:
self.target = target self.target = target
# This cache is configured once on load, so it's important that it is # This cache is configured once on load, so it's important that it is
# complete and deterministic to avoid different behaviors across loads. # complete and deterministic to avoid different behaviors across loads.
# E.g. https://github.com/dcs-liberation/dcs_liberation/issues/819 # E.g. https://github.com/Khopa/dcs_liberation/issues/819
self.closest_airfields: List[ControlPoint] = sorted( self.closest_airfields: List[ControlPoint] = sorted(
all_control_points, key=lambda c: self.target.distance_to(c) all_control_points, key=lambda c: self.target.distance_to(c)
) )
@@ -27,36 +27,18 @@ class ClosestAirfields:
def operational_airfields(self) -> Iterator[ControlPoint]: def operational_airfields(self) -> Iterator[ControlPoint]:
return (c for c in self.closest_airfields if c.runway_is_operational()) return (c for c in self.closest_airfields if c.runway_is_operational())
def _airfields_within( def airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
self, distance: Distance, operational: bool """Iterates over all airfields within the given range of the target.
) -> Iterator[ControlPoint]:
airfields = ( Note that this iterates over *all* airfields, not just friendly
self.operational_airfields if operational else self.closest_airfields airfields.
) """
for cp in airfields: for cp in self.closest_airfields:
if cp.distance_to(self.target) < distance.meters: if cp.distance_to(self.target) < distance.meters:
yield cp yield cp
else: else:
break break
def operational_airfields_within(
self, distance: Distance
) -> Iterator[ControlPoint]:
"""Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
return self._airfields_within(distance, operational=True)
def all_airfields_within(self, distance: Distance) -> Iterator[ControlPoint]:
"""Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
return self._airfields_within(distance, operational=False)
class ObjectiveDistanceCache: class ObjectiveDistanceCache:
theater: Optional[ConflictTheater] = None theater: Optional[ConflictTheater] = None

View File

@@ -1,58 +1,25 @@
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass from collections import defaultdict
from datetime import timedelta from datetime import timedelta
from enum import Enum from enum import Enum
from typing import List, Optional, TYPE_CHECKING, Type, Union from typing import Dict, List, Optional, TYPE_CHECKING, Type
from dcs.mapping import Point from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction from dcs.point import MovingPoint, PointAction
from dcs.unit import Unit
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from game import db from game import db
from game.squadrons import Pilot, Squadron from game.data.weapons import Weapon
from game.theater.controlpoint import ControlPoint, MissionTarget from game.theater.controlpoint import ControlPoint, MissionTarget
from game.utils import Distance, meters from game.utils import Distance, meters
from gen.flights.loadouts import Loadout
if TYPE_CHECKING: if TYPE_CHECKING:
from game.transfers import TransferOrder
from gen.ato import Package from gen.ato import Package
from gen.flights.flightplan import FlightPlan from gen.flights.flightplan import FlightPlan
class FlightType(Enum): class FlightType(Enum):
"""Enumeration of mission types.
The value of each enumeration is the name that will be shown in the UI.
These values are persisted to the save game as well since they are a part of
each flight and thus a part of the ATO, so changing these values will break
save compat.
When adding new mission types to this list, you will also need to update:
* flightplan.py: Add waypoint population in generate_flight_plan. Add a new flight
plan type if necessary, though most are a subclass of StrikeFlightPlan.
* aircraft.py: Add a configuration method and call it in setup_flight_group. This is
responsible for configuring waypoint 0 actions like setting ROE, threat reaction,
and mission abort parameters (winchester, bingo, etc).
* Implementations of MissionTarget.mission_types: A mission type can only be planned
against compatible targets. The mission_types method of each target class defines
which missions may target it.
* ai_flight_planner_db.py: Add the new mission type to aircraft_for_task that
returns the list of compatible aircraft in order of preference.
You may also need to update:
* flight.py: Add a new waypoint type if necessary. Most mission types will need
these, as aircraft.py uses the ingress point type to specialize AI tasks, and non-
strike-like missions will need more specialized control.
* ai_flight_planner.py: Use the new mission type in propose_missions so the AI will
plan the new mission type.
"""
TARCAP = "TARCAP" TARCAP = "TARCAP"
BARCAP = "BARCAP" BARCAP = "BARCAP"
CAS = "CAS" CAS = "CAS"
@@ -66,36 +33,12 @@ class FlightType(Enum):
SWEEP = "Fighter sweep" SWEEP = "Fighter sweep"
OCA_RUNWAY = "OCA/Runway" OCA_RUNWAY = "OCA/Runway"
OCA_AIRCRAFT = "OCA/Aircraft" OCA_AIRCRAFT = "OCA/Aircraft"
AEWC = "AEW&C"
TRANSPORT = "Transport"
SEAD_ESCORT = "SEAD Escort"
def __str__(self) -> str: def __str__(self) -> str:
return self.value return self.value
@classmethod
def from_name(cls, name: str) -> FlightType:
for entry in cls:
if name == entry.value:
return entry
raise KeyError(f"No FlightType with name {name}")
class FlightWaypointType(Enum): class FlightWaypointType(Enum):
"""Enumeration of waypoint types.
The value of the enum has no meaning but should remain stable to prevent breaking
save game compatibility.
When adding a new waypoint type, you will also need to update:
* waypointbuilder.py: Add a builder to simplify construction of the new waypoint
type unless the new waypoint type will be a parameter to an existing builder
method (such as how escort ingress waypoints work).
* aircraft.py: Associate AI actions with the new waypoint type by subclassing
PydcsWaypointBuilder and using it in PydcsWaypointBuilder.for_waypoint.
"""
TAKEOFF = 0 # Take off point TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point PATROL = 2 # Patrol point
@@ -110,7 +53,7 @@ class FlightWaypointType(Enum):
LANDING_POINT = 11 # Should land there LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # Unused. TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour) CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16 JOIN = 16
SPLIT = 17 SPLIT = 17
@@ -122,9 +65,6 @@ class FlightWaypointType(Enum):
DIVERT = 23 DIVERT = 23
INGRESS_OCA_RUNWAY = 24 INGRESS_OCA_RUNWAY = 24
INGRESS_OCA_AIRCRAFT = 25 INGRESS_OCA_AIRCRAFT = 25
PICKUP = 26
DROP_OFF = 27
BULLSEYE = 28
class FlightWaypoint: class FlightWaypoint:
@@ -154,7 +94,7 @@ class FlightWaypoint:
# Only used in the waypoint list in the flight edit page. No sense # Only used in the waypoint list in the flight edit page. No sense
# having three names. A short and long form is enough. # having three names. A short and long form is enough.
self.description = "" self.description = ""
self.targets: List[Union[MissionTarget, Unit]] = [] self.targets: List[MissionTarget] = []
self.obj_name = "" self.obj_name = ""
self.pretty_name = "" self.pretty_name = ""
self.only_for_player = False self.only_for_player = False
@@ -201,55 +141,12 @@ class FlightWaypoint:
return waypoint return waypoint
class FlightRoster:
def __init__(self, squadron: Squadron, initial_size: int = 0) -> None:
self.squadron = squadron
self.pilots: list[Optional[Pilot]] = []
self.resize(initial_size)
@property
def max_size(self) -> int:
return len(self.pilots)
@property
def player_count(self) -> int:
return len([p for p in self.pilots if p is not None and p.player])
@property
def missing_pilots(self) -> int:
return len([p for p in self.pilots if p is None])
def resize(self, new_size: int) -> None:
if self.max_size > new_size:
self.squadron.return_pilots(
[p for p in self.pilots[new_size:] if p is not None]
)
self.pilots = self.pilots[:new_size]
return
self.pilots.extend(
[
self.squadron.claim_available_pilot()
for _ in range(new_size - self.max_size)
]
)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
if pilot is not None:
self.squadron.claim_pilot(pilot)
if (current_pilot := self.pilots[index]) is not None:
self.squadron.return_pilot(current_pilot)
self.pilots[index] = pilot
def clear(self) -> None:
self.squadron.return_pilots([p for p in self.pilots if p is not None])
class Flight: class Flight:
def __init__( def __init__(
self, self,
package: Package, package: Package,
country: str, country: str,
squadron: Squadron, unit_type: Type[FlyingType],
count: int, count: int,
flight_type: FlightType, flight_type: FlightType,
start_type: str, start_type: str,
@@ -257,30 +154,23 @@ class Flight:
arrival: ControlPoint, arrival: ControlPoint,
divert: Optional[ControlPoint], divert: Optional[ControlPoint],
custom_name: Optional[str] = None, custom_name: Optional[str] = None,
cargo: Optional[TransferOrder] = None,
roster: Optional[FlightRoster] = None,
) -> None: ) -> None:
self.package = package self.package = package
self.country = country self.country = country
self.squadron = squadron self.unit_type = unit_type
if roster is None: self.count = count
self.roster = FlightRoster(self.squadron, initial_size=count)
else:
self.roster = roster
self.departure = departure self.departure = departure
self.arrival = arrival self.arrival = arrival
self.divert = divert self.divert = divert
self.flight_type = flight_type self.flight_type = flight_type
# TODO: Replace with FlightPlan. # TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = [] self.targets: List[MissionTarget] = []
self.loadout = Loadout.default_for(self) self.loadout: Dict[int, Optional[Weapon]] = {}
self.start_type = start_type self.start_type = start_type
self.use_custom_loadout = False self.use_custom_loadout = False
self.client_count = 0
self.custom_name = custom_name self.custom_name = custom_name
# Only used by transport missions.
self.cargo = cargo
# Will be replaced with a more appropriate FlightPlan by # Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an # FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan. # empty flight plan.
@@ -290,18 +180,6 @@ class Flight:
package=package, flight=self, custom_waypoints=[] package=package, flight=self, custom_waypoints=[]
) )
@property
def count(self) -> int:
return self.roster.max_size
@property
def client_count(self) -> int:
return self.roster.player_count
@property
def unit_type(self) -> Type[FlyingType]:
return self.squadron.aircraft
@property @property
def from_cp(self) -> ControlPoint: def from_cp(self) -> ControlPoint:
return self.departure return self.departure
@@ -310,19 +188,6 @@ class Flight:
def points(self) -> List[FlightWaypoint]: def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:] return self.flight_plan.waypoints[1:]
def resize(self, new_size: int) -> None:
self.roster.resize(new_size)
def set_pilot(self, index: int, pilot: Optional[Pilot]) -> None:
self.roster.set_pilot(index, pilot)
@property
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def clear_roster(self) -> None:
self.roster.clear()
def __repr__(self): def __repr__(self):
name = db.unit_type_name(self.unit_type) name = db.unit_type_name(self.unit_type)
if self.custom_name: if self.custom_name:

View File

@@ -10,18 +10,16 @@ from __future__ import annotations
import logging import logging
import math import math
import random import random
from dataclasses import dataclass, field from dataclasses import dataclass
from datetime import timedelta from datetime import timedelta
from functools import cached_property from functools import cached_property
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple
from dcs.mapping import Point from dcs.mapping import Point
from dcs.planes import E_3A, E_2C, A_50, KJ_2000
from dcs.unit import Unit from dcs.unit import Unit
from shapely.geometry import Point as ShapelyPoint from shapely.geometry import Point as ShapelyPoint
from game.data.doctrine import Doctrine from game.data.doctrine import Doctrine
from game.squadrons import Pilot
from game.theater import ( from game.theater import (
Airfield, Airfield,
ControlPoint, ControlPoint,
@@ -31,7 +29,7 @@ from game.theater import (
TheaterGroundObject, TheaterGroundObject,
) )
from game.theater.theatergroundobject import EwrGroundObject from game.theater.theatergroundobject import EwrGroundObject
from game.utils import Distance, Speed, feet, meters, nautical_miles from game.utils import Distance, Speed, meters, nautical_miles
from .closestairfields import ObjectiveDistanceCache from .closestairfields import ObjectiveDistanceCache
from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType
from .traveltime import GroundSpeed, TravelTime from .traveltime import GroundSpeed, TravelTime
@@ -41,7 +39,6 @@ from ..conflictgen import Conflict, FRONTLINE_LENGTH
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from gen.ato import Package from gen.ato import Package
from game.transfers import Convoy
INGRESS_TYPES = { INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS, FlightWaypointType.INGRESS_CAS,
@@ -125,10 +122,6 @@ class FlightPlan:
""" """
raise NotImplementedError raise NotImplementedError
@property
def tot(self) -> timedelta:
return self.package.time_over_target + self.tot_offset
@cached_property @cached_property
def bingo_fuel(self) -> int: def bingo_fuel(self) -> int:
"""Bingo fuel value for the FlightPlan""" """Bingo fuel value for the FlightPlan"""
@@ -202,28 +195,15 @@ class FlightPlan:
def dismiss_escort_at(self) -> Optional[FlightWaypoint]: def dismiss_escort_at(self) -> Optional[FlightWaypoint]:
return None return None
def escorted_waypoints(self) -> Iterator[FlightWaypoint]:
begin = self.request_escort_at()
end = self.dismiss_escort_at()
if begin is None or end is None:
return
escorting = False
for waypoint in self.waypoints:
if waypoint == begin:
escorting = True
if escorting:
yield waypoint
if waypoint == end:
return
def takeoff_time(self) -> Optional[timedelta]: def takeoff_time(self) -> Optional[timedelta]:
tot_waypoint = self.tot_waypoint tot_waypoint = self.tot_waypoint
if tot_waypoint is None: if tot_waypoint is None:
return None return None
time = self.tot time = self.tot_for_waypoint(tot_waypoint)
if time is None: if time is None:
return None return None
time += self.tot_offset
return time - self._travel_time_to_waypoint(tot_waypoint) return time - self._travel_time_to_waypoint(tot_waypoint)
def startup_time(self) -> Optional[timedelta]: def startup_time(self) -> Optional[timedelta]:
@@ -260,7 +240,7 @@ class FlightPlan:
if self.flight.from_cp.is_fleet: if self.flight.from_cp.is_fleet:
return timedelta(minutes=2) return timedelta(minutes=2)
else: else:
return timedelta(minutes=8) return timedelta(minutes=5)
@property @property
def mission_departure_time(self) -> timedelta: def mission_departure_time(self) -> timedelta:
@@ -298,11 +278,11 @@ class LoiterFlightPlan(FlightPlan):
travel_time = super().travel_time_between_waypoints(a, b) travel_time = super().travel_time_between_waypoints(a, b)
if a != self.hold: if a != self.hold:
return travel_time return travel_time
return travel_time + self.hold_duration try:
return travel_time + self.hold_duration
@property except AttributeError:
def mission_departure_time(self) -> timedelta: # Save compat for 2.3.
raise NotImplementedError return travel_time + timedelta(minutes=5)
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -443,7 +423,6 @@ class BarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint] divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff yield self.takeoff
@@ -456,7 +435,6 @@ class BarCapFlightPlan(PatrollingFlightPlan):
yield self.land yield self.land
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye
@dataclass(frozen=True) @dataclass(frozen=True)
@@ -465,7 +443,6 @@ class CasFlightPlan(PatrollingFlightPlan):
target: FlightWaypoint target: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint] divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff yield self.takeoff
@@ -479,7 +456,6 @@ class CasFlightPlan(PatrollingFlightPlan):
yield self.land yield self.land
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye
def request_escort_at(self) -> Optional[FlightWaypoint]: def request_escort_at(self) -> Optional[FlightWaypoint]:
return self.patrol_start return self.patrol_start
@@ -493,7 +469,6 @@ class TarCapFlightPlan(PatrollingFlightPlan):
takeoff: FlightWaypoint takeoff: FlightWaypoint
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint] divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
lead_time: timedelta lead_time: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -507,7 +482,6 @@ class TarCapFlightPlan(PatrollingFlightPlan):
yield self.land yield self.land
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye
@property @property
def tot_offset(self) -> timedelta: def tot_offset(self) -> timedelta:
@@ -523,7 +497,7 @@ class TarCapFlightPlan(PatrollingFlightPlan):
start = self.package.escort_start_time start = self.package.escort_start_time
if start is not None: if start is not None:
return start + self.tot_offset return start + self.tot_offset
return self.tot return super().patrol_start_time + self.tot_offset
@property @property
def patrol_end_time(self) -> timedelta: def patrol_end_time(self) -> timedelta:
@@ -546,8 +520,6 @@ class StrikeFlightPlan(FormationFlightPlan):
nav_from: List[FlightWaypoint] nav_from: List[FlightWaypoint]
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint] divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
lead_time: timedelta = field(default_factory=timedelta)
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff yield self.takeoff
@@ -562,7 +534,6 @@ class StrikeFlightPlan(FormationFlightPlan):
yield self.land yield self.land
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye
@property @property
def package_speed_waypoints(self) -> Set[FlightWaypoint]: def package_speed_waypoints(self) -> Set[FlightWaypoint]:
@@ -586,13 +557,6 @@ class StrikeFlightPlan(FormationFlightPlan):
def tot_waypoint(self) -> FlightWaypoint: def tot_waypoint(self) -> FlightWaypoint:
return self.targets[0] return self.targets[0]
@property
def tot_offset(self) -> timedelta:
try:
return -self.lead_time
except AttributeError:
return timedelta()
@property @property
def target_area_waypoint(self) -> FlightWaypoint: def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint( return FlightWaypoint(
@@ -625,6 +589,10 @@ class StrikeFlightPlan(FormationFlightPlan):
) )
return total return total
@property
def mission_speed(self) -> Speed:
return GroundSpeed.for_flight(self.flight, self.ingress.alt)
@property @property
def join_time(self) -> timedelta: def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(self.join, self.ingress) travel_time = self.travel_time_between_waypoints(self.join, self.ingress)
@@ -637,7 +605,7 @@ class StrikeFlightPlan(FormationFlightPlan):
@property @property
def ingress_time(self) -> timedelta: def ingress_time(self) -> timedelta:
tot = self.tot tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.ingress, self.target_area_waypoint self.ingress, self.target_area_waypoint
) )
@@ -645,7 +613,7 @@ class StrikeFlightPlan(FormationFlightPlan):
@property @property
def egress_time(self) -> timedelta: def egress_time(self) -> timedelta:
tot = self.tot tot = self.package.time_over_target
travel_time = self.travel_time_between_waypoints( travel_time = self.travel_time_between_waypoints(
self.target_area_waypoint, self.egress self.target_area_waypoint, self.egress
) )
@@ -657,7 +625,7 @@ class StrikeFlightPlan(FormationFlightPlan):
elif waypoint == self.egress: elif waypoint == self.egress:
return self.egress_time return self.egress_time
elif waypoint in self.targets: elif waypoint in self.targets:
return self.tot return self.package.time_over_target
return super().tot_for_waypoint(waypoint) return super().tot_for_waypoint(waypoint)
@@ -670,7 +638,6 @@ class SweepFlightPlan(LoiterFlightPlan):
nav_from: List[FlightWaypoint] nav_from: List[FlightWaypoint]
land: FlightWaypoint land: FlightWaypoint
divert: Optional[FlightWaypoint] divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
lead_time: timedelta lead_time: timedelta
def iter_waypoints(self) -> Iterator[FlightWaypoint]: def iter_waypoints(self) -> Iterator[FlightWaypoint]:
@@ -683,7 +650,6 @@ class SweepFlightPlan(LoiterFlightPlan):
yield self.land yield self.land
if self.divert is not None: if self.divert is not None:
yield self.divert yield self.divert
yield self.bullseye
@property @property
def tot_waypoint(self) -> Optional[FlightWaypoint]: def tot_waypoint(self) -> Optional[FlightWaypoint]:
@@ -702,7 +668,7 @@ class SweepFlightPlan(LoiterFlightPlan):
@property @property
def sweep_end_time(self) -> timedelta: def sweep_end_time(self) -> timedelta:
return self.tot return self.package.time_over_target + self.tot_offset
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.sweep_start: if waypoint == self.sweep_start:
@@ -728,89 +694,6 @@ class SweepFlightPlan(LoiterFlightPlan):
return self.sweep_end_time return self.sweep_end_time
@dataclass(frozen=True)
class AwacsFlightPlan(LoiterFlightPlan):
takeoff: FlightWaypoint
nav_to: List[FlightWaypoint]
nav_from: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to
yield self.hold
yield from self.nav_from
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def mission_start_time(self) -> Optional[timedelta]:
return self.takeoff_time()
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
if waypoint == self.hold:
return self.package.time_over_target
return None
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.hold
@property
def push_time(self) -> timedelta:
return self.package.time_over_target + self.hold_duration
@property
def mission_departure_time(self) -> timedelta:
return self.push_time
@dataclass(frozen=True)
class AirliftFlightPlan(FlightPlan):
takeoff: FlightWaypoint
nav_to_pickup: List[FlightWaypoint]
pickup: Optional[FlightWaypoint]
nav_to_drop_off: List[FlightWaypoint]
drop_off: FlightWaypoint
nav_to_home: List[FlightWaypoint]
land: FlightWaypoint
divert: Optional[FlightWaypoint]
bullseye: FlightWaypoint
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
yield self.takeoff
yield from self.nav_to_pickup
if self.pickup:
yield self.pickup
yield from self.nav_to_drop_off
yield self.drop_off
yield from self.nav_to_home
yield self.land
if self.divert is not None:
yield self.divert
yield self.bullseye
@property
def tot_waypoint(self) -> Optional[FlightWaypoint]:
return self.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
# TOT planning isn't really useful for transports. They're behind the front
# lines so no need to wait for escorts or for other missions to complete.
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]:
return None
@property
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
@dataclass(frozen=True) @dataclass(frozen=True)
class CustomFlightPlan(FlightPlan): class CustomFlightPlan(FlightPlan):
custom_waypoints: List[FlightWaypoint] custom_waypoints: List[FlightWaypoint]
@@ -858,7 +741,11 @@ class FlightPlanBuilder:
self.game = game self.game = game
self.package = package self.package = package
self.is_player = is_player self.is_player = is_player
self.doctrine: Doctrine = self.game.faction_for(self.is_player).doctrine if is_player:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
self.doctrine: Doctrine = faction.doctrine
self.threat_zones = self.game.threat_zone_for(not self.is_player) self.threat_zones = self.game.threat_zone_for(not self.is_player)
def populate_flight_plan( def populate_flight_plan(
@@ -870,19 +757,9 @@ class FlightPlanBuilder:
"""Creates a default flight plan for the given mission.""" """Creates a default flight plan for the given mission."""
if flight not in self.package.flights: if flight not in self.package.flights:
raise RuntimeError("Flight must be a part of the package") raise RuntimeError("Flight must be a part of the package")
if self.package.waypoints is None:
from game.navmesh import NavMeshError self.regenerate_package_waypoints()
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
try:
if self.package.waypoints is None:
self.regenerate_package_waypoints()
flight.flight_plan = self.generate_flight_plan(flight, custom_targets)
except NavMeshError as ex:
color = "blue" if self.is_player else "red"
raise PlanningError(
f"Could not plan {color} {flight.flight_type.value} from "
f"{flight.departure} to {flight.package.target}"
) from ex
def generate_flight_plan( def generate_flight_plan(
self, flight: Flight, custom_targets: Optional[List[Unit]] self, flight: Flight, custom_targets: Optional[List[Unit]]
@@ -907,18 +784,12 @@ class FlightPlanBuilder:
return self.generate_runway_attack(flight) return self.generate_runway_attack(flight)
elif task == FlightType.SEAD: elif task == FlightType.SEAD:
return self.generate_sead(flight, custom_targets) return self.generate_sead(flight, custom_targets)
elif task == FlightType.SEAD_ESCORT:
return self.generate_escort(flight)
elif task == FlightType.STRIKE: elif task == FlightType.STRIKE:
return self.generate_strike(flight) return self.generate_strike(flight)
elif task == FlightType.SWEEP: elif task == FlightType.SWEEP:
return self.generate_sweep(flight) return self.generate_sweep(flight)
elif task == FlightType.TARCAP: elif task == FlightType.TARCAP:
return self.generate_tarcap(flight) return self.generate_tarcap(flight)
elif task == FlightType.AEWC:
return self.generate_aewc(flight)
elif task == FlightType.TRANSPORT:
return self.generate_transport(flight)
raise PlanningError(f"{task} flight plan generation not implemented") raise PlanningError(f"{task} flight plan generation not implemented")
def regenerate_package_waypoints(self) -> None: def regenerate_package_waypoints(self) -> None:
@@ -1049,49 +920,7 @@ class FlightPlanBuilder:
flight, location, FlightWaypointType.INGRESS_STRIKE, targets flight, location, FlightWaypointType.INGRESS_STRIKE, targets
) )
def generate_aewc(self, flight: Flight) -> AwacsFlightPlan: def generate_bai(self, flight: Flight) -> StrikeFlightPlan:
"""Generate a AWACS flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
location = self.package.target
orbit_location = self.aewc_orbit(location)
# As high as possible to maximize detection and on-station time.
if flight.unit_type == E_2C:
patrol_alt = feet(30000)
elif flight.unit_type == E_3A:
patrol_alt = feet(35000)
elif flight.unit_type == A_50:
patrol_alt = feet(33000)
elif flight.unit_type == KJ_2000:
patrol_alt = feet(40000)
else:
patrol_alt = feet(25000)
builder = WaypointBuilder(flight, self.game, self.is_player)
orbit_location = builder.orbit(orbit_location, patrol_alt)
return AwacsFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to=builder.nav_path(
flight.departure.position, orbit_location.position, patrol_alt
),
nav_from=builder.nav_path(
orbit_location.position, flight.arrival.position, patrol_alt
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
hold=orbit_location,
hold_duration=timedelta(hours=4),
)
def generate_bai(self, flight: Flight) -> FlightPlan:
"""Generates a BAI flight plan. """Generates a BAI flight plan.
Args: Args:
@@ -1099,28 +928,17 @@ class FlightPlanBuilder:
""" """
location = self.package.target location = self.package.target
from game.transfers import Convoy if not isinstance(location, TheaterGroundObject):
raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = [] targets: List[StrikeTarget] = []
if isinstance(location, TheaterGroundObject): for group in location.groups:
for group in location.groups: targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
if group.units:
targets.append(
StrikeTarget(f"{group.name} at {location.name}", group)
)
elif isinstance(location, Convoy):
targets.append(StrikeTarget(location.name, location))
else:
raise InvalidObjectiveLocation(flight.flight_type, location)
return self.strike_flightplan( return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets flight, location, FlightWaypointType.INGRESS_BAI, targets
) )
@staticmethod
def anti_ship_targets_for_tgo(tgo: TheaterGroundObject) -> List[StrikeTarget]:
return [StrikeTarget(f"{g.name} at {tgo.name}", g) for g in tgo.groups]
def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan: def generate_anti_ship(self, flight: Flight) -> StrikeFlightPlan:
"""Generates an anti-ship flight plan. """Generates an anti-ship flight plan.
@@ -1129,20 +947,20 @@ class FlightPlanBuilder:
""" """
location = self.package.target location = self.package.target
from game.transfers import CargoShip
if isinstance(location, ControlPoint): if isinstance(location, ControlPoint):
if not location.is_fleet: if location.is_fleet:
# The first group generated will be the carrier group itself.
location = location.ground_objects[0]
else:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
# The first group generated will be the carrier group itself.
targets = self.anti_ship_targets_for_tgo(location.ground_objects[0]) if not isinstance(location, TheaterGroundObject):
elif isinstance(location, TheaterGroundObject):
targets = self.anti_ship_targets_for_tgo(location)
elif isinstance(location, CargoShip):
targets = [StrikeTarget(location.name, location)]
else:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
targets: List[StrikeTarget] = []
for group in location.groups:
targets.append(StrikeTarget(f"{group.name} at {location.name}", group))
return self.strike_flightplan( return self.strike_flightplan(
flight, location, FlightWaypointType.INGRESS_BAI, targets flight, location, FlightWaypointType.INGRESS_BAI, targets
) )
@@ -1185,7 +1003,6 @@ class FlightPlanBuilder:
patrol_end=end, patrol_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
) )
def generate_sweep(self, flight: Flight) -> SweepFlightPlan: def generate_sweep(self, flight: Flight) -> SweepFlightPlan:
@@ -1222,59 +1039,6 @@ class FlightPlanBuilder:
sweep_end=end, sweep_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
)
def generate_transport(self, flight: Flight) -> AirliftFlightPlan:
"""Generate an airlift flight at a given location.
Args:
flight: The flight to generate the flight plan for.
"""
cargo = flight.cargo
if cargo is None:
raise PlanningError(
"Cannot plan transport mission for flight with no cargo."
)
altitude = feet(1500)
altitude_is_agl = True
builder = WaypointBuilder(flight, self.game, self.is_player)
pickup = None
nav_to_pickup = []
if cargo.origin != flight.departure:
pickup = builder.pickup(cargo.origin)
nav_to_pickup = builder.nav_path(
flight.departure.position,
cargo.origin.position,
altitude,
altitude_is_agl,
)
return AirliftFlightPlan(
package=self.package,
flight=flight,
takeoff=builder.takeoff(flight.departure),
nav_to_pickup=nav_to_pickup,
pickup=pickup,
nav_to_drop_off=builder.nav_path(
cargo.origin.position,
cargo.next_stop.position,
altitude,
altitude_is_agl,
),
drop_off=builder.drop_off(cargo.next_stop),
nav_to_home=builder.nav_path(
cargo.origin.position,
flight.arrival.position,
altitude,
altitude_is_agl,
),
land=builder.land(flight.arrival),
divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
) )
def racetrack_for_objective( def racetrack_for_objective(
@@ -1338,32 +1102,14 @@ class FlightPlanBuilder:
start = end.point_from_heading(heading - 180, diameter) start = end.point_from_heading(heading - 180, diameter)
return start, end return start, end
def aewc_orbit(self, location: MissionTarget) -> Point:
closest_boundary = self.threat_zones.closest_boundary(location.position)
heading_to_threat_boundary = location.position.heading_between_point(
closest_boundary
)
distance_to_threat = meters(
location.position.distance_to_point(closest_boundary)
)
orbit_heading = heading_to_threat_boundary
# Station 100nm outside the threat zone.
threat_buffer = nautical_miles(100)
if self.threat_zones.threatened(location.position):
orbit_distance = distance_to_threat + threat_buffer
else:
orbit_distance = distance_to_threat - threat_buffer
return location.position.point_from_heading(
orbit_heading, orbit_distance.meters
)
def racetrack_for_frontline( def racetrack_for_frontline(
self, origin: Point, front_line: FrontLine self, origin: Point, front_line: FrontLine
) -> Tuple[Point, Point]: ) -> Tuple[Point, Point]:
ally_cp, enemy_cp = front_line.control_points
# Find targets waypoints # Find targets waypoints
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(
front_line, self.game.theater ally_cp, enemy_cp, self.game.theater
) )
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
orbit_center = center.point_from_heading( orbit_center = center.point_from_heading(
@@ -1430,7 +1176,6 @@ class FlightPlanBuilder:
patrol_end=end, patrol_end=end,
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
) )
def generate_dead( def generate_dead(
@@ -1524,11 +1269,7 @@ class FlightPlanBuilder:
targets.append(StrikeTarget(location.name, target)) targets.append(StrikeTarget(location.name, target))
return self.strike_flightplan( return self.strike_flightplan(
flight, flight, location, FlightWaypointType.INGRESS_SEAD, targets
location,
FlightWaypointType.INGRESS_SEAD,
targets,
lead_time=timedelta(minutes=1),
) )
def generate_escort(self, flight: Flight) -> StrikeFlightPlan: def generate_escort(self, flight: Flight) -> StrikeFlightPlan:
@@ -1563,7 +1304,6 @@ class FlightPlanBuilder:
), ),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
) )
def generate_cas(self, flight: Flight) -> CasFlightPlan: def generate_cas(self, flight: Flight) -> CasFlightPlan:
@@ -1578,7 +1318,7 @@ class FlightPlanBuilder:
raise InvalidObjectiveLocation(flight.flight_type, location) raise InvalidObjectiveLocation(flight.flight_type, location)
ingress, heading, distance = Conflict.frontline_vector( ingress, heading, distance = Conflict.frontline_vector(
location, self.game.theater location.control_points[0], location.control_points[1], self.game.theater
) )
center = ingress.point_from_heading(heading, distance / 2) center = ingress.point_from_heading(heading, distance / 2)
egress = ingress.point_from_heading(heading, distance) egress = ingress.point_from_heading(heading, distance)
@@ -1609,7 +1349,6 @@ class FlightPlanBuilder:
patrol_end=builder.egress(egress, location), patrol_end=builder.egress(egress, location),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
) )
@staticmethod @staticmethod
@@ -1706,7 +1445,6 @@ class FlightPlanBuilder:
location: MissionTarget, location: MissionTarget,
ingress_type: FlightWaypointType, ingress_type: FlightWaypointType,
targets: Optional[List[StrikeTarget]] = None, targets: Optional[List[StrikeTarget]] = None,
lead_time: timedelta = timedelta(),
) -> StrikeFlightPlan: ) -> StrikeFlightPlan:
assert self.package.waypoints is not None assert self.package.waypoints is not None
builder = WaypointBuilder(flight, self.game, self.is_player, targets) builder = WaypointBuilder(flight, self.game, self.is_player, targets)
@@ -1745,8 +1483,6 @@ class FlightPlanBuilder:
), ),
land=builder.land(flight.arrival), land=builder.land(flight.arrival),
divert=builder.divert(flight.divert), divert=builder.divert(flight.divert),
bullseye=builder.bullseye(),
lead_time=lead_time,
) )
def _retreating_rendezvous_point(self, attack_transition: Point) -> Point: def _retreating_rendezvous_point(self, attack_transition: Point) -> Point:
@@ -1811,7 +1547,7 @@ class FlightPlanBuilder:
# We'll always have a package, but if this is being planned via the UI # We'll always have a package, but if this is being planned via the UI
# it could be the first flight in the package. # it could be the first flight in the package.
if not self.package.flights: if not self.package.flights:
raise PlanningError( raise RuntimeError(
"Cannot determine source airfield for package with no flights" "Cannot determine source airfield for package with no flights"
) )
@@ -1823,4 +1559,4 @@ class FlightPlanBuilder:
for flight in self.package.flights: for flight in self.package.flights:
if flight.departure == airfield: if flight.departure == airfield:
return airfield return airfield
raise PlanningError("Could not find any airfield assigned to this package") raise RuntimeError("Could not find any airfield assigned to this package")

View File

@@ -1,139 +0,0 @@
from __future__ import annotations
import datetime
from typing import Optional, List, Iterator, Type, TYPE_CHECKING, Mapping
from dcs.unittype import FlyingType
from game.data.weapons import Weapon, Pylon
if TYPE_CHECKING:
from gen.flights.flight import Flight
class Loadout:
def __init__(
self,
name: str,
pylons: Mapping[int, Optional[Weapon]],
date: Optional[datetime.date],
is_custom: bool = False,
) -> None:
self.name = name
self.pylons = {k: v for k, v in pylons.items() if v is not None}
self.date = date
self.is_custom = is_custom
def derive_custom(self, name: str) -> Loadout:
return Loadout(name, self.pylons, self.date, is_custom=True)
def degrade_for_date(
self, unit_type: Type[FlyingType], date: datetime.date
) -> Loadout:
if self.date is not None and self.date <= date:
return Loadout(self.name, self.pylons, self.date)
new_pylons = dict(self.pylons)
for pylon_number, weapon in self.pylons.items():
if weapon is None:
del new_pylons[pylon_number]
continue
if not weapon.available_on(date):
pylon = Pylon.for_aircraft(unit_type, pylon_number)
for fallback in weapon.fallbacks:
if not pylon.can_equip(fallback):
continue
if not fallback.available_on(date):
continue
new_pylons[pylon_number] = fallback
break
else:
del new_pylons[pylon_number]
return Loadout(f"{self.name} ({date.year})", new_pylons, date)
@classmethod
def iter_for(cls, flight: Flight) -> Iterator[Loadout]:
# Dict of payload ID (numeric) to:
#
# {
# "name": The name the user set in the ME
# "pylons": List (as a dict) of dicts of:
# {"CLSID": class ID, "num": pylon number}
# "tasks": List (as a dict) of task IDs the payload is used by.
# }
payloads = flight.unit_type.load_payloads()
for payload in payloads.values():
name = payload["name"]
pylons = payload["pylons"]
yield Loadout(
name,
{p["num"]: Weapon.from_clsid(p["CLSID"]) for p in pylons.values()},
date=None,
)
@classmethod
def all_for(cls, flight: Flight) -> List[Loadout]:
return list(cls.iter_for(flight))
@classmethod
def default_loadout_names_for(cls, flight: Flight) -> Iterator[str]:
from gen.flights.flight import FlightType
# This is a list of mappings from the FlightType of a Flight to the type of
# payload defined in the resources/payloads/UNIT_TYPE.lua file. A Flight has no
# concept of a PyDCS task, so COMMON_OVERRIDE cannot be used here. This is used
# in the payload editor, for setting the default loadout of an object. The left
# element is the FlightType name, and the right element is a tuple containing
# what is used in the lua file. Some aircraft differ from the standard 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}
legacy_names = {
FlightType.TARCAP: ("CAP HEAVY", "CAP", "Liberation BARCAP"),
FlightType.BARCAP: ("CAP HEAVY", "CAP", "Liberation TARCAP"),
FlightType.CAS: ("CAS MAVERICK F", "CAS"),
FlightType.STRIKE: ("STRIKE",),
FlightType.ANTISHIP: ("ANTISHIP",),
FlightType.SEAD: ("SEAD",),
FlightType.BAI: ("BAI",),
FlightType.OCA_RUNWAY: ("RUNWAY_ATTACK", "RUNWAY_STRIKE"),
FlightType.OCA_AIRCRAFT: ("OCA",),
}
for flight_type, names in legacy_names.items():
loadout_names[flight_type].extend(names)
# A SEAD escort typically does not need a different loadout than a regular
# SEAD flight, so fall back to SEAD if needed.
loadout_names[FlightType.SEAD_ESCORT].extend(loadout_names[FlightType.SEAD])
# Sweep and escort can fall back to TARCAP.
loadout_names[FlightType.ESCORT].extend(loadout_names[FlightType.TARCAP])
loadout_names[FlightType.SWEEP].extend(loadout_names[FlightType.TARCAP])
# Intercept can fall back to BARCAP.
loadout_names[FlightType.INTERCEPTION].extend(loadout_names[FlightType.BARCAP])
# OCA/Aircraft falls back to BAI, which falls back to CAS.
loadout_names[FlightType.BAI].extend(loadout_names[FlightType.CAS])
loadout_names[FlightType.OCA_AIRCRAFT].extend(loadout_names[FlightType.BAI])
# DEAD also falls back to BAI.
loadout_names[FlightType.DEAD].extend(loadout_names[FlightType.BAI])
# OCA/Runway falls back to Strike
loadout_names[FlightType.OCA_RUNWAY].extend(loadout_names[FlightType.STRIKE])
yield from loadout_names[flight.flight_type]
@classmethod
def default_for(cls, flight: Flight) -> Loadout:
# Iterate through each possible payload type for a given aircraft.
# Some aircraft have custom loadouts that in aren't the standard set.
for name in cls.default_loadout_names_for(flight):
# This operation is cached, but must be called before load_by_name will
# work.
flight.unit_type.load_payloads()
payload = flight.unit_type.loadout_by_name(name)
if payload is not None:
return Loadout(
name,
{i: Weapon.from_clsid(d["clsid"]) for i, d in payload},
date=None,
)
# TODO: Try group.load_task_default_loadout(loadout_for_task)
return Loadout("Empty", {}, date=None)

View File

@@ -36,23 +36,23 @@ class GroundSpeed:
# DCS's max speed is in kph at 0 MSL. # DCS's max speed is in kph at 0 MSL.
max_speed = kph(flight.unit_type.max_speed) max_speed = kph(flight.unit_type.max_speed)
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL: if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL:
# Aircraft is supersonic. Limit to mach 0.85 to conserve fuel and # Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
# account for heavily loaded jets. # account for heavily loaded jets.
return mach(0.85, altitude) return mach(0.8, altitude)
# For subsonic aircraft, assume the aircraft can reasonably perform at # For subsonic aircraft, assume the aircraft can reasonably perform at
# 80% of its maximum, and that it can maintain the same mach at altitude # 80% of its maximum, and that it can maintain the same mach at altitude
# as it can at sea level. This probably isn't great assumption, but # as it can at sea level. This probably isn't great assumption, but
# might. be sufficient given the wiggle room. We can come up with # might. be sufficient given the wiggle room. We can come up with
# another heuristic if needed. # another heuristic if needed.
cruise_mach = max_speed.mach() * 0.85 cruise_mach = max_speed.mach() * 0.8
return mach(cruise_mach, altitude) return mach(cruise_mach, altitude)
class TravelTime: class TravelTime:
@staticmethod @staticmethod
def between_points(a: Point, b: Point, speed: Speed) -> timedelta: def between_points(a: Point, b: Point, speed: Speed) -> timedelta:
error_factor = 1.05 error_factor = 1.1
distance = meters(a.distance_to_point(b)) distance = meters(a.distance_to_point(b))
return timedelta(hours=distance.nautical_miles / speed.knots * error_factor) return timedelta(hours=distance.nautical_miles / speed.knots * error_factor)
@@ -72,9 +72,6 @@ class TotEstimator:
return startup_time return startup_time
def earliest_tot(self) -> timedelta: def earliest_tot(self) -> timedelta:
if not self.package.flights:
return timedelta(0)
earliest_tot = max( earliest_tot = max(
(self.earliest_tot_for_flight(f) for f in self.package.flights) (self.earliest_tot_for_flight(f) for f in self.package.flights)
) )

View File

@@ -14,11 +14,10 @@ from typing import (
from dcs.mapping import Point from dcs.mapping import Point
from dcs.unit import Unit from dcs.unit import Unit
from dcs.unitgroup import Group, VehicleGroup from dcs.unitgroup import VehicleGroup
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
from game.transfers import MultiGroupTransport
from game.theater import ( from game.theater import (
ControlPoint, ControlPoint,
@@ -33,7 +32,7 @@ from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True) @dataclass(frozen=True)
class StrikeTarget: class StrikeTarget:
name: str name: str
target: Union[VehicleGroup, TheaterGroundObject, Unit, Group, MultiGroupTransport] target: Union[VehicleGroup, TheaterGroundObject, Unit]
class WaypointBuilder: class WaypointBuilder:
@@ -50,7 +49,6 @@ class WaypointBuilder:
self.threat_zones = game.threat_zone_for(not player) self.threat_zones = game.threat_zone_for(not player)
self.navmesh = game.navmesh_for(player) self.navmesh = game.navmesh_for(player)
self.targets = targets self.targets = targets
self._bullseye = game.bullseye_for(player)
@property @property
def is_helo(self) -> bool: def is_helo(self) -> bool:
@@ -146,19 +144,6 @@ class WaypointBuilder:
waypoint.only_for_player = True waypoint.only_for_player = True
return waypoint return waypoint
def bullseye(self) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.BULLSEYE,
self._bullseye.position.x,
self._bullseye.position.y,
meters(0),
)
waypoint.pretty_name = "Bullseye"
waypoint.description = "Bullseye"
waypoint.name = "BULLSEYE"
waypoint.only_for_player = True
return waypoint
def hold(self, position: Point) -> FlightWaypoint: def hold(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.LOITER, FlightWaypointType.LOITER,
@@ -176,10 +161,8 @@ class WaypointBuilder:
FlightWaypointType.JOIN, FlightWaypointType.JOIN,
position.x, position.x,
position.y, position.y,
meters(80) if self.is_helo else self.doctrine.ingress_altitude, meters(500) if self.is_helo else self.doctrine.ingress_altitude,
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "Join" waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package" waypoint.description = "Rendezvous with package"
waypoint.name = "JOIN" waypoint.name = "JOIN"
@@ -190,10 +173,8 @@ class WaypointBuilder:
FlightWaypointType.SPLIT, FlightWaypointType.SPLIT,
position.x, position.x,
position.y, position.y,
meters(80) if self.is_helo else self.doctrine.ingress_altitude, meters(500) if self.is_helo else self.doctrine.ingress_altitude,
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "Split" waypoint.pretty_name = "Split"
waypoint.description = "Depart from package" waypoint.description = "Depart from package"
waypoint.name = "SPLIT" waypoint.name = "SPLIT"
@@ -209,14 +190,13 @@ class WaypointBuilder:
ingress_type, ingress_type,
position.x, position.x,
position.y, position.y,
meters(50) if self.is_helo else self.doctrine.ingress_altitude, meters(500) if self.is_helo else self.doctrine.ingress_altitude,
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "INGRESS on " + objective.name waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name waypoint.description = "INGRESS on " + objective.name
waypoint.name = "INGRESS" waypoint.name = "INGRESS"
waypoint.targets = objective.strike_targets # TODO: This seems wrong, but it's what was there before.
waypoint.targets.append(objective)
return waypoint return waypoint
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint: def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
@@ -224,10 +204,8 @@ class WaypointBuilder:
FlightWaypointType.EGRESS, FlightWaypointType.EGRESS,
position.x, position.x,
position.y, position.y,
meters(50) if self.is_helo else self.doctrine.ingress_altitude, meters(500) if self.is_helo else self.doctrine.ingress_altitude,
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.pretty_name = "EGRESS from " + target.name waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name waypoint.description = "EGRESS from " + target.name
waypoint.name = "EGRESS" waypoint.name = "EGRESS"
@@ -308,7 +286,7 @@ class WaypointBuilder:
FlightWaypointType.CAS, FlightWaypointType.CAS,
position.x, position.x,
position.y, position.y,
meters(50) if self.is_helo else meters(1000), meters(500) if self.is_helo else meters(1000),
) )
waypoint.alt_type = "RADIO" waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS" waypoint.description = "Provide CAS"
@@ -363,21 +341,6 @@ class WaypointBuilder:
self.race_track_end(end, altitude), self.race_track_end(end, altitude),
) )
@staticmethod
def orbit(start: Point, altitude: Distance) -> FlightWaypoint:
"""Creates an circular orbit point.
Args:
start: Position of the waypoint.
altitude: Altitude of the racetrack.
"""
waypoint = FlightWaypoint(FlightWaypointType.LOITER, start.x, start.y, altitude)
waypoint.name = "ORBIT"
waypoint.description = "Anchor and hold at this point"
waypoint.pretty_name = "Orbit"
return waypoint
@staticmethod @staticmethod
def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint: def sweep_start(position: Point, altitude: Distance) -> FlightWaypoint:
"""Creates a sweep start waypoint. """Creates a sweep start waypoint.
@@ -420,13 +383,10 @@ class WaypointBuilder:
end: The end of the sweep. end: The end of the sweep.
altitude: The sweep altitude. altitude: The sweep altitude.
""" """
return self.sweep_start(start, altitude), self.sweep_end(end, altitude) return (self.sweep_start(start, altitude), self.sweep_end(end, altitude))
def escort( def escort(
self, self, ingress: Point, target: MissionTarget, egress: Point
ingress: Point,
target: MissionTarget,
egress: Point,
) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]: ) -> Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package. """Creates the waypoints needed to escort the package.
@@ -447,10 +407,8 @@ class WaypointBuilder:
FlightWaypointType.TARGET_GROUP_LOC, FlightWaypointType.TARGET_GROUP_LOC,
target.position.x, target.position.x,
target.position.y, target.position.y,
meters(50) if self.is_helo else self.doctrine.ingress_altitude, meters(500) if self.is_helo else self.doctrine.ingress_altitude,
) )
if self.is_helo:
waypoint.alt_type = "RADIO"
waypoint.name = "TARGET" waypoint.name = "TARGET"
waypoint.description = "Escort the package" waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area" waypoint.pretty_name = "Target area"
@@ -459,69 +417,24 @@ class WaypointBuilder:
return ingress, waypoint, egress return ingress, waypoint, egress
@staticmethod @staticmethod
def pickup(control_point: ControlPoint) -> FlightWaypoint: def nav(position: Point, altitude: Distance) -> FlightWaypoint:
"""Creates a cargo pickup waypoint.
Args:
control_point: Pick up location.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PICKUP,
control_point.position.x,
control_point.position.y,
meters(0),
)
waypoint.alt_type = "RADIO"
waypoint.name = "PICKUP"
waypoint.description = f"Pick up cargo from {control_point}"
waypoint.pretty_name = "Pick up location"
return waypoint
@staticmethod
def drop_off(control_point: ControlPoint) -> FlightWaypoint:
"""Creates a cargo drop-off waypoint.
Args:
control_point: Drop-off location.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PICKUP,
control_point.position.x,
control_point.position.y,
meters(0),
)
waypoint.alt_type = "RADIO"
waypoint.name = "DROP OFF"
waypoint.description = f"Drop off cargo at {control_point}"
waypoint.pretty_name = "Drop off location"
return waypoint
@staticmethod
def nav(
position: Point, altitude: Distance, altitude_is_agl: bool = False
) -> FlightWaypoint:
"""Creates a navigation point. """Creates a navigation point.
Args: Args:
position: Position of the waypoint. position: Position of the waypoint.
altitude: Altitude of the waypoint. altitude: Altitude of the waypoint.
altitude_is_agl: True for altitude is AGL. False if altitude is MSL.
""" """
waypoint = FlightWaypoint( waypoint = FlightWaypoint(
FlightWaypointType.NAV, position.x, position.y, altitude FlightWaypointType.NAV, position.x, position.y, altitude
) )
if altitude_is_agl:
waypoint.alt_type = "RADIO"
waypoint.name = "NAV" waypoint.name = "NAV"
waypoint.description = "NAV" waypoint.description = "NAV"
waypoint.pretty_name = "Nav" waypoint.pretty_name = "Nav"
return waypoint return waypoint
def nav_path( def nav_path(self, a: Point, b: Point, altitude: Distance) -> List[FlightWaypoint]:
self, a: Point, b: Point, altitude: Distance, altitude_is_agl: bool = False
) -> List[FlightWaypoint]:
path = self.clean_nav_points(self.navmesh.shortest_path(a, b)) path = self.clean_nav_points(self.navmesh.shortest_path(a, b))
return [self.nav(self.perturb(p), altitude, altitude_is_agl) for p in path] return [self.nav(self.perturb(p), altitude) for p in path]
def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]: def clean_nav_points(self, points: Iterable[Point]) -> Iterator[Point]:
# Examine a sliding window of three waypoints. `current` is the waypoint # Examine a sliding window of three waypoints. `current` is the waypoint

View File

@@ -1,4 +1,3 @@
import logging
import random import random
from enum import Enum from enum import Enum
from typing import Dict, List from typing import Dict, List
@@ -6,8 +5,7 @@ from typing import Dict, List
from dcs.unittype import VehicleType from dcs.unittype import VehicleType
from game.theater import ControlPoint from game.theater import ControlPoint
from gen.ground_forces.ai_ground_planner_db import *
from game.data.groundunitclass import GroundUnitClass
from gen.ground_forces.combat_stance import CombatStance from gen.ground_forces.combat_stance import CombatStance
MAX_COMBAT_GROUP_PER_CP = 10 MAX_COMBAT_GROUP_PER_CP = 10
@@ -22,19 +20,17 @@ class CombatGroupRole(Enum):
LOGI = 6 LOGI = 6
INFANTRY = 7 INFANTRY = 7
ATGM = 8 ATGM = 8
RECON = 9
DISTANCE_FROM_FRONTLINE = { DISTANCE_FROM_FRONTLINE = {
CombatGroupRole.TANK: (2200, 3200), CombatGroupRole.TANK: (2200, 3200),
CombatGroupRole.APC: (2700, 3700), CombatGroupRole.APC: (7500, 8500),
CombatGroupRole.IFV: (2700, 3700), CombatGroupRole.IFV: (2700, 3700),
CombatGroupRole.ARTILLERY: (16000, 18000), CombatGroupRole.ARTILLERY: (16000, 18000),
CombatGroupRole.SHORAD: (5000, 8000), CombatGroupRole.SHORAD: (12000, 13000),
CombatGroupRole.LOGI: (18000, 20000), CombatGroupRole.LOGI: (18000, 20000),
CombatGroupRole.INFANTRY: (2800, 3300), CombatGroupRole.INFANTRY: (2800, 3300),
CombatGroupRole.ATGM: (5200, 6200), CombatGroupRole.ATGM: (5200, 6200),
CombatGroupRole.RECON: (2000, 3000),
} }
GROUP_SIZES_BY_COMBAT_STANCE = { GROUP_SIZES_BY_COMBAT_STANCE = {
@@ -76,7 +72,6 @@ class GroundPlanner:
self.atgm_group: List[CombatGroup] = [] self.atgm_group: List[CombatGroup] = []
self.logi_groups: List[CombatGroup] = [] self.logi_groups: List[CombatGroup] = []
self.shorad_groups: List[CombatGroup] = [] self.shorad_groups: List[CombatGroup] = []
self.recon_groups: List[CombatGroup] = []
self.units_per_cp: Dict[int, List[CombatGroup]] = {} self.units_per_cp: Dict[int, List[CombatGroup]] = {}
for cp in self.connected_enemy_cp: for cp in self.connected_enemy_cp:
@@ -85,10 +80,6 @@ class GroundPlanner:
def plan_groundwar(self): def plan_groundwar(self):
ground_unit_limit = self.cp.frontline_unit_count_limit
remaining_available_frontline_units = ground_unit_limit
if hasattr(self.cp, "stance"): if hasattr(self.cp, "stance"):
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance] group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
else: else:
@@ -96,44 +87,37 @@ class GroundPlanner:
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE] group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# Create combat groups and assign them randomly to each enemy CP # Create combat groups and assign them randomly to each enemy CP
for unit_type in self.cp.base.armor: for key in self.cp.base.armor.keys():
if unit_type in GroundUnitClass.Tank:
role = None
collection = None
if key in TYPE_TANKS:
collection = self.tank_groups collection = self.tank_groups
role = CombatGroupRole.TANK role = CombatGroupRole.TANK
elif unit_type in GroundUnitClass.Apc: elif key in TYPE_APC:
collection = self.apc_group collection = self.apc_group
role = CombatGroupRole.APC role = CombatGroupRole.APC
elif unit_type in GroundUnitClass.Artillery: elif key in TYPE_ARTILLERY:
collection = self.art_group collection = self.art_group
role = CombatGroupRole.ARTILLERY role = CombatGroupRole.ARTILLERY
elif unit_type in GroundUnitClass.Ifv: elif key in TYPE_IFV:
collection = self.ifv_group collection = self.ifv_group
role = CombatGroupRole.IFV role = CombatGroupRole.IFV
elif unit_type in GroundUnitClass.Logistics: elif key in TYPE_LOGI:
collection = self.logi_groups collection = self.logi_groups
role = CombatGroupRole.LOGI role = CombatGroupRole.LOGI
elif unit_type in GroundUnitClass.Atgm: elif key in TYPE_ATGM:
collection = self.atgm_group collection = self.atgm_group
role = CombatGroupRole.ATGM role = CombatGroupRole.ATGM
elif unit_type in GroundUnitClass.Shorads: elif key in TYPE_SHORAD:
collection = self.shorad_groups collection = self.shorad_groups
role = CombatGroupRole.SHORAD role = CombatGroupRole.SHORAD
elif unit_type in GroundUnitClass.Recon:
collection = self.recon_groups
role = CombatGroupRole.RECON
else: else:
logging.warning( print("Warning unit type not handled by ground generator")
f"Unused front line vehicle at base {unit_type}: unknown unit class" print(key)
)
continue continue
available = self.cp.base.armor[unit_type] available = self.cp.base.armor[key]
if available > remaining_available_frontline_units:
available = remaining_available_frontline_units
remaining_available_frontline_units -= available
while available > 0: while available > 0:
if role == CombatGroupRole.SHORAD: if role == CombatGroupRole.SHORAD:
@@ -157,17 +141,14 @@ class GroundPlanner:
group.assigned_enemy_cp = "__reserve__" group.assigned_enemy_cp = "__reserve__"
for i in range(n): for i in range(n):
group.units.append(unit_type) group.units.append(key)
collection.append(group) collection.append(group)
if remaining_available_frontline_units == 0:
break
print("------------------") print("------------------")
print("Ground Planner : ") print("Ground Planner : ")
print(self.cp.name) print(self.cp.name)
print("------------------") print("------------------")
for unit_type in self.units_per_cp.keys(): for key in self.units_per_cp.keys():
print("For : #" + str(unit_type)) print("For : #" + str(key))
for group in self.units_per_cp[unit_type]: for group in self.units_per_cp[key]:
print(str(group)) print(str(group))

View File

@@ -0,0 +1,180 @@
from dcs.vehicles import AirDefence, Infantry, Unarmed, Artillery, Armor
from pydcs_extensions.frenchpack import frenchpack
TYPE_TANKS = [
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_72B3,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_Mk__4,
Armor.ZTZ_96B,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
]
TYPE_ATGM = [
Armor.ATGM_M1045_HMMWV_TOW,
Armor.ATGM_M1134_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
Armor.M30_Cargo_Carrier,
Armor.TD_Jagdpanzer_IV,
Armor.TD_Jagdpanther_G1,
Armor.TD_M10_GMC,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
]
TYPE_IFV = [
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_MCV_80,
Armor.IFV_LAV_25,
Armor.SPG_M1128_Stryker_MGS,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# WW2
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
Armor.Daimler_Armoured_Car,
# Mods
frenchpack.ERC_90,
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13,
]
TYPE_APC = [
Armor.APC_M1043_HMMWV_Armament,
Armor.APC_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.APC_BTR_82A,
Armor.APC_MTLB,
Armor.APC_M2A1,
Armor.APC_Cobra,
Armor.APC_Sd_Kfz_251,
Armor.APC_AAV_7,
Armor.TPz_Fuchs,
Armor.ARV_BRDM_2,
Armor.ARV_BTR_RD,
Armor.FDDM_Grad,
# WW2
Armor.APC_M2A1,
Armor.APC_Sd_Kfz_251,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
]
TYPE_ARTILLERY = [
Artillery.MLRS_9A52_Smerch,
Artillery.SPH_2S1_Gvozdika,
Artillery.SPH_2S3_Akatsia,
Artillery.MLRS_BM_21_Grad,
Artillery.MLRS_9K57_Uragan_BM_27,
Artillery.SPH_M109_Paladin,
Artillery.MLRS_M270,
Artillery.SPH_2S9_Nona,
Artillery.SpGH_Dana,
Artillery.SPH_2S19_Msta,
Artillery.MLRS_FDDM,
# WW2
Artillery.Sturmpanzer_IV_Brummbär,
Artillery.M12_GMC,
]
TYPE_LOGI = [
Unarmed.Transport_M818,
Unarmed.Transport_KAMAZ_43101,
Unarmed.Transport_Ural_375,
Unarmed.Transport_GAZ_66,
Unarmed.Transport_GAZ_3307,
Unarmed.Transport_GAZ_3308,
Unarmed.Transport_Ural_4320_31_Armored,
Unarmed.Transport_Ural_4320T,
Unarmed.Blitz_3_6_6700A,
Unarmed.Kübelwagen_82,
Unarmed.Sd_Kfz_7,
Unarmed.Sd_Kfz_2,
Unarmed.Willys_MB,
Unarmed.Land_Rover_109_S3,
Unarmed.Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
]
TYPE_INFANTRY = [
Infantry.Infantry_Soldier_Insurgents,
Infantry.Soldier_AK,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Soldier_M249,
Infantry.Infantry_M4,
Infantry.Soldier_RPG,
]
TYPE_SHORAD = [
AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZSU_57_2,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.SAM_SA_8_Osa_9A33,
AirDefence.SAM_SA_9_Strela_1_9P31,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3,
AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_19_Tunguska_2S6,
AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163,
AirDefence.SAM_Linebacker_M6,
AirDefence.SAM_Chaparral_M48,
AirDefence.SAM_Avenger_M1097,
AirDefence.SAM_Roland_ADS,
AirDefence.HQ_7_Self_Propelled_LN,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm,
AirDefence.AAA_M1_37mm,
AirDefence.AA_gun_QF_3_7,
]

View File

@@ -11,11 +11,9 @@ import logging
import random import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List from typing import Dict, Iterator, Optional, TYPE_CHECKING, Type, List
from dcs import Mission, Point, unitgroup from dcs import Mission, Point
from dcs.action import SceneryDestructionZone
from dcs.country import Country from dcs.country import Country
from dcs.point import StaticPoint from dcs.statics import fortification_map, warehouse_map
from dcs.statics import Fortification, fortification_map, warehouse_map
from dcs.task import ( from dcs.task import (
ActivateBeaconCommand, ActivateBeaconCommand,
ActivateICLSCommand, ActivateICLSCommand,
@@ -23,8 +21,7 @@ from dcs.task import (
OptAlarmState, OptAlarmState,
FireAtPoint, FireAtPoint,
) )
from dcs.triggers import TriggerStart, TriggerZone from dcs.unit import Ship, Unit, Vehicle
from dcs.unit import Ship, Unit, Vehicle, SingleHeliPad
from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup from dcs.unitgroup import Group, ShipGroup, StaticGroup, VehicleGroup
from dcs.unittype import StaticType, UnitType from dcs.unittype import StaticType, UnitType
from dcs.vehicles import vehicle_map from dcs.vehicles import vehicle_map
@@ -36,15 +33,13 @@ from game.theater import ControlPoint, TheaterGroundObject
from game.theater.theatergroundobject import ( from game.theater.theatergroundobject import (
BuildingGroundObject, BuildingGroundObject,
CarrierGroundObject, CarrierGroundObject,
FactoryGroundObject,
GenericCarrierGroundObject, GenericCarrierGroundObject,
LhaGroundObject, LhaGroundObject,
ShipGroundObject, ShipGroundObject,
MissileSiteGroundObject, MissileSiteGroundObject,
SceneryGroundObject,
) )
from game.unitmap import UnitMap from game.unitmap import UnitMap
from game.utils import feet, knots, mps from game.utils import knots, mps
from .radios import RadioFrequency, RadioRegistry from .radios import RadioFrequency, RadioRegistry
from .runways import RunwayData from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry from .tacan import TacanBand, TacanChannel, TacanRegistry
@@ -52,6 +47,7 @@ from .tacan import TacanBand, TacanChannel, TacanRegistry
if TYPE_CHECKING: if TYPE_CHECKING:
from game import Game from game import Game
FARP_FRONTLINE_DISTANCE = 10000 FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000 AA_CP_MIN_DISTANCE = 40000
@@ -76,12 +72,8 @@ class GenericGroundObjectGenerator:
self.m = mission self.m = mission
self.unit_map = unit_map self.unit_map = unit_map
@property
def culled(self) -> bool:
return self.game.position_culled(self.ground_object.position)
def generate(self) -> None: def generate(self) -> None:
if self.culled: if self.game.position_culled(self.ground_object.position):
return return
for group in self.ground_object.groups: for group in self.ground_object.groups:
@@ -100,11 +92,13 @@ class GenericGroundObjectGenerator:
position=group.position, position=group.position,
heading=group.units[0].heading, heading=group.units[0].heading,
) )
vg.units[0].name = group.units[0].name vg.units[0].name = self.m.string(group.units[0].name)
vg.units[0].player_can_drive = True vg.units[0].player_can_drive = True
for i, u in enumerate(group.units): for i, u in enumerate(group.units):
if i > 0: if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), u.name, u.type) vehicle = Vehicle(
self.m.next_unit_id(), self.m.string(u.name), u.type
)
vehicle.position.x = u.position.x vehicle.position.x = u.position.x
vehicle.position.y = u.position.y vehicle.position.y = u.position.y
vehicle.heading = u.heading vehicle.heading = u.heading
@@ -134,12 +128,6 @@ class GenericGroundObjectGenerator:
class MissileSiteGenerator(GenericGroundObjectGenerator): class MissileSiteGenerator(GenericGroundObjectGenerator):
@property
def culled(self) -> bool:
# Don't cull missile sites - their range is long enough to make them easily
# culled despite being a threat.
return False
def generate(self) -> None: def generate(self) -> None:
super(MissileSiteGenerator, self).generate() super(MissileSiteGenerator, self).generate()
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now) # Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
@@ -225,7 +213,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
f"{self.ground_object.dcs_identifier} not found in static maps" f"{self.ground_object.dcs_identifier} not found in static maps"
) )
def generate_vehicle_group(self, unit_type: Type[UnitType]) -> None: def generate_vehicle_group(self, unit_type: UnitType) -> None:
if not self.ground_object.is_dead: if not self.ground_object.is_dead:
group = self.m.vehicle_group( group = self.m.vehicle_group(
country=self.country, country=self.country,
@@ -236,7 +224,7 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
) )
self._register_fortification(group) self._register_fortification(group)
def generate_static(self, static_type: Type[StaticType]) -> None: def generate_static(self, static_type: StaticType) -> None:
group = self.m.static_group( group = self.m.static_group(
country=self.country, country=self.country,
name=self.ground_object.group_name, name=self.ground_object.group_name,
@@ -256,74 +244,6 @@ class BuildingSiteGenerator(GenericGroundObjectGenerator):
self.unit_map.add_building(self.ground_object, building) self.unit_map.add_building(self.ground_object, building)
class FactoryGenerator(BuildingSiteGenerator):
"""Generator for factory sites.
Factory sites are the buildings that allow the recruitment of ground units.
Destroying these prevents the owner from recruiting ground units at the connected
control point.
"""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
# TODO: Faction specific?
self.generate_static(Fortification.Workshop_A)
class SceneryGenerator(BuildingSiteGenerator):
def generate(self) -> None:
assert isinstance(self.ground_object, SceneryGroundObject)
trigger_zone = self.generate_trigger_zone(self.ground_object)
# DCS only visually shows a scenery object is dead when
# this trigger rule is applied. Otherwise you can kill a
# structure twice.
if self.ground_object.is_dead:
self.generate_dead_trigger_rule(trigger_zone)
# Tell Liberation to manage this groundobjectsgen as part of the campaign.
self.register_scenery()
def generate_trigger_zone(self, scenery: SceneryGroundObject) -> TriggerZone:
zone = scenery.zone
# Align the trigger zones to the faction color on the DCS briefing/F10 map.
if scenery.is_friendly(to_player=True):
color = {1: 0.2, 2: 0.7, 3: 1, 4: 0.15}
else:
color = {1: 1, 2: 0.2, 3: 0.2, 4: 0.15}
# Create the smallest valid size trigger zone (16 feet) so that risk of overlap is minimized.
# As long as the triggerzone is over the scenery object, we're ok.
smallest_valid_radius = feet(16).meters
return self.m.triggers.add_triggerzone(
zone.position,
smallest_valid_radius,
zone.hidden,
zone.name,
color,
zone.properties,
)
def generate_dead_trigger_rule(self, trigger_zone: TriggerZone) -> None:
# Add destruction zone trigger
t = TriggerStart(comment="Destruction")
t.actions.append(
SceneryDestructionZone(destruction_level=100, zone=trigger_zone.id)
)
self.m.triggerrules.triggers.append(t)
def register_scenery(self) -> None:
scenery = self.ground_object
assert isinstance(scenery, SceneryGroundObject)
self.unit_map.add_scenery(scenery)
class GenericCarrierGenerator(GenericGroundObjectGenerator): class GenericCarrierGenerator(GenericGroundObjectGenerator):
"""Base type for carrier group generation. """Base type for carrier group generation.
@@ -393,13 +313,13 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator):
heading=group.units[0].heading, heading=group.units[0].heading,
) )
ship_group.set_frequency(atc_channel.hertz) ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = group.units[0].name ship_group.units[0].name = self.m.string(group.units[0].name)
return ship_group return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship: def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship( ship = Ship(
self.m.next_unit_id(), self.m.next_unit_id(),
unit.name, self.m.string(unit.name),
unit_type_from_name(unit.type), unit_type_from_name(unit.type),
) )
ship.position.x = unit.position.x ship.position.x = unit.position.x
@@ -482,23 +402,20 @@ class CarrierGenerator(GenericCarrierGenerator):
def tacan_callsign(self) -> str: def tacan_callsign(self) -> str:
# TODO: Assign these properly. # TODO: Assign these properly.
if self.control_point.name == "Carrier Strike Group 8": return random.choice(
return "TRU" [
else: "STE",
return random.choice( "CVN",
[ "CVH",
"STE", "CCV",
"CVN", "ACC",
"CVH", "ARC",
"CCV", "GER",
"ACC", "ABR",
"ARC", "LIN",
"GER", "TRU",
"ABR", ]
"LIN", )
"TRU",
]
)
class LhaGenerator(GenericCarrierGenerator): class LhaGenerator(GenericCarrierGenerator):
@@ -544,11 +461,11 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
position=group_def.position, position=group_def.position,
heading=group_def.units[0].heading, heading=group_def.units[0].heading,
) )
group.units[0].name = group_def.units[0].name group.units[0].name = self.m.string(group_def.units[0].name)
# TODO: Skipping the first unit looks like copy pasta from the carrier. # TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]: for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type) unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(), unit.name, unit_type) ship = Ship(self.m.next_unit_id(), self.m.string(unit.name), unit_type)
ship.position.x = unit.position.x ship.position.x = unit.position.x
ship.position.y = unit.position.y ship.position.y = unit.position.y
ship.heading = unit.heading ship.heading = unit.heading
@@ -557,48 +474,6 @@ class ShipObjectGenerator(GenericGroundObjectGenerator):
self._register_unit_group(group_def, group) self._register_unit_group(group_def, group)
class HelipadGenerator:
"""
Generates helipads for given control point
"""
def __init__(
self,
mission: Mission,
cp: ControlPoint,
game: Game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry,
):
self.m = mission
self.cp = cp
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
def generate(self) -> None:
if self.cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
for i, helipad in enumerate(self.cp.helipads):
name = self.cp.name + "_helipad_" + str(i)
logging.info("Generating helipad : " + name)
pad = SingleHeliPad(name=(name + "_unit"))
pad.position = Point(helipad.x, helipad.y)
pad.heading = helipad.heading
# pad.heliport_frequency = self.radio_registry.alloc_uhf() TODO : alloc radio & callsign
sg = unitgroup.StaticGroup(self.m.next_group_id(), name)
sg.add_unit(pad)
sp = StaticPoint()
sp.position = pad.position
sg.add_point(sp)
country.add_static_group(sg)
class GroundObjectsGenerator: class GroundObjectsGenerator:
"""Creates DCS groups and statics for the theater during mission generation. """Creates DCS groups and statics for the theater during mission generation.
@@ -632,20 +507,8 @@ class GroundObjectsGenerator:
country_name = self.game.enemy_country country_name = self.game.enemy_country
country = self.m.country(country_name) country = self.m.country(country_name)
HelipadGenerator(
self.m, cp, self.game, self.radio_registry, self.tacan_registry
).generate()
for ground_object in cp.ground_objects: for ground_object in cp.ground_objects:
if isinstance(ground_object, FactoryGroundObject): if isinstance(ground_object, BuildingGroundObject):
generator = FactoryGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, SceneryGroundObject):
generator = SceneryGenerator(
ground_object, country, self.game, self.m, self.unit_map
)
elif isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator( generator = BuildingSiteGenerator(
ground_object, country, self.game, self.m, self.unit_map ground_object, country, self.game, self.m, self.unit_map
) )

View File

@@ -23,27 +23,21 @@ only be added per airframe, so PvP missions where each side have the same
aircraft will be able to see the enemy's kneeboard for the same airframe. aircraft will be able to see the enemy's kneeboard for the same airframe.
""" """
import datetime import datetime
import textwrap
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, TYPE_CHECKING, Tuple, Iterator from typing import Dict, List, Optional, TYPE_CHECKING, Tuple
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission from dcs.mission import Mission
from dcs.unit import Unit
from dcs.unittype import FlyingType from dcs.unittype import FlyingType
from tabulate import tabulate from tabulate import tabulate
from game.data.alic import AlicCodes
from game.db import unit_type_from_name
from game.theater import ConflictTheater, TheaterGroundObject, LatLon
from game.theater.bullseye import Bullseye
from game.utils import meters from game.utils import meters
from .aircraft import AIRCRAFT_DATA, FlightData from .aircraft import AIRCRAFT_DATA, FlightData
from .airsupportgen import AwacsInfo, TankerInfo from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType, FlightType from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency from .radios import RadioFrequency
from .runways import RunwayData from .runways import RunwayData
@@ -54,16 +48,8 @@ if TYPE_CHECKING:
class KneeboardPageWriter: class KneeboardPageWriter:
"""Creates kneeboard images.""" """Creates kneeboard images."""
def __init__( def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
self, page_margin: int = 24, line_spacing: int = 12, dark_theme: bool = False self.image = Image.new("RGB", (768, 1024), (0xFF, 0xFF, 0xFF))
) -> None:
if dark_theme:
self.foreground_fill = (215, 200, 200)
self.background_fill = (10, 5, 5)
else:
self.foreground_fill = (15, 15, 15)
self.background_fill = (255, 252, 252)
self.image = Image.new("RGB", (768, 1024), self.background_fill)
# These font sizes create a relatively full page for current sorties. If # These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including # we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should # more information in the comm ladder (the latter of which we should
@@ -101,10 +87,10 @@ class KneeboardPageWriter:
self.y += height + self.line_spacing self.y += height + self.line_spacing
def title(self, title: str) -> None: def title(self, title: str) -> None:
self.text(title, font=self.title_font, fill=self.foreground_fill) self.text(title, font=self.title_font)
def heading(self, text: str) -> None: def heading(self, text: str) -> None:
self.text(text, font=self.heading_font, fill=self.foreground_fill) self.text(text, font=self.heading_font)
def table( def table(
self, cells: List[List[str]], headers: Optional[List[str]] = None self, cells: List[List[str]], headers: Optional[List[str]] = None
@@ -112,7 +98,7 @@ class KneeboardPageWriter:
if headers is None: if headers is None:
headers = [] headers = []
table = tabulate(cells, headers=headers, numalign="right") table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font, fill=self.foreground_fill) self.text(table, font=self.table_font)
def write(self, path: Path) -> None: def write(self, path: Path) -> None:
self.image.save(path) self.image.save(path)
@@ -142,11 +128,6 @@ class KneeboardPage:
"""Writes the kneeboard page to the given path.""" """Writes the kneeboard page to the given path."""
raise NotImplementedError raise NotImplementedError
def format_ll(self, ll: LatLon) -> str:
ns = "N" if ll.latitude >= 0 else "S"
ew = "E" if ll.longitude >= 0 else "W"
return f"{ll.latitude:.4}°{ns} {ll.longitude:.4}°{ew}"
@dataclass(frozen=True) @dataclass(frozen=True)
class NumberedWaypoint: class NumberedWaypoint:
@@ -259,24 +240,23 @@ class BriefingPage(KneeboardPage):
def __init__( def __init__(
self, self,
flight: FlightData, flight: FlightData,
bullseye: Bullseye, comms: List[CommInfo],
theater: ConflictTheater, awacs: List[AwacsInfo],
tankers: List[TankerInfo],
jtacs: List[JtacInfo],
start_time: datetime.datetime, start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None: ) -> None:
self.flight = flight self.flight = flight
self.bullseye = bullseye self.comms = list(comms)
self.theater = theater self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.start_time = start_time self.start_time = start_time
self.dark_kneeboard = dark_kneeboard self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None: def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard) writer = KneeboardPageWriter()
if self.flight.custom_name is not None: writer.title(f"{self.flight.callsign} Mission Info")
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else:
custom_name_title = ""
writer.title(f"{self.flight.callsign} Mission Info{custom_name_title}")
# TODO: Handle carriers. # TODO: Handle carriers.
writer.heading("Airfield Info") writer.heading("Airfield Info")
@@ -298,8 +278,7 @@ class BriefingPage(KneeboardPage):
headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"], headers=["#", "Action", "Alt", "Dist", "GSPD", "Time", "Departure"],
) )
writer.text(f"Bullseye: {self.bullseye.to_lat_lon(self.theater).format_dms()}") flight_plan_builder
writer.table( writer.table(
[ [
[ [
@@ -310,6 +289,37 @@ class BriefingPage(KneeboardPage):
["Bingo", "Joker"], ["Bingo", "Joker"],
) )
# Package Section
writer.heading("Comm ladder")
comm_ladder = []
for comm in self.comms:
comm_ladder.append(
[comm.name, "", "", "", self.format_frequency(comm.freq)]
)
for a in self.awacs:
comm_ladder.append(
[a.callsign, "AWACS", "", "", self.format_frequency(a.freq)]
)
for tanker in self.tankers:
comm_ladder.append(
[
tanker.callsign,
"Tanker",
tanker.variant,
str(tanker.tacan),
self.format_frequency(tanker.freq),
]
)
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
writer.heading("JTAC")
jtacs = []
for jtac in self.jtacs:
jtacs.append([jtac.callsign, jtac.region, jtac.code])
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"])
writer.write(path) writer.write(path)
def airfield_info_row( def airfield_info_row(
@@ -343,110 +353,13 @@ class BriefingPage(KneeboardPage):
ils = "" ils = ""
return [ return [
row_title, row_title,
"\n".join(textwrap.wrap(runway.airfield_name, width=24)), runway.airfield_name,
atc, atc,
tacan, tacan,
ils, ils,
runway.runway_name, runway.runway_name,
] ]
def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency)
if channel is None:
return str(frequency)
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name}\n{frequency}"
class SupportPage(KneeboardPage):
"""A kneeboard page containing information about support units."""
def __init__(
self,
flight: FlightData,
comms: List[CommInfo],
awacs: List[AwacsInfo],
tankers: List[TankerInfo],
jtacs: List[JtacInfo],
start_time: datetime.datetime,
dark_kneeboard: bool,
) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.start_time = start_time
self.dark_kneeboard = dark_kneeboard
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
if self.flight.custom_name is not None:
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else:
custom_name_title = ""
writer.title(f"{self.flight.callsign} Support Info{custom_name_title}")
# AEW&C
writer.heading("AEW&C")
aewc_ladder = []
for single_aewc in self.awacs:
if single_aewc.depature_location is None:
dep = "-"
arr = "-"
else:
dep = self._format_time(single_aewc.start_time)
arr = self._format_time(single_aewc.end_time)
aewc_ladder.append(
[
str(single_aewc.callsign),
str(single_aewc.freq),
str(single_aewc.depature_location),
str(dep),
str(arr),
]
)
writer.table(
aewc_ladder,
headers=["Callsign", "FREQ", "Depature", "ETD", "ETA"],
)
# Package Section
writer.heading("Comm ladder")
comm_ladder = []
for comm in self.comms:
comm_ladder.append(
[comm.name, "", "", "", self.format_frequency(comm.freq)]
)
for tanker in self.tankers:
comm_ladder.append(
[
tanker.callsign,
"Tanker",
tanker.variant,
str(tanker.tacan),
self.format_frequency(tanker.freq),
]
)
writer.table(comm_ladder, headers=["Callsign", "Task", "Type", "TACAN", "FREQ"])
writer.heading("JTAC")
jtacs = []
for jtac in self.jtacs:
jtacs.append([jtac.callsign, jtac.region, jtac.code])
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"])
writer.write(path)
def format_frequency(self, frequency: RadioFrequency) -> str: def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency) channel = self.flight.channel_for(frequency)
if channel is None: if channel is None:
@@ -456,109 +369,12 @@ class SupportPage(KneeboardPage):
channel_name = namer.channel_name(channel.radio_id, channel.channel) channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name} {frequency}" return f"{channel_name} {frequency}"
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None:
return ""
local_time = self.start_time + time
return local_time.strftime(f"%H:%M:%S")
class SeadTaskPage(KneeboardPage):
"""A kneeboard page containing SEAD/DEAD target information."""
def __init__(
self, flight: FlightData, dark_kneeboard: bool, theater: ConflictTheater
) -> None:
self.flight = flight
self.dark_kneeboard = dark_kneeboard
self.theater = theater
@property
def target_units(self) -> Iterator[Unit]:
if isinstance(self.flight.package.target, TheaterGroundObject):
yield from self.flight.package.target.units
@staticmethod
def alic_for(unit: Unit) -> str:
try:
return str(AlicCodes.code_for(unit))
except KeyError:
return ""
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
if self.flight.custom_name is not None:
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else:
custom_name_title = ""
task = "DEAD" if self.flight.flight_type == FlightType.DEAD else "SEAD"
writer.title(f"{self.flight.callsign} {task} Target Info{custom_name_title}")
writer.table(
[self.target_info_row(t) for t in self.target_units],
headers=["Description", "ALIC", "Location"],
)
writer.write(path)
def target_info_row(self, unit: Unit) -> List[str]:
ll = self.theater.point_to_ll(unit.position)
unit_type = unit_type_from_name(unit.type)
name = unit.name if unit_type is None else unit_type.name
return [name, self.alic_for(unit), ll.format_dms(include_decimal_seconds=True)]
class StrikeTaskPage(KneeboardPage):
"""A kneeboard page containing strike target information."""
def __init__(
self,
flight: FlightData,
dark_kneeboard: bool,
theater: ConflictTheater,
) -> None:
self.flight = flight
self.dark_kneeboard = dark_kneeboard
self.theater = theater
@property
def targets(self) -> Iterator[NumberedWaypoint]:
for idx, waypoint in enumerate(self.flight.waypoints):
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
yield NumberedWaypoint(idx, waypoint)
def write(self, path: Path) -> None:
writer = KneeboardPageWriter(dark_theme=self.dark_kneeboard)
if self.flight.custom_name is not None:
custom_name_title = ' ("{}")'.format(self.flight.custom_name)
else:
custom_name_title = ""
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"],
)
writer.write(path)
def target_info_row(self, target: NumberedWaypoint) -> List[str]:
ll = self.theater.point_to_ll(target.waypoint.position)
return [
str(target.number),
target.waypoint.pretty_name,
ll.format_dms(include_decimal_seconds=True),
]
class KneeboardGenerator(MissionInfoGenerator): class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission.""" """Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission, game: "Game") -> None: def __init__(self, mission: Mission, game: "Game") -> None:
super().__init__(mission, game) super().__init__(mission, game)
self.dark_kneeboard = self.game.settings.generate_dark_kneeboard and (
self.mission.start_time.hour > 19 or self.mission.start_time.hour < 7
)
def generate(self) -> None: def generate(self) -> None:
"""Generates a kneeboard per client flight.""" """Generates a kneeboard per client flight."""
@@ -592,35 +408,15 @@ class KneeboardGenerator(MissionInfoGenerator):
) )
return all_flights return all_flights
def generate_task_page(self, flight: FlightData) -> Optional[KneeboardPage]:
if flight.flight_type in (FlightType.DEAD, FlightType.SEAD):
return SeadTaskPage(flight, self.dark_kneeboard, self.game.theater)
elif flight.flight_type is FlightType.STRIKE:
return StrikeTaskPage(flight, self.dark_kneeboard, self.game.theater)
return None
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]: def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
"""Returns a list of kneeboard pages for the given flight.""" """Returns a list of kneeboard pages for the given flight."""
pages: List[KneeboardPage] = [ return [
BriefingPage( BriefingPage(
flight,
self.game.bullseye_for(flight.friendly),
self.game.theater,
self.mission.start_time,
self.dark_kneeboard,
),
SupportPage(
flight, flight,
self.comms, self.comms,
self.awacs, self.awacs,
self.tankers, self.tankers,
self.jtacs, self.jtacs,
self.mission.start_time, self.mission.start_time,
self.dark_kneeboard,
), ),
] ]
if (target_page := self.generate_task_page(flight)) is not None:
pages.append(target_page)
return pages

View File

@@ -9,7 +9,7 @@ from gen.locations.preset_locations import PresetLocation
class PresetControlPointLocations: class PresetControlPointLocations:
"""A repository of preset locations for a given control point""" """A repository of preset locations for a given control point"""
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7_Amphibious) # List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
ashore_locations: List[PresetLocation] = field(default_factory=list) ashore_locations: List[PresetLocation] = field(default_factory=list)
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry) # List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)

View File

@@ -0,0 +1,86 @@
from pathlib import Path
from typing import List
from dcs import Mission, ships
from dcs.vehicles import MissilesSS
from gen.locations.preset_control_point_locations import PresetControlPointLocations
from gen.locations.preset_locations import PresetLocation
class MizDataLocationFinder:
@staticmethod
def compute_possible_locations(
terrain_name: str, cp_name: str
) -> PresetControlPointLocations:
"""
Extract the list of preset locations from miz data
:param terrain_name: Terrain/Map name
:param cp_name: Control Point / Airbase name
:return:
"""
miz_file = Path("./resources/mizdata/", terrain_name.lower(), cp_name + ".miz")
offshore_locations: List[PresetLocation] = []
ashore_locations: List[PresetLocation] = []
powerplants_locations: List[PresetLocation] = []
antiship_locations: List[PresetLocation] = []
if miz_file.exists():
m = Mission()
m.load_file(miz_file.absolute())
for vehicle_group in m.country("USA").vehicle_group:
if len(vehicle_group.units) > 0:
ashore_locations.append(
PresetLocation(
vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name,
)
)
for ship_group in m.country("USA").ship_group:
if (
len(ship_group.units) > 0
and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id
):
offshore_locations.append(
PresetLocation(
ship_group.position,
ship_group.units[0].heading,
ship_group.name,
)
)
for static_group in m.country("USA").static_group:
if len(static_group.units) > 0:
powerplants_locations.append(
PresetLocation(
static_group.position,
static_group.units[0].heading,
static_group.name,
)
)
if m.country("Iran") is not None:
for vehicle_group in m.country("Iran").vehicle_group:
if (
len(vehicle_group.units) > 0
and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id
):
antiship_locations.append(
PresetLocation(
vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name,
)
)
return PresetControlPointLocations(
ashore_locations,
offshore_locations,
antiship_locations,
powerplants_locations,
)

View File

@@ -9,7 +9,7 @@ MISSILES_MAP = {"V1GroupGenerator": V1GroupGenerator, "ScudGenerator": ScudGener
def generate_missile_group(game, ground_object, faction_name: str): def generate_missile_group(game, ground_object, faction_name: str):
""" """
This generate a missiles group This generate a ship group
:return: Nothing, but put the group reference inside the ground object :return: Nothing, but put the group reference inside the ground object
""" """
faction = db.FACTIONS[faction_name] faction = db.FACTIONS[faction_name]

View File

@@ -14,21 +14,21 @@ class ScudGenerator(GroupGenerator):
# Scuds # Scuds
self.add_unit( self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B, MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
"V1#0", "V1#0",
self.position.x, self.position.x,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B, MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
"V1#1", "V1#1",
self.position.x + 50, self.position.x + 50,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_SS_1C_Scud_B, MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M,
"V1#2", "V1#2",
self.position.x + 100, self.position.x + 100,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
@@ -37,7 +37,7 @@ class ScudGenerator(GroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.LUV_UAZ_469_Jeep, Unarmed.Transport_UAZ_469,
"Kubel#0", "Kubel#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -46,7 +46,7 @@ class ScudGenerator(GroupGenerator):
# Shorad # Shorad
self.add_unit( self.add_unit(
AirDefence.SPAAA_ZSU_23_4_Shilka_Gun_Dish, AirDefence.SPAAA_ZSU_23_4_Shilka,
"SHILKA#0", "SHILKA#0",
self.position.x - 55, self.position.x - 55,
self.position.y - 38, self.position.y - 38,
@@ -54,7 +54,7 @@ class ScudGenerator(GroupGenerator):
) )
self.add_unit( self.add_unit(
AirDefence.SAM_SA_9_Strela_1_Gaskin_TEL, AirDefence.SAM_SA_9_Strela_1_9P31,
"STRELA#0", "STRELA#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -14,21 +14,21 @@ class V1GroupGenerator(GroupGenerator):
# Ramps # Ramps
self.add_unit( self.add_unit(
MissilesSS.SSM_V_1_Launcher, MissilesSS.V_1_ramp,
"V1#0", "V1#0",
self.position.x, self.position.x,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_V_1_Launcher, MissilesSS.V_1_ramp,
"V1#1", "V1#1",
self.position.x + 50, self.position.x + 50,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
MissilesSS.SSM_V_1_Launcher, MissilesSS.V_1_ramp,
"V1#2", "V1#2",
self.position.x + 100, self.position.x + 100,
self.position.y + random.randint(1, 8), self.position.y + random.randint(1, 8),
@@ -37,7 +37,7 @@ class V1GroupGenerator(GroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.LUV_Kubelwagen_82, Unarmed.belwagen_82,
"Kubel#0", "Kubel#0",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -46,7 +46,7 @@ class V1GroupGenerator(GroupGenerator):
# Self defense flak # Self defense flak
flak_unit = random.choice( flak_unit = random.choice(
[AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_38_20mm] [AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38]
) )
self.add_unit( self.add_unit(
@@ -58,7 +58,7 @@ class V1GroupGenerator(GroupGenerator):
) )
self.add_unit( self.add_unit(
Unarmed.Truck_Opel_Blitz, Unarmed.Blitz_3_6_6700A,
"Blitz#0", "Blitz#0",
self.position.x + 200, self.position.x + 200,
self.position.y + 15, self.position.y + 15,

View File

@@ -39,7 +39,7 @@ ALPHA_MILITARY = [
"Zero", "Zero",
] ]
ANIMALS: tuple[str, ...] = ( ANIMALS = [
"SHARK", "SHARK",
"TORTOISE", "TORTOISE",
"BAT", "BAT",
@@ -61,7 +61,7 @@ ANIMALS: tuple[str, ...] = (
"MAMBA", "MAMBA",
"DOLPHIN", "DOLPHIN",
"PHEASANT", "PHEASANT",
"ARMADILLO", "ARMADILLLO",
"RACOON", "RACOON",
"ZEBRA", "ZEBRA",
"COW", "COW",
@@ -243,26 +243,22 @@ ANIMALS: tuple[str, ...] = (
"CANARY", "CANARY",
"WOODCHUCK", "WOODCHUCK",
"ANACONDA", "ANACONDA",
) ]
class NameGenerator: class NameGenerator:
number = 0 number = 0
infantry_number = 0 infantry_number = 0
aircraft_number = 0 aircraft_number = 0
convoy_number = 0
cargo_ship_number = 0
animals: list[str] = list(ANIMALS) ANIMALS = ANIMALS
existing_alphas: List[str] = [] existing_alphas: List[str] = []
@classmethod @classmethod
def reset(cls): def reset(cls):
cls.number = 0 cls.number = 0
cls.infantry_number = 0 cls.infantry_number = 0
cls.convoy_number = 0 cls.ANIMALS = ANIMALS
cls.cargo_ship_number = 0
cls.animals = list(ANIMALS)
cls.existing_alphas = [] cls.existing_alphas = []
@classmethod @classmethod
@@ -270,8 +266,6 @@ class NameGenerator:
cls.number = 0 cls.number = 0
cls.infantry_number = 0 cls.infantry_number = 0
cls.aircraft_number = 0 cls.aircraft_number = 0
cls.convoy_number = 0
cls.cargo_ship_number = 0
@classmethod @classmethod
def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight): def next_aircraft_name(cls, country: Country, parent_base_id: int, flight: Flight):
@@ -312,6 +306,10 @@ class NameGenerator:
db.unit_type_name(unit_type), db.unit_type_name(unit_type),
) )
@staticmethod
def next_basedefense_name():
return "basedefense_aa|0|0|"
@classmethod @classmethod
def next_awacs_name(cls, country: Country): def next_awacs_name(cls, country: Country):
cls.number += 1 cls.number += 1
@@ -329,37 +327,32 @@ class NameGenerator:
cls.number += 1 cls.number += 1
return "carrier|{}|{}|0|".format(country.id, cls.number) return "carrier|{}|{}|0|".format(country.id, cls.number)
@classmethod
def next_convoy_name(cls) -> str:
cls.convoy_number += 1
return f"Convoy {cls.convoy_number:03}"
@classmethod
def next_cargo_ship_name(cls) -> str:
cls.cargo_ship_number += 1
return f"Cargo Ship {cls.cargo_ship_number:03}"
@classmethod @classmethod
def random_objective_name(cls): def random_objective_name(cls):
if cls.animals: if len(cls.ANIMALS) == 0:
animal = random.choice(cls.animals) for i in range(10):
cls.animals.remove(animal) new_name_generated = True
alpha_mil_name = (
random.choice(ALPHA_MILITARY).upper()
+ "#"
+ str(random.randint(0, 100))
)
for existing_name in cls.existing_alphas:
if existing_name == alpha_mil_name:
new_name_generated = False
if new_name_generated:
cls.existing_alphas.append(alpha_mil_name)
return alpha_mil_name
# At this point, give up trying - something has gone wrong and we haven't been able to make a new name in 10 tries.
# We'll just make a longer name using the current unix epoch in nanoseconds. That should be unique... right?
last_chance_name = alpha_mil_name + str(time.time_ns())
cls.existing_alphas.append(last_chance_name)
return last_chance_name
else:
animal = random.choice(cls.ANIMALS)
cls.ANIMALS.remove(animal)
return animal return animal
for _ in range(10):
alpha = random.choice(ALPHA_MILITARY).upper()
number = random.randint(0, 100)
alpha_mil_name = f"{alpha} #{number:02}"
if alpha_mil_name not in cls.existing_alphas:
cls.existing_alphas.append(alpha_mil_name)
return alpha_mil_name
# At this point, give up trying - something has gone wrong and we haven't been
# able to make a new name in 10 tries. We'll just make a longer name using the
# current unix epoch in nanoseconds. That should be unique... right?
last_chance_name = alpha_mil_name + str(time.time_ns())
cls.existing_alphas.append(last_chance_name)
return last_chance_name
namegen = NameGenerator namegen = NameGenerator

View File

@@ -134,7 +134,6 @@ RADIOS: List[Radio] = [
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)), Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)), Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)), Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
Radio("R&S Series 6000", MHz(100), MHz(156), step=kHz(25)),
] ]
@@ -207,7 +206,7 @@ class RadioRegistry:
except StopIteration: except StopIteration:
# In the event of too many channel users, fail gracefully by reusing # In the event of too many channel users, fail gracefully by reusing
# the last channel. # the last channel.
# https://github.com/dcs-liberation/dcs_liberation/issues/598 # https://github.com/Khopa/dcs_liberation/issues/598
channel = radio.last_channel channel = radio.last_channel
logging.warning( logging.warning(
f"No more free channels for {radio.name}. Reusing {channel}." f"No more free channels for {radio.name}. Reusing {channel}."

View File

@@ -36,4 +36,4 @@ class BoforsGenerator(AirDefenseGroupGenerator):
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:
return AirDefenseRange.AAA return AirDefenseRange.Short

View File

@@ -8,12 +8,12 @@ from gen.sam.airdefensegroupgenerator import (
) )
GFLAK = [ GFLAK = [
AirDefence.AAA_Flak_Vierling_38_Quad_20mm, AirDefence.AAA_Flak_Vierling_38,
AirDefence.AAA_8_8cm_Flak_18, AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_8_8cm_Flak_36, AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37, AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_8_8cm_Flak_41, AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Flak_38_20mm, AirDefence.AAA_Flak_38,
] ]
@@ -53,7 +53,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
search_pos = self.get_circular_position(random.randint(2, 3), 80) search_pos = self.get_circular_position(random.randint(2, 3), 80)
for index, pos in enumerate(search_pos): for index, pos in enumerate(search_pos):
self.add_unit( self.add_unit(
AirDefence.SL_Flakscheinwerfer_37, AirDefence.Flak_Searchlight_37,
"SearchLight#" + str(index), "SearchLight#" + str(index),
pos[0], pos[0],
pos[1], pos[1],
@@ -62,14 +62,14 @@ class FlakGenerator(AirDefenseGroupGenerator):
# Support # Support
self.add_unit( self.add_unit(
AirDefence.PU_Maschinensatz_33, AirDefence.Maschinensatz_33,
"MC33#", "MC33#",
self.position.x - 20, self.position.x - 20,
self.position.y - 20, self.position.y - 20,
self.heading, self.heading,
) )
self.add_unit( self.add_unit(
AirDefence.AAA_SP_Kdo_G_40, AirDefence.AAA_Kdo_G_40,
"KDO#", "KDO#",
self.position.x - 25, self.position.x - 25,
self.position.y - 20, self.position.y - 20,
@@ -78,7 +78,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
# Commander # Commander
self.add_unit( self.add_unit(
Unarmed.LUV_Kubelwagen_82, Unarmed.belwagen_82,
"Kubel#", "Kubel#",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -89,7 +89,7 @@ class FlakGenerator(AirDefenseGroupGenerator):
for i in range(int(max(1, grid_x / 2))): for i in range(int(max(1, grid_x / 2))):
for j in range(int(max(1, grid_x / 2))): for j in range(int(max(1, grid_x / 2))):
self.add_unit( self.add_unit(
Unarmed.Truck_Opel_Blitz, Unarmed.Blitz_3_6_6700A,
"BLITZ#" + str(index), "BLITZ#" + str(index),
self.position.x + 125 + 15 * i + random.randint(1, 5), self.position.x + 125 + 15 * i + random.randint(1, 5),
self.position.y + 15 * j + random.randint(1, 5), self.position.y + 15 * j + random.randint(1, 5),
@@ -98,4 +98,4 @@ class FlakGenerator(AirDefenseGroupGenerator):
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:
return AirDefenseRange.AAA return AirDefenseRange.Short

View File

@@ -34,7 +34,7 @@ class Flak18Generator(AirDefenseGroupGenerator):
# Add a commander truck # Add a commander truck
self.add_unit( self.add_unit(
Unarmed.Truck_Opel_Blitz, Unarmed.Blitz_3_6_6700A,
"Blitz#", "Blitz#",
self.position.x - 35, self.position.x - 35,
self.position.y - 20, self.position.y - 20,
@@ -43,4 +43,4 @@ class Flak18Generator(AirDefenseGroupGenerator):
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:
return AirDefenseRange.AAA return AirDefenseRange.Short

View File

@@ -41,4 +41,4 @@ class KS19Generator(AirDefenseGroupGenerator):
@classmethod @classmethod
def range(cls) -> AirDefenseRange: def range(cls) -> AirDefenseRange:
return AirDefenseRange.AAA return AirDefenseRange.Short

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