Compare commits

..

52 Commits

Author SHA1 Message Date
RndName
440fe2e019 Only check for ground units in capture trigger zone
requires pydcs update: https://github.com/pydcs/dcs/pull/279

(cherry picked from commit f9903f1e19)
2022-12-21 13:26:42 -08:00
RndName
c82bc1c211 Change iads command unit type
(cherry picked from commit 4a4935f165)
2022-12-21 13:26:42 -08:00
RndName
f855d2956c Only add skynet iads command unit when advanced iads is in use
(cherry picked from commit 09f92cc5e4)
2022-12-21 13:26:42 -08:00
Dan Albert
24aebf5ff6 Update pydcs.
Includes UnitType parameter of AllOfCoalition in/out zone condition.

(cherry picked from commit 887e5997c2)
2022-12-21 13:26:42 -08:00
Dan Albert
8004dccdb2 Update changelog for Blackshark 3.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2657.

(cherry picked from commit f88a50dd07)
2022-12-21 12:43:05 -08:00
Dan Albert
7e7cb4f0bc Add icons and banners for BS3.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
(cherry picked from commit 3f12a5ae3d)
2022-12-21 12:43:05 -08:00
Dan Albert
801e3afb98 Add BS3 loadouts.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
(cherry picked from commit f2946817bf)
2022-12-21 12:43:05 -08:00
Dan Albert
48ea33611d Add BS3 to factions that have BS2.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
(cherry picked from commit 935a9b0631)
2022-12-21 12:43:05 -08:00
Dan Albert
7b588c5437 Add blackshark 3 yaml.
There are no significant notable changes from Blackshark 2, so this is
the same YAML.

https://github.com/dcs-liberation/dcs_liberation/issues/2657
(cherry picked from commit c0dc411102)
2022-12-21 12:43:05 -08:00
Dan Albert
e85b1a3195 Add Blackshark 3 to the mission planning DB.
https://github.com/dcs-liberation/dcs_liberation/issues/2657
(cherry picked from commit 55037626a4)
2022-12-21 12:43:05 -08:00
Dan Albert
8132b9a080 Fix type-only import that needs to be real.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2660.

(cherry picked from commit fc3e72bacf)
2022-12-21 12:43:05 -08:00
Dan Albert
8e0cf2284b Update changelog for pydcs update.
(cherry picked from commit 66523301aa)
2022-12-21 12:43:05 -08:00
Dan Albert
87405ba58a Expand gitattributes to cover common files.
Specifically adding this so that yaml changes stop being uploaded as
CRLF, but the rest is likely useful too.

(cherry picked from commit 54546aaefb)
2022-12-21 12:43:05 -08:00
Dan Albert
0952cf4c75 Update bug templates to 6.0.0.
We're not fixing 5.x bugs any more.

(cherry picked from commit ded5fc8b1d)
2022-12-21 12:43:05 -08:00
SnappyComebacks
5bf5b41f2d Add Recovery Tankers (#2661)
Add support for recovery tankers at aircraft carriers.

Cherry picked from 9a81121ac1 and
0fd0f0e7c0
2022-12-21 05:17:02 +00:00
Dan Albert
715c60583a Save the last turn for bug reports.
We often get save games uploaded with bug reports that are already in a
broken state with nothing we can do about it. This saves that turn to
`last_turn.liberation` so users are less likely to have clobbered the
useful data before filing the report.

(cherry picked from commit 22503d4e95)
2022-12-20 14:09:25 -08:00
Dan Albert
5dcd1e9360 Fix the channel's landmap.
Caused by a bad rename when I did the migration. The landmap in 6.0.0
was actually a GIF...

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

(cherry picked from commit de9236e93a)
2022-12-20 13:06:15 -08:00
SnappyComebacks
ef7978e887 Update F-15E loadouts.
(cherry picked from commit a245ba80c3)
2022-12-14 22:00:06 -07:00
Dan Albert
8cfb1790b2 Add new British navy units to the UK faction.
I haven't removed the old US navy stuff from this faction, since all the
new UK ships are frigates, and removing the US stuff would deprive the
faction of cruisers and destroyers, which might break generation (I'm
not familiar enough with the new system to say for sure).

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

(cherry picked from commit 9a1860fc5e)
2022-12-13 00:14:48 -08:00
Dan Albert
41203b9bed Add HMS Invincible as a valid runway.
(cherry picked from commit f3f5ab70ea)
2022-12-13 00:14:48 -08:00
ColonelAkirNakesh
50fb1f5d97 Add HMS Invincible.
(cherry picked from commit 7f916d55e7)
2022-12-13 00:14:48 -08:00
Dan Albert
e041fa3c70 Fix changelog formatting.
(cherry picked from commit 6ce7638fdc)
2022-12-12 23:43:09 -08:00
Dan Albert
f358c1c0b2 Add missing changelog note.
(cherry picked from commit 43ea019091)
2022-12-12 21:33:12 -08:00
ColonelAkirNakesh
a18dbe0a72 Add support for Leander class HMS Andromeda.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
(cherry picked from commit 7673ca5481)
2022-12-12 19:30:26 -08:00
ColonelAkirNakesh
00e7a1fe55 Add support for Leander class HMS Ariadne.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
(cherry picked from commit 774a37a7d2)
2022-12-12 19:30:26 -08:00
ColonelAkirNakesh
b664a8cac5 Add support for Castle Class.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
(cherry picked from commit 905094f63f)
2022-12-12 19:30:26 -08:00
ColonelAkirNakesh
2c4678aa48 Add support for Leander class HMS Achilles.
https://github.com/dcs-liberation/dcs_liberation/issues/2571
(cherry picked from commit fd5b7ba49d)
2022-12-12 19:30:26 -08:00
Dan Albert
eafc70a5e5 Fix livery for VF-33 F-14A squadron.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2610.

(cherry picked from commit 6025cad716)
2022-12-12 19:30:26 -08:00
Dan Albert
7fb1d25887 Add changelog notes that were missed in PRs.
(cherry picked from commit 28859a8a9c)
2022-12-12 19:30:26 -08:00
Dan Albert
d9491c47b8 Fix encoding issues with the Peru faction.
Not really sure what's going on here, but presumably it's UTF-16 and
UTF-8 fighting.

(cherry picked from commit 9365aea724)
2022-12-12 19:30:26 -08:00
DillieKoe
2c26f366a8 Add Peru faction.
A new Peru faction with a mirage squadon

(cherry picked from commit 304fd7ea80)
2022-12-12 19:30:26 -08:00
ColonelAkirNakesh
c30636b4d2 Override liveries for Russian aircraft in bluefor faction.
(cherry picked from commit 5e345263a7)
2022-12-12 18:47:44 -08:00
dependabot[bot]
a3a91c5771 Bump certifi from 2022.6.15 to 2022.12.7
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.6.15 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2022.06.15...2022.12.07)

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

Signed-off-by: dependabot[bot] <support@github.com>
(cherry picked from commit 445ee25bbf)
2022-12-12 17:50:19 -08:00
Dan Albert
3d64619481 Fix CAS not having landing waypoints.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2611.

(cherry picked from commit 20937815f8)
2022-12-03 17:01:26 -08:00
RndName
3b97f7a2ba Fix manual debrief submit not ending mission
cherry-pick from 3863b8ef40
2022-11-30 13:29:40 +01:00
SnappyComebacks
6779ee96be Add F-15E to DEAD_CAPABLE in AI flight planner.
(cherry picked from commit 1b828b95b3)
2022-11-27 22:16:30 -07:00
Dan Albert
d9134650dc Branch for 6.1. 2022-11-27 21:07:47 -08:00
Dan Albert
4ef2cc26c8 Remove incompatible campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2558.
2022-11-25 15:29:49 -08:00
Starfire13
a03cfdf305 Updates Khopa's Normandy and Channel campaign to 10.0
Note that Operation Dynamo only requires a yaml file update. The .miz file is fine and is not included here.
2022-11-25 15:29:49 -08:00
Dan Albert
9777e5e432 Remove dead code. 2022-11-25 15:29:49 -08:00
Dan Albert
5f74fd81eb Unfilter the custom waypoint targets.
There doesn't appear to be any reason for us to be poking at
implementation details here aside from changing the name from "unit" to
"building" for that case. Just iterate over the known strike targets.

Making this change uncovered some latent type errors.

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

(cherry picked from commit 5e7e5e2636)
2022-11-25 14:22:11 -08:00
Dan Albert
b57e30a13c Add radios for the MB-339A.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2511.

(cherry picked from commit e208df16b2)
2022-11-25 14:15:23 -08:00
Dan Albert
789c863922 Update RoleplayingPleb's campaigns.
https://github.com/dcs-liberation/dcs_liberation/issues/2558
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2561.

(cherry picked from commit 11632b0ef1)
2022-11-25 13:15:50 -08:00
Dan Albert
3cbd4c7dd4 Make the casualty report scrollable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2567.

(cherry picked from commit b0bc46f539)
2022-11-25 13:15:50 -08:00
SnappyComebacks
9d0c75d199 Add banner for MB-339A.
(cherry picked from commit 627ed45065)
2022-11-25 12:14:52 -07:00
RndName
8e63aa4d72 Remove dcs capture event from state json
With the latest change we added capture zones and corresponding trigger rules for all Airfields as well so we do not need to rely on the dcs capture event S_EVENT_BASE_CAPTURED anymore.

cherry-pick from fc9ad5b519
2022-11-24 11:29:16 +01:00
RndName
06a4dce555 Add Airfield to list of capture zone types
This will create capture zones and the trigger rules to check for a base capture. Will fix an issue where the dcs capture event is not fired and therefore the capture not recognized by liberation

cherry-pick from 40ddad1d9a
2022-11-24 11:29:16 +01:00
RndName
65a232a0c7 Fix carrier group generation
cherry-pick from eb997db703
2022-11-24 11:29:16 +01:00
MetalStormGhost
bde22b52ea Attempt at fixing Carrier killed in state.json but not being removed from game, issue #2405. GenericCarrierGenerator.generate() will now generate the ship group with an array that only contains alive ship units, just like GroundObjectGenerator.generate() has previously done.
Carrier groups will now also show up as destroyed/damaged on the map when the carrier is sunk.

cherry-pick from e53dc5b80b
2022-11-24 11:29:16 +01:00
RndName
bfed69573f Validate primary and secondary nodes for iads network
cherry-pick from ab64655f05
2022-11-21 12:23:40 +01:00
RndName
9ba717fd82 Fix IADS network error caused by dead groups
Fixed an error which would occur when dead units which are non static would be added as secondary node during the skynet lua data generation. This should in general not be possible as connection nodes and power sources are currently most of the time static.

cherry-pick from e1b530e4fc
2022-11-21 12:23:40 +01:00
SnappyComebacks
f2e8a77862 Update pydcs.
Added ice halo generation.

(cherry picked from commit 4414853e45)
2022-11-20 18:03:58 -07:00
935 changed files with 9801 additions and 18381 deletions

View File

@@ -1,8 +0,0 @@
[report]
exclude_lines =
pragma: no cover
if TYPE_CHECKING:
[run]
branch = True
source = game,pydcs_extensions,qt_ui,resources/tools

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 7.1.0
- 6.0.0
- Development build
- type: textarea
attributes:
@@ -53,7 +53,7 @@ body:
description: >
Attach any files needed to reproduce the bug here. **A save game is
required.** We typically cannot help without a save game (the
`.liberation.zip` file found in `%USERPROFILE%/Saved
`.liberation` file found in `%USERPROFILE%/Saved
Games/DCS/Liberation/Saves`), so most bugs filed without saved games
will be closed without investigation.
@@ -73,7 +73,8 @@ body:
The `state.json` file for the most recently completed turn, located at
`<Liberation install directory>/state.json`. This file is essential for
investigating any issues with end-of-turn results processing.
investigating any issues with end-of-turn results processing. **If you
include this file, also include `last_turn.liberation`.**
You can attach files to the bug by dragging and dropping the file into

View File

@@ -39,7 +39,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 7.1.0
- 6.0.0
- Development build
- type: textarea
attributes:

View File

@@ -6,7 +6,7 @@ runs:
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: "3.11"
python-version: "3.10"
cache: pip
- name: Install environment
@@ -19,3 +19,5 @@ runs:
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force

View File

@@ -11,7 +11,6 @@ jobs:
- uses: actions/setup-python@v2
- uses: psf/black@stable
with:
version: ~=22.12
src: "."
options: "--check"

View File

@@ -15,24 +15,4 @@ jobs:
- name: run tests
run: |
./venv/scripts/activate
pytest --cov --cov-report=xml tests
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
ts-tests:
name: Typescript tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Set up JS environment
uses: ./.github/actions/setup-liberation-js
- name: run tests
run: |
cd client
npm test -- --coverage
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
pytest tests

4
.gitignore vendored
View File

@@ -1,15 +1,11 @@
*.pyc
__pycache__
build/**
# Sphinx
docs/_build
resources/payloads/*.lua
venv
.DS_Store
.vscode/settings.json
dist/**
/.coverage
/coverage.xml
# User-specific stuff
.idea/
.env

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/psf/black
rev: 22.12.0
rev: 22.6.0
hooks:
- id: black
language_version: python3

View File

@@ -1,13 +0,0 @@
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: docs/conf.py
python:
install:
- requirements: docs/requirements.txt

View File

@@ -8,19 +8,13 @@
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![codecov](https://codecov.io/gh/dcs-liberation/dcs_liberation/branch/develop/graph/badge.svg?token=EEQ7G76K2L)](https://codecov.io/gh/dcs-liberation/dcs_liberation)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/dcs-liberation/dcs_liberation?style=social)
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
**Note that DCS Liberation does not support the stable release of DCS. We can
only guarantee compatibility with either the open beta or the stable release,
and more people play the open beta. DCS stable _might_ work sometimes, but it's
untested, and we will be unable to fix any bugs unique to stable DCS.**
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Screenshot](https://user-images.githubusercontent.com/315852/120939254-0b4a9f80-c6cc-11eb-82f5-ce3f8d714bfe.png)
@@ -35,6 +29,7 @@ To download preview builds of the next version of DCS Liberation, see https://gi
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
_don't_ spam them with comments):
* [Hold points do not work in DCS 2.8](https://forum.dcs.world/topic/311458-humvee-ground-unit-holdstop-conditiontime-more-bug-28-mission-editor/)
* [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/)

View File

@@ -1,102 +1,3 @@
# 8.0.0
Saves from 7.x are not compatible with 8.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map.
* **[UI]** Limited size of overfull airbase display and added scrollbar.
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
* **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays.
* **[UI]** Added basic game over dialog.
## Fixes
* **[Campaign]** Fix bug introduced in 7.0 where map strike target deaths are no longer tracked.
* **[Mission Generation]** Fix crash during mission generation caused by out of date DCS data for the Gazelle.
* **[Mission Generation]** Fix crash during mission generation when DCS beacon data is inconsistent.
# 7.1.0
Saves from 7.0.0 are compatible with 7.1.0
## Features/Improvements
* **[Engine]** Support for Normandy 2 airfields.
* **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer.
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
* **[Mission Generation]** Added settings for controlling number of tactical commander, observer, JTAC, and game master slots.
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
* **[New Game Wizard]** The air wing configuration dialog will check for and reject overfull airbases before continuing when the new squadron rules are used.
* **[New Game Wizard]** Closing the air wing configuration dialog will now cancel and return to the new game wizard rather than reverting changes and continuing.
* **[New Game Wizard]** A warning will be displayed next to the new squadron rules button if the campaign predates the new rules and will likely require user intervention before continuing.
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
## Fixes
* **[Mission Planning]** BAI is once again plannable against missile sites and coastal defense batteries.
* **[UI]** Fixed formatting of departure time in flight details dialog.
# 7.0.0
Saves from 6.x are not compatible with 7.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.5.40170.
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
* **[Campaign]** Added options to limit squadron sizes and to begin all squadrons at maximum strength. Maximum squadron size is defined during air wing configuration with default values provided by the campaign.
* **[Campaign]** Added handling for more DCS death events. This probably does not catch any deaths that weren't previously tracked, but it should record them sooner, which will improve results for game crashes or other early exits.
* **[Campaign AI]** The campaign AI now prefers fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable.
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
* **[Mission Generation]** The A-10C II now uses separate radios for inter- and intra-flight comms (similar to other modern aircraft).
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
* **[Modding]** Add support for VSN F-4B and F-4C mod.
* **[Modding]** Added support for AI C-47 mod.
* **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion.
* **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example.
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
* **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference.
* **[Modding]** Wind speed generation inputs are now moddable. See https://dcs-liberation.rtfd.io/en/latest/modding/weather.html.
* **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent.
* **[New Game Wizard]** Lua plugins can now be set while creating a new game.
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
* **[New Game Wizard]** Squadron liveries can now be selected during air wing configuration.
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
* **[UI]** The orientation of objects like SAMs, EWRs, garrisons, and ships can now be manually adjusted.
## Fixes
* **[Campaign]** Fixed a longstanding bug where oversized airlifts could corrupt a save with empty convoys.
* **[Campaign]** Aircraft with built-in TGPs but without an external pod will no longer degrade automatic loadouts to iron bombs.
* **[Engine]** Fixed crash in startup caused by a corrupted Liberation preferences file.
* **[Flight Planning]** AEW&C missions are now plannable over FOBs and LHAs.
* **[Flight Planning]** BAI is no longer plannable against buildings.
* **[Modding]** Fixed an issue where Falklands campaigns created or edited with new versions of DCS could not be loaded.
* **[Modding]** Fixed decoding of campaign yaml files to use UTF-8 rather than the system locale's default. It's now possible to use "Bf 109 K-4 Kurfürst" as a preferred aircraft type.
* **[Mission Generation]** Planes will no longer spawn in helipads that are not also designated for fixed wing parking.
* **[Mission Generation]** Potentially an issue where ground war planning game state could become corrupted, preventing mission generation.
* **[Mission Generation]** Refueling tasks will now only be created for flights that have a tanker in their package.
* **[Mission Generation]** Fixed missing Tanker task on recovery tanker missions.
* **[UI]** Fixed error when resetting air wing configuration during game setup.
* **[UI]** Fixed flight plan recreation when changing mission type with "Recreate as" flight options.
* **[UI]** Fixed failure to launch UI when Liberation persistent preferences file was corrupt.
# 6.1.1
## Fixes
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
* **[Mission Generation]** Prevent helicopters from spawning into collisions at FARPs when more than one flight uses the same FARP.
# 6.1.0
Saves from 6.0.0 are compatible with 6.1.0

149
client/package-lock.json generated
View File

@@ -41,7 +41,6 @@
"electron": "^21.1.0",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0",
"license-checker": "^25.0.1",
"react-scripts": "5.0.1",
"ts-node": "^10.9.1",
@@ -3915,9 +3914,9 @@
}
},
"node_modules/@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
"dev": true
},
"node_modules/@sideway/pinpoint": {
@@ -5624,9 +5623,9 @@
}
},
"node_modules/acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true,
"bin": {
"acorn": "bin/acorn"
@@ -6205,9 +6204,9 @@
}
},
"node_modules/babel-loader/node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -8413,9 +8412,9 @@
}
},
"node_modules/enhanced-resolve": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.1.tgz",
"integrity": "sha512-jdyZMwCQ5Oj4c5+BTnkxPgDZO/BJzh/ADDmKebayyzNwjVX1AFCeGkOfxNx0mHi2+8BKC5VxUYiw3TIvoT7vhw==",
"dev": true,
"dependencies": {
"graceful-fs": "^4.2.4",
@@ -10671,9 +10670,9 @@
}
},
"node_modules/http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"dev": true
},
"node_modules/http-deceiver": {
@@ -10817,7 +10816,7 @@
"node_modules/identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
"integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=",
"dev": true,
"dependencies": {
"harmony-reflect": "^1.4.6"
@@ -13482,6 +13481,12 @@
"integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true
},
"node_modules/json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -13514,10 +13519,13 @@
"optional": true
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dev": true,
"dependencies": {
"minimist": "^1.2.5"
},
"bin": {
"json5": "lib/cli.js"
},
@@ -19393,9 +19401,9 @@
}
},
"node_modules/tsconfig-paths/node_modules/json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"dependencies": {
"minimist": "^1.2.0"
@@ -19848,9 +19856,9 @@
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"dev": true,
"dependencies": {
"glob-to-regexp": "^0.4.1",
@@ -19888,9 +19896,9 @@
}
},
"node_modules/webpack": {
"version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"version": "5.69.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz",
"integrity": "sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A==",
"dev": true,
"dependencies": {
"@types/eslint-scope": "^3.7.3",
@@ -19898,24 +19906,24 @@
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.7.1",
"acorn": "^8.4.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.10.0",
"enhanced-resolve": "^5.8.3",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.9",
"json-parse-even-better-errors": "^2.3.1",
"json-parse-better-errors": "^1.0.2",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.4.0",
"watchpack": "^2.3.1",
"webpack-sources": "^3.2.3"
},
"bin": {
@@ -23670,9 +23678,9 @@
}
},
"@sideway/formula": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
"dev": true
},
"@sideway/pinpoint": {
@@ -25012,9 +25020,9 @@
}
},
"acorn": {
"version": "8.8.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
"version": "8.7.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
"dev": true
},
"acorn-globals": {
@@ -25441,9 +25449,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -27127,9 +27135,9 @@
}
},
"enhanced-resolve": {
"version": "5.12.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.1.tgz",
"integrity": "sha512-jdyZMwCQ5Oj4c5+BTnkxPgDZO/BJzh/ADDmKebayyzNwjVX1AFCeGkOfxNx0mHi2+8BKC5VxUYiw3TIvoT7vhw==",
"dev": true,
"requires": {
"graceful-fs": "^4.2.4",
@@ -28827,9 +28835,9 @@
}
},
"http-cache-semantics": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
"integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==",
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz",
"integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==",
"dev": true
},
"http-deceiver": {
@@ -28939,7 +28947,7 @@
"identity-obj-proxy": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
"integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=",
"dev": true,
"requires": {
"harmony-reflect": "^1.4.6"
@@ -30884,6 +30892,12 @@
"integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==",
"dev": true
},
"json-parse-better-errors": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
"integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==",
"dev": true
},
"json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
@@ -30916,10 +30930,13 @@
"optional": true
},
"json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
"dev": true,
"requires": {
"minimist": "^1.2.5"
}
},
"jsonfile": {
"version": "6.1.0",
@@ -35280,9 +35297,9 @@
},
"dependencies": {
"json5": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
"dev": true,
"requires": {
"minimist": "^1.2.0"
@@ -35630,9 +35647,9 @@
}
},
"watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
"dev": true,
"requires": {
"glob-to-regexp": "^0.4.1",
@@ -35664,9 +35681,9 @@
"dev": true
},
"webpack": {
"version": "5.76.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
"version": "5.69.1",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz",
"integrity": "sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A==",
"dev": true,
"requires": {
"@types/eslint-scope": "^3.7.3",
@@ -35674,24 +35691,24 @@
"@webassemblyjs/ast": "1.11.1",
"@webassemblyjs/wasm-edit": "1.11.1",
"@webassemblyjs/wasm-parser": "1.11.1",
"acorn": "^8.7.1",
"acorn": "^8.4.1",
"acorn-import-assertions": "^1.7.6",
"browserslist": "^4.14.5",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.10.0",
"enhanced-resolve": "^5.8.3",
"es-module-lexer": "^0.9.0",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
"glob-to-regexp": "^0.4.1",
"graceful-fs": "^4.2.9",
"json-parse-even-better-errors": "^2.3.1",
"json-parse-better-errors": "^1.0.2",
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
"schema-utils": "^3.1.0",
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.1.3",
"watchpack": "^2.4.0",
"watchpack": "^2.3.1",
"webpack-sources": "^3.2.3"
},
"dependencies": {

View File

@@ -69,18 +69,9 @@
"electron": "^21.1.0",
"electron-is-dev": "^2.0.0",
"generate-license-file": "^2.0.0",
"identity-obj-proxy": "^3.0.0",
"license-checker": "^25.0.1",
"react-scripts": "5.0.1",
"ts-node": "^10.9.1",
"wait-on": "^6.0.1"
},
"jest": {
"transformIgnorePatterns": [
"node_modules/(?!(@?react-leaflet|axios)/)"
],
"moduleNameMapper": {
".+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "identity-obj-proxy"
}
}
}

View File

@@ -1,11 +1,11 @@
import App from "./App";
import { setupStore } from "./app/store";
import { store } from "./app/store";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
test("app renders", () => {
render(
<Provider store={setupStore()}>
<Provider store={store}>
<App />
</Provider>
);

View File

@@ -3,48 +3,36 @@ import combatReducer from "../api/combatSlice";
import controlPointsReducer from "../api/controlPointsSlice";
import flightsReducer from "../api/flightsSlice";
import frontLinesReducer from "../api/frontLinesSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import mapReducer from "../api/mapSlice";
import navMeshReducer from "../api/navMeshSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import threatZonesReducer from "../api/threatZonesSlice";
import unculledZonesReducer from "../api/unculledZonesSlice";
import {
Action,
PreloadedState,
ThunkAction,
combineReducers,
configureStore,
} from "@reduxjs/toolkit";
import unculledZonesReducer from "../api/unculledZonesSlice";
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
const rootReducer = combineReducers({
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
export const store = configureStore({
reducer: {
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
});
export function setupStore(preloadedState?: PreloadedState<RootState>) {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
preloadedState: preloadedState,
});
}
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore["dispatch"];
export type RootState = ReturnType<typeof rootReducer>;
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,

View File

@@ -1,53 +0,0 @@
import Aircraft from "./Aircraft";
import { render } from "@testing-library/react";
import { Icon } from "leaflet";
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
Marker: (props: any) => {
mockMarker(props);
},
}));
test("grounded aircraft do not render", async () => {
const { container } = render(
<Aircraft
flight={{
id: "",
blue: true,
position: undefined,
sidc: "",
waypoints: [],
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
test("in-flight aircraft render", async () => {
render(
<Aircraft
flight={{
id: "",
blue: true,
position: {
lat: 10,
lng: 20,
},
sidc: "foobar",
waypoints: [],
}}
/>
);
expect(mockMarker).toHaveBeenCalledWith(
expect.objectContaining({
position: {
lat: 10,
lng: 20,
},
icon: expect.any(Icon),
})
);
});

View File

@@ -1,53 +0,0 @@
import { renderWithProviders } from "../../testutils";
import AircraftLayer from "./AircraftLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
test("layer is empty by default", async () => {
renderWithProviders(<AircraftLayer />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
test("layer has aircraft if non-empty", async () => {
renderWithProviders(<AircraftLayer />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
bar: {
id: "bar",
blue: false,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
},
selected: null,
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).toHaveBeenCalledTimes(2);
});

View File

@@ -1,146 +0,0 @@
import { renderWithProviders } from "../../testutils";
import AirDefenseRangeLayer, { colorFor } from "./AirDefenseRangeLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockCircle = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("colorFor", () => {
it("has a unique color for each configuration", () => {
const params = [
[false, false],
[false, true],
[true, false],
[true, true],
];
var colors = new Set<string>();
for (const [blue, detection] of params) {
colors.add(colorFor(blue, detection));
}
expect(colors.size).toEqual(4);
});
});
describe("AirDefenseRangeLayer", () => {
it("draws nothing when there are no TGOs", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("does not draw wrong range types", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: false,
position: {
lat: 0,
lng: 0,
},
units: [],
threat_ranges: [],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("draws threat ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 10,
color: colorFor(true, false),
interactive: false,
})
);
});
it("draws detection ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} detection />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 20,
color: colorFor(true, true),
interactive: false,
})
);
});
});

View File

@@ -9,7 +9,7 @@ interface TgoRangeCirclesProps {
detection?: boolean;
}
export function colorFor(blue: boolean, detection: boolean) {
function colorFor(blue: boolean, detection: boolean) {
if (blue) {
return detection ? "#bb89ff" : "#0084ff";
}

View File

@@ -1,132 +0,0 @@
import { renderWithProviders } from "../../testutils";
import Combat from "./Combat";
import { LatLng } from "leaflet";
const mockPolyline = jest.fn();
const mockPolygon = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: any) => {
mockPolyline(props);
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
describe("Combat", () => {
describe("footprint", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [[new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)]],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders single polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ positions: boundary })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders multiple polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary, boundary],
}}
/>
);
expect(mockPolygon).toBeCalledTimes(2);
});
});
describe("lines", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
it("renders single line", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({
positions: [new LatLng(0, 0), new LatLng(0, 1)],
})
);
});
it("renders multiple lines", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1), new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledTimes(2);
});
});
it("renders nothing if no footprint or targets", () => {
const { container } = renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: null,
footprint: null,
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -1,48 +0,0 @@
import { renderWithProviders } from "../../testutils";
import CombatLayer from "./CombatLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
describe("CombatLayer", () => {
it("renders each combat", () => {
renderWithProviders(<CombatLayer />, {
preloadedState: {
combat: {
combat: {
foo: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
bar: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
},
},
},
});
expect(mockPolyline).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<CombatLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockPolyline).not.toHaveBeenCalled();
});
});

View File

@@ -1,52 +0,0 @@
import { renderWithProviders } from "../../testutils";
import ControlPointsLayer from "./ControlPointsLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockMarker = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
describe("ControlPointsLayer", () => {
it("renders each control point", () => {
renderWithProviders(<ControlPointsLayer />, {
preloadedState: {
controlPoints: {
controlPoints: {
foo: {
id: "foo",
name: "Foo",
blue: true,
position: new LatLng(0, 0),
mobile: false,
sidc: "",
},
bar: {
id: "bar",
name: "Bar",
blue: false,
position: new LatLng(1, 0),
mobile: false,
sidc: "",
},
},
},
},
});
expect(mockMarker).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<ControlPointsLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
});

View File

@@ -1,78 +0,0 @@
import { renderWithProviders } from "../../testutils";
import CullingExclusionZones from "./CullingExclusionZones";
import { PropsWithChildren } from "react";
const mockCircle = jest.fn();
const mockLayerGroup = jest.fn();
const mockLayerControlOverlay = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
LayersControl: {
Overlay: (props: PropsWithChildren<any>) => {
mockLayerControlOverlay(props);
return <>{props.children}</>;
},
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("CullingExclusionZones", () => {
it("is empty there are no exclusion zones", () => {
renderWithProviders(<CullingExclusionZones />);
expect(mockCircle).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockLayerControlOverlay).toHaveBeenCalledTimes(1);
});
describe("zone circles", () => {
it("are drawn in the correct locations", () => {
renderWithProviders(<CullingExclusionZones />, {
preloadedState: {
unculledZones: {
zones: [
{
position: {
lat: 0,
lng: 0,
},
radius: 10,
},
{
position: {
lat: 1,
lng: 1,
},
radius: 2,
},
],
},
},
});
expect(mockCircle).toHaveBeenCalledTimes(2);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 0,
lng: 0,
},
radius: 10,
})
);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 1,
lng: 1,
},
radius: 2,
})
);
});
it("are not interactive", () => {});
});
});

View File

@@ -30,10 +30,18 @@ const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => {
export default function CullingExclusionZones() {
const data = useAppSelector(selectUnculledZones).zones;
var cez = <></>;
if (!data) {
console.log("Empty response when loading culling exclusion zones");
} else {
cez = (
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
);
}
return (
<LayersControl.Overlay name="Culling exclusion zones">
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
{cez}
</LayersControl.Overlay>
);
}

View File

@@ -1,405 +0,0 @@
import { renderWithProviders } from "../../testutils";
import FlightPlansLayer from "./FlightPlansLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FlightPlansLayer", () => {
describe("unselected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if wrong coalition", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn when only selected flights are to be drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
describe("selected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn twice", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if red", () => {
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
it("are not drawn if there are no flights", () => {
renderWithProviders(<FlightPlansLayer blue={true} />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});

View File

@@ -1,32 +0,0 @@
import { renderWithProviders } from "../../testutils";
import FrontLine from "./FrontLine";
import { PolylineProps } from "react-leaflet";
const mockPolyline = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: PolylineProps) => {
mockPolyline(props);
},
}));
describe("FrontLine", () => {
it("is drawn in the correct location", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 0 },
];
renderWithProviders(
<FrontLine
front={{
id: "",
extents: extents,
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
});
});

View File

@@ -1,56 +0,0 @@
import { renderWithProviders } from "../../testutils";
import FrontLinesLayer from "./FrontLinesLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FrontLinesLayer", () => {
it("draws nothing when there are no front lines", () => {
renderWithProviders(<FrontLinesLayer />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws front lines", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 1 },
];
renderWithProviders(<FrontLinesLayer />, {
preloadedState: {
frontLines: {
fronts: {
foo: {
id: "foo",
extents: extents,
},
bar: {
id: "bar",
extents: extents,
},
},
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from "../../app/hooks";
import FrontLine from "../frontline";
import { LayerGroup } from "react-leaflet";
export default function FrontLinesLayer() {
export default function SupplyRoutesLayer() {
const fronts = useAppSelector(selectFrontLines).fronts;
return (
<LayerGroup>

View File

@@ -1,125 +0,0 @@
import { renderWithProviders } from "../../testutils";
import NavMeshLayer from "./NavMeshLayer";
import { PropsWithChildren } from "react";
const mockPolygon = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("NavMeshLayer", () => {
it("draws blue meshes", () => {
const poly1 = [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 1 },
{ lat: 1, lng: 0 },
],
];
const poly2 = [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: -1 },
{ lat: 1, lng: 0 },
],
];
renderWithProviders(<NavMeshLayer blue={true} />, {
preloadedState: {
navmeshes: {
blue: [
{
poly: poly1,
threatened: false,
},
{
poly: poly2,
threatened: true,
},
],
red: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 2 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
],
},
},
});
expect(mockPolygon).toHaveBeenCalledTimes(2);
expect(mockPolygon).toHaveBeenCalledWith(
expect.objectContaining({
fillColor: "#00ff00",
positions: poly1,
interactive: false,
})
);
expect(mockPolygon).toHaveBeenCalledWith(
expect.objectContaining({
fillColor: "#ff0000",
positions: poly2,
interactive: false,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws red navmesh", () => {
renderWithProviders(<NavMeshLayer blue={false} />, {
preloadedState: {
navmeshes: {
blue: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 1 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: -1 },
{ lat: 1, lng: 0 },
],
],
threatened: true,
},
],
red: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 2 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
],
},
},
});
expect(mockPolygon).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -1,5 +1,5 @@
import App from "./App";
import { setupStore } from "./app/store";
import { store } from "./app/store";
import { SocketProvider } from "./components/socketprovider/socketprovider";
import "./index.css";
import * as serviceWorker from "./serviceWorker";
@@ -12,7 +12,7 @@ const root = ReactDOM.createRoot(
);
root.render(
<React.StrictMode>
<Provider store={setupStore()}>
<Provider store={store}>
<SocketProvider>
<App />
</SocketProvider>

View File

@@ -1,30 +0,0 @@
// https://redux.js.org/usage/writing-tests
import { setupStore } from "../app/store";
import type { AppStore, RootState } from "../app/store";
import type { PreloadedState } from "@reduxjs/toolkit";
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import React, { PropsWithChildren } from "react";
import { Provider } from "react-redux";
// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: PreloadedState<RootState>;
store?: AppStore;
}
export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
return <Provider store={store}>{children}</Provider>;
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

View File

@@ -1,8 +0,0 @@
coverage:
status:
patch:
default:
informational: true
project:
default:
informational: true

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

View File

@@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

View File

@@ -1,34 +0,0 @@
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = "DCS Liberation"
copyright = "2023, DCS Liberation Team"
author = "DCS Liberation Team"
release = "8.0.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
"myst_parser",
"sphinx_rtd_theme",
"sphinx.ext.autosectionlabel",
"sphinx.ext.todo",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "sphinx_rtd_theme"
html_static_path = ["_static"]
todo_include_todos = True

View File

@@ -1,8 +0,0 @@
Design docs
===========
.. toctree::
:maxdepth: 2
:caption: Contents:
turnless.md

View File

@@ -1,8 +0,0 @@
Developer documentation
=======================
.. toctree::
:maxdepth: 2
:caption: Contents:
design/index.rst

View File

@@ -1,6 +0,0 @@
Manual
======
.. toctree::
:maxdepth: 2
:caption: Contents:

View File

@@ -1,16 +0,0 @@
DCS Liberation
==============
.. toctree::
:maxdepth: 2
:caption: Contents:
game/index.rst
modding/index.rst
dev/index.rst
Indices and tables
==================
* :ref:`genindex`
* :ref:`search`

View File

@@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@@ -1,10 +0,0 @@
Modding guide
=============
.. toctree::
:maxdepth: 2
:caption: Contents:
fuel-consumption-measurement.md
layouts.rst
weather.rst

View File

@@ -1,397 +0,0 @@
The Layout System
=================
.. note::
The documentation of the layout system is still WIP and not
complete as the development of this feature involves a major refactoring
of the base code. Therefore this documentation is currently used for
development purpose primarily. The documentation will be updated soon.
Any help in updating this wiki page is appreciated!
The Layout System is a new way of defining how ground objects like SAM
Sites or other Vehicle / Ship Groups will be generated (which type of
units, how many units, alignment and orientation). It is a complete
rework of the previous generator-based logic which was written in python
code. The new system allows to define layouts with easy to write yaml
code and the use of the DCS Mission Editor for easier placement of the
units. The layout system also introduced a new logical grouping of Units
and layouts for them, the Armed Forces, which will allow major
improvements to the Ground Warfare in upcoming features.
**Armed Forces**
The Armed Forces is a new system introduced with the layout system which will
allow to identitfy and group possible units from the faction together with
available layouts for these groups. It is comparable to the AirWing and Squadron
implementation but just for Ground and Naval Forces. All possible Force Groups
(grouping of 1 or more units and and the available layouts for them) will be
generated during campaign initialization and will be used later by many
different systems. A Force Group can also include static objects which was not
possible before the introduction of the layout system. It is also possible to
define presets of these Force Groups within the faction file which is handy for
more complex groups like a SA-10 Battery or similar. Example: `SA-10.yaml`_.
which includes all the units like SR, TR, LN and has the layout of a
`S-300_Site.yaml`_.
.. _SA-10.yaml: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/groups/SA-10.yaml
.. _S-300_Site.yaml: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/layouts/anti_air/S-300_Site.yaml
**The Layout System**
In the previous system the generator which created the ground object was written
in python which made modifications and reusability very complicated. To allow
easier handling of the layouts and decoupling of alignment of units and the
actual unit type (for example Ural-375) the layout system was introduced.
Previously we had a generator for every different SAM Site, now we can just
reuse the alignemnt (e.g. 6 Launchers in a circle alignment) for multiple SAM
Systems and introduce more variety.
This new System allows Users and Designers to easily create or modify
layouts as the new alginment and orientation of units is defined with
the DCS Mission editor. An additional .yaml file allows the
configuration of the layout with settings like allow unit types or
random amounts of units. In total the new system reduces the complexity
and allows to precisely align / orient units as needed and create
realistic looking ground units.
As the whole ground unit generation and handling was reworked it is now
also possible to add static units to a ground object, so even
Fortifcation or similar can be added to templates in the future.
General Concept
~~~~~~~~~~~~~~~
.. figure:: images/layouts.png
:alt: Overview
Overview
All possible Force Groups will be generated during campaign
initialization by checking all possible units for the specific faction
and all available layouts. The code will automatically match general
layouts with available units. It is also possible to define preset
groups within the faction files which group many units and the prefered
layouts for the group. This is especially handy for unique layouts which
are not defined as ``global``. For example complex sam sites like the
S-300 or Patriot which have very specific alignment of the different
units.
Layouts will be matched to units based on the special definition given
in the corresponding yaml file. For example a layout which is defined as
global and allows the unit_class SHORAD will automatically be used for
all available SHORAD units which are defined in the faction file.
.. todo:: Describe the optional flag.
All these generated ForceGroups will be managed by the ArmedForces class
of the specific coalition. This class will be used by other parts of the
system like the start_generator or the BuyMenu. The ArmedForces class
will then generate the TheaterGroundObject which will be used by
liberation.
Example for a customized Ground Object Buy Menu which makes use of
Templates and UnitGroups:
.. figure:: images/ground_object_buy_menu.png
:alt: Ground object buy menu
Ground object buy menu
How to modify or add layouts
----------------------------
.. warning::
Whenever changes were made to layouts they have to be re-imported into
Liberation. See :ref:`Import Layouts into Liberation`.
A layout consists of two special files:
- layout.miz which defines the actual positioning and alignment of the
groups / units
- layout.yaml which defines the necessary information like amount of
units, possible types or classes.
To add a new template a new yaml has to be created as every yaml can
only define exact one template. Best practice is to copy paste an
existing template which is similar to the one to be created as starting
point. The next step is to create a new .miz file and align Units and
statics to the wishes. Even if existing ones can be reused, best
practice is to always create a fresh one to prevent side effects. The
most important part is to use a new Group for every different Unit Type.
It is not possible to mix Unit Types in one group within a template. For
example it is not possible to have a logistic truck and a AAA in the
same group. The miz file will only be used to get the exact position and
orientation of the units, therefore it is irrelevant which type of unit
will be used. The unit type will be later defined inside the yaml file.
For the next step all Group names have to be added to the yaml file.
Take care to that these names match exactly! Assign the unit_types or
unit_classes properties to math the needs.
The Layout miz
~~~~~~~~~~~~~~
The miz file is used to define the positioning and orientation of the
different units for the template. The actual unit which is used is
irrelevant. It is important to use a unique and meaningful name for the
groups as this will be used in the yaml file as well. The information
which will be extracted from the miz file are just the coordinates and
heading of the units.
*Important*: Every different unit type has to be in a separate Group for
the template to work. You can not add units of different types to the
same group. They can get merged back together during generation by
setting the group property. In the example below both groups
``AAA Site 0`` and ``AAA Site 1`` have the group = 1 which means that
they will be in the same dcs group during generation.
*Important*: Liberation expects every template to be designed with an
orientation of heading 0 (North) in mind. The complete GroundObject will
during the campaign generation process be rotated to match the
orientation defined by the campaign designer. If the layout was not
created with an orientation of heading 0 the later generated
GroundObject will likely be misaligned and not work properly.
.. todo::
max amount of possible units is defined from the miz. Example if later the
group should have 6 units than there have to be 6 defined in the miz.
.. figure:: images/layout_miz_example.png
:alt: Example template mission
Example template mission
The Layout configuration file
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. todo:: Description about the layout yaml file.
Possible Information:
.. list-table::
:header-rows: 1
* - Property
- Type
- Required
- Description
- Example
* - name
- ``str``
- Yes
- A name to identify the template
- .. code:: yaml
name: Armor Group
* - tasks
- list of ``GroupTask``
- Yes
- A list of tasks which the template can fulfill
- .. code:: yaml
tasks:
- AAA
- SHORAD
* - generic
- ``bool``, default false
- No
- True if this template will be used to create general ``UnitGroups``
-
* - description
- ``str``
- No
- Short description of the template
-
* - groups
- List of ``Groups``
- Yes
- See below for definition of a group
-
* - layout_file
- ``str``
- No
- The .miz file which has the groups/units of the layout included. Only
needed if the file has a different name than the yaml file
- .. code:: yaml
layout_file: resources/layouts/naval/legacy_naval_templates.miz
.. todo:: Group and SubGroup
A group has 1..N sub groups. The name of the Group will be used later
within the DCS group name.
All SubGroups will be merged into one DCS Group
Every unit type has to be defined as a sub group as following:
.. list-table::
:header-rows: 1
* - Property
- Type
- Required
- Description
* - name
- ``str``
- Yes
- The group name used in the .miz. Must match exactly!
* - optional
- ``bool``, default: false
- No
- Defines wether the layout can be used without this group if the faction
has no access to the unit type or the user wants to disable this group
* - fill
- ``bool``, default: false
- No
- If the group is optional the layout is used from a PresetGroup this
property tells the system if it should use any possible faction
accessible unit to fill up this slot if no capable one was defined in
the preset yaml.
* - unit_count
- list of ``int``
- No
- Amount of units to be generated for this group. Can be fixed or a range
where it will be picked randomly
* - unit_types
- list of DCS unit type IDs
- No
- Specific unit_types for ground units. Complete list from `vehicles.py`_.
This list is extended by all supported mods!
* - unit_classes
- list of unit classes
- No
- Unit classes of supported units. Defined by ``UnitClass`` in
`game/data/units.py`_.
* - statics
- list of static types
- No
- Specific unit_types of statics. Complete list from `statics.py`_
.. _vehicles.py: https://github.com/pydcs/dcs/blob/master/dcs/vehicles.py
.. _game/data/units.py: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/data/units.py
.. _statics.py: https://github.com/pydcs/dcs/blob/master/dcs/statics.py
Complete example of a generic template for an Aircraft Carrier group:
.. code:: yaml
name: Carrier Group
generic: true
tasks:
- AircraftCarrier
groups:
- Carrier: # Group Name of the DCS Group
- name: Carrier Group 0 # Sub Group used in the layout.miz
unit_count:
- 1
unit_classes:
- AircraftCarrier
- Escort: # Group name of the 2nd Group
- name: Carrier Group 1
unit_count:
- 4
unit_classes:
- Destroyer
layout_file: resources/layouts/naval/legacy_naval_templates.miz
Import Layouts into Liberation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For performance improvements all layouts are serialized to a so called
pickle file inside the save folder defined in the liberation
preferences. Every time changes are made to the layouts this file has to
be recreated. It can be recreated by either deleting the layouts.p file
manually or using the special option in the Liberation Toolbar
(Developer Tools -> Import Layouts). It will also be recreated after
each Liberation update as it will check the Version Number and recreate
it when changes are recognized.
Migration from Generators
-------------------------
The previous generators were migrated using a script which build a group using
the generator. All of these groups were save into one .miz file
`original_generator_layouts.miz`_. This miz file can be used to verify the
templates and to generalize similar templates to decouple the layout from the
actual units. As this is a time-consuming and sphisticated task this will be
done over time. With the first step the technical requirements will be fulfilled
so that the generalization can happen afterwards the technical pr gets merged.
.. _original_generator_layouts.miz: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/layouts/original_generator_layouts.miz
Updates for Factions
~~~~~~~~~~~~~~~~~~~~
With the rework there were also some changes to the faction file
definitions. Older faction files can not be loaded anymore and have to
be adopted to the new changes. During migration all default factions
were automatically updated, so they will work out of the box.
You can find more detailed information about how to customize the
faction file in `Custom factions`_.
What was changed:
* Removed the ``ewrs`` list. All EWRs are now defined in the list
``air_defense_units``.
* Added the ``air_defense_units`` list. All units with the Role AntiAir can be
defined here as `GroundUnitType`_. All possible units are defined in
`resources/units/ground_units`_.
* Added ``preset_groups``. This list allows to define Preset Groups (described
above) like SAM Systems consisting of Launcher, SR, TR and so on instead of
adding them each to “air_defense_units”. The presets are defined in
`resources/groups`_
* Migrated ``air_defenses`` to air_defense_units and preset_sets.
* ``Missiles`` are migrated to GroundUnitTypes instead of Generator names (see
air_defense_units for how to use)
* Removed ``cruisers``, ``destroyers`` and ``naval_generators``. Migrated them
to naval_units and preset_groups
* Added ``naval_units`` with the correct ship name found in
`resources/units/ships`_.
* ``aircraft_carrier`` and ``helicopter_carrier`` were moved to ``naval_units``
as well.
.. _Custom factions: https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Factions
.. _GroundUnitType: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/dcs/groundunittype.py
.. _resources/units/ground_units: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/units/ground_units
.. _resources/units/ships: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/units/ships
.. _resources/groups: https://github.com/dcs-liberation/dcs_liberation/blob/develop/resources/groups
Preset Groups
-------------
Instead of adding the exact name of the previous generator to add
complex groups like SAM sites or similar to the faction it is now
possible to add preset groups to the faction file. As described earlier
such a preset group (Force Group) can be defined very easy with a yaml
file. This file allows to define the name, tasking, units, statics and
the prefered layouts. The first task defines the primary role of the
ForceGroup which gets generated from the preset.
Example:
.. code:: yaml
name: SA-10/S-300PS # The name of the group
tasks: # Define at least 1 task
- LORAD # The task(s) the Group can fulfill
units: # Define at least 1 unit
- SAM SA-10 S-300 "Grumble" Clam Shell SR
- SAM SA-10 S-300 "Grumble" Big Bird SR
- SAM SA-10 S-300 "Grumble" C2
- SAM SA-10 S-300 "Grumble" Flap Lid TR
- SAM SA-10 S-300 "Grumble" TEL D
- SAM SA-10 S-300 "Grumble" TEL C
statics: # Optional
- # Add some statics here
layouts: # Define at least one layout
- S-300 Site # prefered layouts for these groups
Resources:
* A list of all available preset groups can be found here: `resources/groups`_
* All possible tasks can be found in the `game/data/groups.py`_
* Units are defined with the variant name found in `resources/units`_
.. _game/data/groups.py: https://github.com/dcs-liberation/dcs_liberation/blob/develop/game/data/groups.py
.. _resources/units: https://github.com/dcs-liberation/dcs_liberation/tree/develop/resources/units

View File

@@ -1,76 +0,0 @@
#######
Weather
#######
Weather conditions in DCS Liberation are randomly generated at the start of each
turn. Some of the inputs to that generator (more to come) can be controlled via
the config files in ``resources/weather``.
**********
Archetypes
**********
A weather archetype defines the the conditions for a style of weather, such as
"clear", or "raining". There are currently four archetypes:
1. clear
2. cloudy
3. raining
4. thunderstorm
The odds of each archetype appearing in each season are defined in the theater
yaml files (``resources/theaters/*/info.yaml``).
.. literalinclude:: ../../resources/weather/archetypes/clear.yaml
:language: yaml
:linenos:
:caption: resources/weather/archetypes/clear.yaml
Wind speeds
===========
DCS missions define wind with a speed and heading at each of three altitudes:
1. MSL
2. 2000 meters
3. 8000 meters
Blending between each altitude band is done in a manner defined by DCS.
Liberation randomly generates a direction for the wind at MSL, and each other
altitude band will have wind within +/- 90 degrees of that heading.
Wind speeds can be modded by altering the ``speed`` dict in the archetype yaml.
The only random distribution currently supported is the Weibull distribution, so
all archetypes currently use:
.. code:: yaml
speed:
weibull:
...
The Weibull distribution has two parameters: a shape and a scale.
The scale is simplest to understand. 63.2% of all outcomes of the distribution
are below the scale parameter.
The shape controls where the peak of the distribution is. See the examples in
the links below for illustrations and guidelines, but generally speaking low
values (between 1 and 2.6) will cause low speeds to be more common, medium
values (around 3) will be fairly evenly distributed around the median, and high
values (greater than 3.7) will cause high speeds to be more common. As wind
speeds tend to be higher at higher altitudes and fairly slow close to the
ground, you typically want a low value for MSL, a medium value for 2000m, and a
high value for 8000m.
For examples, see https://statisticsbyjim.com/probability/weibull-distribution/.
To experiment with different inputs, use Wolfram Alpha, e.g.
https://www.wolframalpha.com/input?i=weibull+distribution+1.5+5.
When generating wind speeds, each subsequent altitude band will have the lower
band's speed added to its scale parameter. That is, for the example above, the
actual scale parameter of ``at_2000m`` will be ``20 + wind speed at MSL``, and
the scale parameter of ``at_8000m`` will be ``20 + wind speed at 2000m``. This
is to ensure that a generally windy day (high wind speed at MSL) will create
similarly high winds at higher altitudes and vice versa.

View File

@@ -1,2 +0,0 @@
myst-parser
sphinx_rtd_theme

View File

@@ -0,0 +1,630 @@
import logging
from collections.abc import Sequence
from typing import Type
from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
AH_64D_BLK_II,
CH_47D,
CH_53E,
Ka_50,
Ka_50_3,
Mi_24P,
Mi_24V,
Mi_26,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
SH_60B,
UH_1H,
UH_60A,
)
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
A_20G,
A_50,
An_26B,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
C_130,
C_17A,
E_2C,
E_3A,
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
F_4E,
F_5E_3,
F_86F_Sabre,
H_6J,
IL_76MD,
IL_78M,
I_16,
JF_17,
J_11A,
Ju_88A4,
KC130,
KC135MPRS,
KC_135,
KJ_2000,
L_39ZA,
MB_339A,
MQ_9_Reaper,
M_2000C,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_27K,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31,
Mirage_2000_5,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1CT,
Mirage_F1C_200,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
MosquitoFBMkVI,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
RQ_1A_Predator,
S_3B,
S_3B_Tanker,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
Su_24M,
Su_25,
Su_25T,
Su_25TM,
Su_27,
Su_30,
Su_33,
Su_34,
Tornado_GR4,
Tornado_IDS,
Tu_142,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I,
Yak_40,
)
from dcs.unittype import FlyingType
from game.dcs.aircrafttype import AircraftType
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.f104.f104 import VSN_F104G, VSN_F104S, VSN_F104S_AG
from pydcs_extensions.f22a.f22a import F_22A
from pydcs_extensions.hercules.hercules import Hercules
from pydcs_extensions.jas39.jas39 import JAS39Gripen, JAS39Gripen_AG
from pydcs_extensions.ov10a.ov10a import Bronco_OV_10A
from pydcs_extensions.su57.su57 import Su_57
from pydcs_extensions.uh60l.uh60l import KC130J, UH_60L
from .flighttype import FlightType
# All aircraft lists are in priority order. Aircraft higher in the list will be
# preferred over those lower in the list.
# TODO: These lists really ought to be era (faction) dependent.
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
# factions that also have F-4s should not.
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
CAP_CAPABLE = [
Su_57,
F_22A,
F_15C,
F_14B,
F_14A_135_GR,
Su_33,
J_11A,
Su_30,
Su_27,
MiG_29S,
F_16C_50,
FA_18C_hornet,
JF_17,
JAS39Gripen,
F_16A,
F_4E,
MiG_31,
MiG_25PD,
MiG_29G,
MiG_29A,
MiG_23MLD,
MiG_21Bis,
Mirage_2000_5,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1C_200,
Mirage_F1CT,
F_15E,
M_2000C,
F_5E_3,
VSN_F104S,
VSN_F104G,
MiG_19P,
A_4E_C,
F_86F_Sabre,
MiG_15bis,
C_101CC,
L_39ZA,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
P_47D_30,
P_47D_30bl1,
P_47D_40,
I_16,
]
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
A_10C_2,
A_10C,
Hercules,
Su_34,
Su_25TM,
Su_25T,
Su_25,
F_15E,
F_16C_50,
FA_18C_hornet,
Tornado_GR4,
Tornado_IDS,
JAS39Gripen_AG,
JF_17,
AV8BNA,
A_10A,
B_1B,
A_4E_C,
Bronco_OV_10A,
F_14B,
F_14A_135_GR,
AJS37,
Su_24M,
Su_17M4,
Su_33,
F_4E,
S_3B,
Su_30,
MiG_29S,
MiG_27K,
MiG_29A,
MiG_21Bis,
AH_64D_BLK_II,
AH_64D,
AH_64A,
AH_1W,
OH_58D,
SA342M,
SA342L,
Ka_50_3,
Ka_50,
Mi_28N,
Mi_24P,
Mi_24V,
Mi_8MT,
H_6J,
MiG_19P,
MiG_15bis,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
F_5E_3,
F_86F_Sabre,
MB_339A,
C_101CC,
L_39ZA,
UH_1H,
VSN_F104S_AG,
VSN_F104G,
A_20G,
Ju_88A4,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
I_16,
Bf_109K_4,
FW_190D9,
FW_190A8,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator,
]
# Aircraft used for SEAD and SEAD Escort tasks. Must be capable of the CAS DCS task.
SEAD_CAPABLE = [
JF_17,
F_16C_50,
FA_18C_hornet,
Tornado_IDS,
Su_25T,
Su_25TM,
F_4E,
A_4E_C,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
AV8BNA,
Su_24M,
Su_17M4,
Su_34,
Su_30,
MiG_27K,
Tornado_GR4,
]
# Aircraft used for DEAD tasks. Must be capable of the CAS DCS task.
DEAD_CAPABLE = SEAD_CAPABLE + [
AJS37,
F_15E,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
H_6J,
A_20G,
Ju_88A4,
VSN_F104S_AG,
VSN_F104G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
Bronco_OV_10A,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
]
# Aircraft used for Strike mission
STRIKE_CAPABLE = [
F_117A,
B_1B,
B_52H,
Tu_160,
Tu_95MS,
Tu_22M3,
H_6J,
F_15E,
AJS37,
Tornado_GR4,
F_16C_50,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24M,
Su_25TM,
Su_25T,
Su_25,
Su_34,
Su_33,
Su_30,
Su_27,
MiG_29S,
MiG_29G,
MiG_29A,
F_4E,
A_10C_2,
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
MiG_27K,
MiG_21Bis,
MiG_15bis,
F_5E_3,
F_86F_Sabre,
MB_339A,
C_101CC,
L_39ZA,
B_17G,
A_20G,
Ju_88A4,
VSN_F104S_AG,
VSN_F104G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
]
ANTISHIP_CAPABLE = [
AJS37,
Tu_142,
Tu_22M3,
H_6J,
FA_18C_hornet,
JAS39Gripen_AG,
Su_24M,
Su_17M4,
JF_17,
Su_34,
Su_30,
Tornado_IDS,
Tornado_GR4,
AV8BNA,
S_3B,
A_20G,
Ju_88A4,
MosquitoFBMkVI,
C_101CC,
SH_60B,
]
# This list does not "inherit" from the strike list because some strike aircraft can
# only carry guided weapons, and the AI cannot do runway attack with dguided weapons.
# https://github.com/dcs-liberation/dcs_liberation/issues/1703
RUNWAY_ATTACK_CAPABLE = [
JF_17,
Su_34,
Su_30,
Tornado_IDS,
M_2000C,
H_6J,
B_1B,
B_52H,
Tu_22M3,
H_6J,
F_15E,
AJS37,
F_16C_50,
FA_18C_hornet,
AV8BNA,
JF_17,
F_16A,
F_14B,
F_14A_135_GR,
JAS39Gripen_AG,
Tornado_IDS,
Su_17M4,
Su_24M,
Su_25TM,
Su_25T,
Su_25,
Su_34,
Su_33,
Su_30,
Su_27,
MiG_29S,
MiG_29G,
MiG_29A,
F_4E,
A_10C_2,
A_10C,
S_3B,
A_4E_C,
Bronco_OV_10A,
M_2000C,
Mirage_F1B,
Mirage_F1BE,
Mirage_F1CE,
Mirage_F1EE,
Mirage_F1EQ,
Mirage_F1M_CE,
Mirage_F1M_EE,
Mirage_F1CT,
MiG_27K,
MiG_21Bis,
MiG_15bis,
MB_339A,
F_5E_3,
F_86F_Sabre,
C_101CC,
L_39ZA,
B_17G,
A_20G,
Ju_88A4,
VSN_F104S_AG,
VSN_F104G,
P_47D_40,
P_47D_30bl1,
P_47D_30,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
MosquitoFBMkVI,
Bf_109K_4,
FW_190D9,
FW_190A8,
]
# For any aircraft that isn't necessarily directly involved in strike
# missions in a direct combat sense, but can transport objects and infantry.
TRANSPORT_CAPABLE = [
C_17A,
Hercules,
C_130,
IL_76MD,
An_26B,
Yak_40,
CH_53E,
CH_47D,
UH_60L,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_8MT,
Mi_26,
]
AIR_ASSAULT_CAPABLE = [
CH_53E,
CH_47D,
UH_60L,
SH_60B,
UH_60A,
UH_1H,
Mi_8MT,
Mi_26,
Mi_24P,
Mi_24V,
Hercules,
]
DRONES = [MQ_9_Reaper, RQ_1A_Predator, WingLoong_I]
AEWC_CAPABLE = [
E_3A,
E_2C,
A_50,
KJ_2000,
]
# Priority is given to the tankers that can carry the most fuel.
REFUELING_CAPABALE = [
KC_135,
KC135MPRS,
IL_78M,
KC130J,
KC130,
S_3B_Tanker,
]
def dcs_types_for_task(task: FlightType) -> Sequence[Type[FlyingType]]:
cap_missions = (
FlightType.BARCAP,
FlightType.INTERCEPTION,
FlightType.SWEEP,
FlightType.TARCAP,
)
if task in cap_missions:
return CAP_CAPABLE
elif task == FlightType.ANTISHIP:
return ANTISHIP_CAPABLE
elif task == FlightType.BAI:
return CAS_CAPABLE
elif task == FlightType.CAS:
return CAS_CAPABLE
elif task == FlightType.SEAD:
return SEAD_CAPABLE
elif task == FlightType.SEAD_ESCORT:
return SEAD_CAPABLE
elif task == FlightType.DEAD:
return DEAD_CAPABLE
elif task == FlightType.OCA_AIRCRAFT:
return CAS_CAPABLE
elif task == FlightType.OCA_RUNWAY:
return RUNWAY_ATTACK_CAPABLE
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
elif task == FlightType.AEWC:
return AEWC_CAPABLE
elif task == FlightType.REFUELING:
return REFUELING_CAPABALE
elif task == FlightType.TRANSPORT:
return TRANSPORT_CAPABLE
elif task == FlightType.AIR_ASSAULT:
return AIR_ASSAULT_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []
def aircraft_for_task(task: FlightType) -> list[AircraftType]:
dcs_types = dcs_types_for_task(task)
types: list[AircraftType] = []
for dcs_type in dcs_types:
types.extend(AircraftType.for_dcs_type(dcs_type))
return types
def tasks_for_aircraft(aircraft: AircraftType) -> list[FlightType]:
tasks: list[FlightType] = []
for task in FlightType:
if task is FlightType.FERRY:
# Not a plannable task, so skip it.
continue
if aircraft in aircraft_for_task(task):
tasks.append(task)
return tasks

View File

@@ -171,15 +171,6 @@ class Flight(SidcDescribable):
def missing_pilots(self) -> int:
return self.roster.missing_pilots
def set_flight_type(self, var: FlightType) -> None:
self.flight_type = var
# Update _flight_plan_builder so that the builder class remains relevant
# to the flight type
from .flightplans.flightplanbuildertypes import FlightPlanBuilderTypes
self._flight_plan_builder = FlightPlanBuilderTypes.for_flight(self)(self)
def return_pilots_and_aircraft(self) -> None:
self.roster.clear()
self.squadron.claim_inventory(-self.count)

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
@@ -55,12 +55,12 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.drop_off
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
@@ -68,11 +68,7 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
return meters(2500)
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target
def ui_zone(self) -> UiZone:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.theater.missiontarget import MissionTarget
@@ -67,20 +67,16 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
# drop-off waypoint.
return self.layout.drop_off or self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
# 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) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from .flightplan import FlightPlan, Layout
@@ -42,20 +42,16 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
return waypoint
return self.layout.departure
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.package.time_over_target
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import feet
@@ -37,20 +37,16 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.arrival
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
# TOT planning isn't really useful for ferries. 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) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.package.time_over_target

View File

@@ -8,10 +8,10 @@ generating the waypoints for the mission.
from __future__ import annotations
import math
from abc import ABC, abstractmethod
from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from functools import cached_property
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
@@ -21,7 +21,6 @@ from .planningerror import PlanningError
from ..flightwaypointtype import FlightWaypointType
from ..starttype import StartType
from ..traveltime import GroundSpeed, TravelTime
from ...savecompat import has_save_compat_for
if TYPE_CHECKING:
from game.dcs.aircrafttype import FuelConsumption
@@ -63,13 +62,6 @@ class FlightPlan(ABC, Generic[LayoutT]):
def __init__(self, flight: Flight, layout: LayoutT) -> None:
self.flight = flight
self.layout = layout
self.tot_offset = self.default_tot_offset()
@has_save_compat_for(7)
def __setstate__(self, state: dict[str, Any]) -> None:
if "tot_offset" not in state:
state["tot_offset"] = self.default_tot_offset()
self.__dict__.update(state)
@property
def package(self) -> Package:
@@ -157,7 +149,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
raise NotImplementedError
@property
def tot(self) -> datetime:
def tot(self) -> timedelta:
return self.package.time_over_target + self.tot_offset
@cached_property
@@ -203,7 +195,8 @@ class FlightPlan(ABC, Generic[LayoutT]):
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
)
def default_tot_offset(self) -> timedelta:
@property
def tot_offset(self) -> timedelta:
"""This flight's offset from the package's TOT.
Positive values represent later TOTs. An offset of -2 minutes is used
@@ -222,13 +215,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
for previous_waypoint, waypoint in self.edges(until=destination):
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
# Trim microseconds. Our simulation tick rate is 1 second, so anything that
# takes 100.1 or 100.9 seconds will take 100 seconds. DCS doesn't handle
# sub-second resolution for tasks anyway, nor are they interesting from a
# mission planning perspective, so there's little value to keeping them in the
# model.
return timedelta(seconds=math.floor(total.total_seconds()))
return total
def travel_time_between_waypoints(
self, a: FlightWaypoint, b: FlightWaypoint
@@ -237,10 +224,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
a.position, b.position, self.speed_between_waypoints(a, b)
)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
raise NotImplementedError
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
raise NotImplementedError
def request_escort_at(self) -> FlightWaypoint | None:
@@ -263,21 +250,35 @@ class FlightPlan(ABC, Generic[LayoutT]):
if waypoint == end:
return
def takeoff_time(self) -> datetime:
def takeoff_time(self) -> timedelta:
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
def minimum_duration_from_start_to_tot(self) -> timedelta:
return (
self._travel_time_to_waypoint(self.tot_waypoint)
+ self.estimate_startup()
+ self.estimate_ground_ops()
)
def startup_time(self) -> datetime:
return (
def startup_time(self) -> timedelta:
start_time = (
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
)
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
start_time = timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
start_time = timedelta(seconds=math.floor(start_time.total_seconds()))
# Feature request #1309: Carrier planes should start at +1s
# This is a workaround to a DCS problem: some AI planes spawn on
# the 'sixpack' when start_time is zero and cause a deadlock.
# Workaround: force the start_time to 1 second for these planes.
if self.flight.from_cp.is_fleet and start_time.total_seconds() == 0:
start_time = timedelta(seconds=1)
return start_time
def estimate_startup(self) -> timedelta:
if self.flight.start_type is StartType.COLD:
if self.flight.client_count:
@@ -296,17 +297,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
return timedelta(minutes=8)
@property
@abstractmethod
def mission_begin_on_station_time(self) -> datetime | None:
"""The time that the mission is first on-station.
Not all mission types will have a time when they can be considered on-station.
Missions that patrol or loiter (CAPs, CAS, refueling, AEW&C, etc) will have this
defined, but strike-like missions will not.
"""
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
"""The time that the mission is complete and the flight RTBs."""
raise NotImplementedError

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from functools import cached_property
from typing import Any, TYPE_CHECKING, TypeGuard
@@ -73,15 +73,15 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
@property
@abstractmethod
def join_time(self) -> datetime:
def join_time(self) -> timedelta:
...
@property
@abstractmethod
def split_time(self) -> datetime:
def split_time(self) -> timedelta:
...
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.join:
return self.join_time
elif waypoint == self.layout.split:
@@ -89,7 +89,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
return None
@property
def push_time(self) -> datetime:
def push_time(self) -> timedelta:
return self.join_time - TravelTime.between_points(
self.layout.hold.position,
self.layout.join.position,
@@ -97,11 +97,7 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
)
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.split_time
@self_type_guard

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from typing import TYPE_CHECKING, TypeVar
from dcs import Point
@@ -25,6 +25,10 @@ if TYPE_CHECKING:
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
@property
def lead_time(self) -> timedelta:
return timedelta()
@property
def package_speed_waypoints(self) -> set[FlightWaypoint]:
return {
@@ -46,6 +50,13 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.targets[0]
@property
def tot_offset(self) -> timedelta:
try:
return -self.lead_time
except AttributeError:
return timedelta()
@property
def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint(
@@ -80,14 +91,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
return total
@property
def join_time(self) -> datetime:
def join_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.layout.join, self.layout.ingress
)
return self.ingress_time - travel_time
@property
def split_time(self) -> datetime:
def split_time(self) -> timedelta:
travel_time_ingress = self.travel_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
@@ -104,14 +115,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
)
@property
def ingress_time(self) -> datetime:
def ingress_time(self) -> timedelta:
tot = self.tot
travel_time = self.travel_time_between_waypoints(
self.layout.ingress, self.target_area_waypoint
)
return tot - travel_time
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.ingress:
return self.ingress_time
elif waypoint in self.layout.targets:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from typing import Any, TYPE_CHECKING, TypeGuard
from game.typeguard import self_type_guard
@@ -25,10 +25,10 @@ class LoiterFlightPlan(StandardFlightPlan[Any], ABC):
@property
@abstractmethod
def push_time(self) -> datetime:
def push_time(self) -> timedelta:
...
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.hold:
return self.push_time
return None

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime, timedelta
from datetime import timedelta
from typing import Type
from dcs import Point
@@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
)
@property
def patrol_start_time(self) -> datetime:
def patrol_start_time(self) -> timedelta:
altitude = self.flight.unit_type.patrol_altitude
if altitude is None:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from typing import Any, TYPE_CHECKING, TypeGuard, TypeVar
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
@@ -61,22 +61,22 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
"""
@property
def patrol_start_time(self) -> datetime:
def patrol_start_time(self) -> timedelta:
return self.package.time_over_target
@property
def patrol_end_time(self) -> datetime:
def patrol_end_time(self) -> timedelta:
# TODO: This is currently wrong for CAS.
# CAS missions end when they're winchester or bingo. We need to
# configure push tasks for the escorts rather than relying on timing.
return self.patrol_start_time + self.patrol_duration
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.patrol_start:
return self.patrol_start_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.patrol_end:
return self.patrol_end_time
return None
@@ -90,11 +90,7 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
return self.layout.patrol_start
@property
def mission_begin_on_station_time(self) -> datetime:
return self.patrol_start_time
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.patrol_end_time
@self_type_guard

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import feet
@@ -43,19 +43,15 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.abort_location
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
return None
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
return self.tot
def mission_departure_time(self) -> timedelta:
return timedelta()
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):

View File

@@ -16,8 +16,9 @@ class SeadFlightPlan(FormationAttackFlightPlan):
def builder_type() -> Type[Builder]:
return Builder
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=1)
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=1)
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):

View File

@@ -1,11 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from typing import Iterator, Type
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.standard import StandardFlightPlan, StandardLayout
from game.ato.flightplans.ibuilder import IBuilder
from game.ato.flightplans.standard import StandardLayout
from game.ato.flightplans.waypointbuilder import WaypointBuilder
from game.ato.flightwaypoint import FlightWaypoint
@@ -37,27 +37,23 @@ class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
return self.layout.recovery_ship
@property
def mission_begin_on_station_time(self) -> datetime:
return self.package.time_over_target
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.patrol_end_time
@property
def patrol_start_time(self) -> datetime:
def patrol_start_time(self) -> timedelta:
return self.package.time_over_target
@property
def patrol_end_time(self) -> datetime:
def patrol_end_time(self) -> timedelta:
return self.tot + timedelta(hours=2)
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.tot
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.tot_waypoint:
return self.mission_departure_time
return None

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from typing import Iterator, TYPE_CHECKING, Type
from dcs import Point
@@ -38,6 +38,10 @@ class SweepLayout(LoiterLayout):
class SweepFlightPlan(LoiterFlightPlan):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=5)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -50,46 +54,42 @@ class SweepFlightPlan(LoiterFlightPlan):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.sweep_end
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=5)
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
@property
def sweep_start_time(self) -> datetime:
def sweep_start_time(self) -> timedelta:
travel_time = self.travel_time_between_waypoints(
self.layout.sweep_start, self.layout.sweep_end
)
return self.sweep_end_time - travel_time
@property
def sweep_end_time(self) -> datetime:
def sweep_end_time(self) -> timedelta:
return self.tot
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.sweep_start:
return self.sweep_start_time
if waypoint == self.layout.sweep_end:
return self.sweep_end_time
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.hold:
return self.push_time
return None
@property
def push_time(self) -> datetime:
def push_time(self) -> timedelta:
return self.sweep_end_time - TravelTime.between_points(
self.layout.hold.position,
self.layout.sweep_end.position,
GroundSpeed.for_flight(self.flight, self.layout.hold.alt),
)
@property
def mission_begin_on_station_time(self) -> datetime | None:
return None
@property
def mission_departure_time(self) -> datetime:
def mission_departure_time(self) -> timedelta:
return self.sweep_end_time

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import random
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime, timedelta
from datetime import timedelta
from typing import TYPE_CHECKING, Type
from game.utils import Distance, Speed, feet
@@ -34,6 +34,10 @@ class TarCapLayout(PatrollingLayout):
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=2)
@property
def patrol_duration(self) -> timedelta:
# Note that this duration only has an effect if there are no
@@ -60,23 +64,24 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.layout.patrol_start, self.layout.patrol_end}
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=2)
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
if waypoint == self.layout.patrol_end:
return self.patrol_end_time
return super().depart_time_for_waypoint(waypoint)
@property
def patrol_start_time(self) -> datetime:
def patrol_start_time(self) -> timedelta:
start = self.package.escort_start_time
if start is not None:
return start + self.tot_offset
return self.tot
@property
def patrol_end_time(self) -> datetime:
def patrol_end_time(self) -> timedelta:
end = self.package.escort_end_time
if end is not None:
return end

View File

@@ -21,36 +21,6 @@ class FlightState(ABC):
self.settings = settings
self.avoid_further_combat = False
def reinitialize(self, now: datetime) -> None:
from game.ato.flightstate import WaitingForStart
if self.flight.flight_plan.startup_time() <= now:
self._set_active_flight_state(now)
else:
self.flight.set_state(WaitingForStart(self.flight, self.settings))
def _set_active_flight_state(self, now: datetime) -> None:
from game.ato.flightstate import StartUp
from game.ato.flightstate import Taxi
from game.ato.flightstate import Takeoff
from game.ato.flightstate import Navigating
match self.flight.start_type:
case StartType.COLD:
self.flight.set_state(StartUp(self.flight, self.settings, now))
case StartType.WARM:
self.flight.set_state(Taxi(self.flight, self.settings, now))
case StartType.RUNWAY:
self.flight.set_state(Takeoff(self.flight, self.settings, now))
case StartType.IN_FLIGHT:
self.flight.set_state(
Navigating(self.flight, self.settings, waypoint_index=0)
)
case _:
raise ValueError(
f"Unknown start type {self.flight.start_type} for {self.flight}"
)
@property
def alive(self) -> bool:
return True

View File

@@ -20,12 +20,11 @@ class Uninitialized(FlightState):
def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:
self.reinitialize(time)
self.flight.state.on_game_tick(events, time, duration)
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
@property
def is_waiting_for_start(self) -> bool:
return True
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
def estimate_position(self) -> Point:
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
@@ -36,6 +35,7 @@ class Uninitialized(FlightState):
@property
def description(self) -> str:
delay = self.flight.flight_plan.startup_time()
if self.flight.start_type is StartType.COLD:
action = "Starting up"
elif self.flight.start_type is StartType.WARM:
@@ -46,4 +46,4 @@ class Uninitialized(FlightState):
action = "In flight"
else:
raise ValueError(f"Unhandled StartType: {self.flight.start_type}")
return f"{action} at {self.flight.flight_plan.startup_time():%H:%M:%S}"
return f"{action} in {delay}"

View File

@@ -18,17 +18,19 @@ if TYPE_CHECKING:
class WaitingForStart(AtDeparture):
def __init__(self, flight: Flight, settings: Settings) -> None:
def __init__(
self,
flight: Flight,
settings: Settings,
start_time: datetime,
) -> None:
super().__init__(flight, settings)
self.start_time = start_time
@property
def start_type(self) -> StartType:
return self.flight.start_type
@property
def start_time(self) -> datetime:
return self.flight.flight_plan.startup_time()
def on_game_tick(
self, events: GameUpdateEvents, time: datetime, duration: timedelta
) -> None:

View File

@@ -24,8 +24,8 @@ class FlightType(Enum):
* 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.
* resources/units/aircraft/*.yaml: Assign aircraft weight for the new task type in
the `tasks` dict for all capable aircraft.
* 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:

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from datetime import timedelta
from typing import Literal, TYPE_CHECKING
from dcs import Point
@@ -43,8 +43,8 @@ class FlightWaypoint:
# generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI.
tot: datetime | None = None
departure_time: datetime | None = None
tot: timedelta | None = None
departure_time: timedelta | None = None
@property
def x(self) -> float:

View File

@@ -92,9 +92,6 @@ class Loadout:
if self.has_weapon_of_type(WeaponType.TGP):
return
if unit_type.has_built_in_target_pod:
return
new_pylons = dict(self.pylons)
for pylon_number, weapon in self.pylons.items():
if weapon is not None and weapon.weapon_group.type is WeaponType.LGB:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import datetime
from datetime import timedelta
from typing import Dict, Optional, TYPE_CHECKING
from game.db import Database
@@ -33,11 +33,8 @@ class Package:
self.auto_asap = auto_asap
self.flights: list[Flight] = []
# Desired TOT as an offset from mission start. Obviously datetime.min is bogus,
# but it's going to be replaced by whatever is scheduling the package very soon.
# TODO: Constructor should maybe take the current time and use that to preserve
# the old behavior?
self.time_over_target: datetime = datetime.min
# Desired TOT as an offset from mission start.
self.time_over_target: timedelta = timedelta()
self.waypoints: PackageWaypoints | None = None
@property
@@ -65,7 +62,7 @@ class Package:
# TODO: Should depend on the type of escort.
# SEAD might be able to leave before CAP.
@property
def escort_start_time(self) -> datetime | None:
def escort_start_time(self) -> Optional[timedelta]:
times = []
for flight in self.flights:
waypoint = flight.flight_plan.request_escort_at()
@@ -84,7 +81,7 @@ class Package:
return None
@property
def escort_end_time(self) -> datetime | None:
def escort_end_time(self) -> Optional[timedelta]:
times = []
for flight in self.flights:
waypoint = flight.flight_plan.dismiss_escort_at()
@@ -106,7 +103,7 @@ class Package:
return None
@property
def mission_departure_time(self) -> datetime | None:
def mission_departure_time(self) -> Optional[timedelta]:
times = []
for flight in self.flights:
times.append(flight.flight_plan.mission_departure_time)
@@ -114,19 +111,8 @@ class Package:
return max(times)
return None
def set_tot_asap(self, now: datetime) -> None:
self.time_over_target = TotEstimator(self).earliest_tot(now)
def clamp_tot_for_current_time(self, now: datetime) -> None:
if not self.all_flights_waiting_for_start():
return
if not self.flights:
return
earliest_startup_time = min(f.flight_plan.startup_time() for f in self.flights)
if earliest_startup_time < now:
self.time_over_target += now - earliest_startup_time
def set_tot_asap(self) -> None:
self.time_over_target = TotEstimator(self).earliest_tot()
def add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package."""
@@ -218,14 +204,3 @@ class Package:
if flight.departure == airfield:
return airfield
raise RuntimeError("Could not find any airfield assigned to this package")
def all_flights_waiting_for_start(self) -> bool:
"""Returns True if all flights in the package are waiting for start."""
for flight in self.flights:
if not flight.state.is_waiting_for_start:
return False
return True
def has_flight_with_task(self, task: FlightType) -> bool:
"""Returns True if any flight in the package has the given task."""
return task in (f.flight_type for f in self.flights)

View File

@@ -1,6 +1,7 @@
from __future__ import annotations
from datetime import datetime, timedelta
import math
from datetime import timedelta
from typing import TYPE_CHECKING
from dcs.mapping import Point
@@ -55,24 +56,44 @@ class TotEstimator:
def __init__(self, package: Package) -> None:
self.package = package
def earliest_tot(self, now: datetime) -> datetime:
def earliest_tot(self) -> timedelta:
if not self.package.flights:
return now
return timedelta(0)
return max(self.earliest_tot_for_flight(f, now) for f in self.package.flights)
earliest_tot = max(
(self.earliest_tot_for_flight(f) for f in self.package.flights)
)
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
@staticmethod
def earliest_tot_for_flight(flight: Flight, now: datetime) -> datetime:
"""Estimate the earliest time the flight can reach the target position.
def earliest_tot_for_flight(flight: Flight) -> timedelta:
"""Estimate the fastest time from mission start to the target position.
The interpretation of the TOT depends on the flight plan type. See the various
FlightPlan implementations for details.
For BARCAP flights, this is time to the racetrack start. This ensures that
they are on station at the same time any other package members reach
their ingress point.
For other mission types this is the time to the mission target.
Args:
flight: The flight to get the earliest TOT for.
now: The current mission time.
flight: The flight to get the earliest TOT time for.
Returns:
The earliest possible TOT for the given flight.
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
"""
return now + flight.flight_plan.minimum_duration_from_start_to_tot()
# Clear the TOT, calculate the startup time. Negating the result gives
# the earliest possible start time.
orig_tot = flight.package.time_over_target
try:
flight.package.time_over_target = timedelta()
time = flight.flight_plan.startup_time()
finally:
flight.package.time_over_target = orig_tot
return -time

View File

@@ -5,24 +5,22 @@ import logging
from collections.abc import Iterator
from dataclasses import dataclass
from pathlib import Path
from typing import Any, Dict, TYPE_CHECKING, Tuple
from typing import Any, Dict, Tuple
import yaml
from packaging.version import Version
from game import persistence
from game import persistency
from game.profiling import logged_duration
from game.theater import ConflictTheater
from game.theater import (
ConflictTheater,
)
from game.theater.iadsnetwork.iadsnetwork import IadsNetwork
from game.theater.theaterloader import TheaterLoader
from game.version import CAMPAIGN_FORMAT_VERSION
from .campaignairwingconfig import CampaignAirWingConfig
from .factionrecommendation import FactionRecommendation
from .mizcampaignloader import MizCampaignLoader
if TYPE_CHECKING:
from game.factions.factions import Factions
PERF_FRIENDLY = 0
PERF_MEDIUM = 1
PERF_HARD = 2
@@ -42,8 +40,8 @@ class Campaign:
#: selecting a campaign that is not up to date.
version: Tuple[int, int]
recommended_player_faction: FactionRecommendation
recommended_enemy_faction: FactionRecommendation
recommended_player_faction: str
recommended_enemy_faction: str
recommended_start_date: datetime.date | None
recommended_start_time: datetime.time | None
@@ -59,9 +57,10 @@ class Campaign:
@classmethod
def from_file(cls, path: Path) -> Campaign:
with path.open(encoding="utf-8") as campaign_file:
with path.open() as campaign_file:
data = yaml.safe_load(campaign_file)
sanitized_theater = data["theater"].replace(" ", "")
version_field = data.get("version", "0")
try:
version = Version(version_field)
@@ -94,12 +93,8 @@ class Campaign:
data.get("authors", "???"),
data.get("description", ""),
(version.major, version.minor),
FactionRecommendation.from_field(
data.get("recommended_player_faction"), player=True
),
FactionRecommendation.from_field(
data.get("recommended_enemy_faction"), player=False
),
data.get("recommended_player_faction", "USA 2005"),
data.get("recommended_enemy_faction", "Russia 1990"),
start_date,
start_time,
data.get("recommended_player_money", DEFAULT_BUDGET),
@@ -168,10 +163,6 @@ class Campaign:
return False
return True
def register_campaign_specific_factions(self, factions: Factions) -> None:
self.recommended_player_faction.register_campaign_specific_faction(factions)
self.recommended_enemy_faction.register_campaign_specific_faction(factions)
@staticmethod
def iter_campaigns_in_dir(path: Path) -> Iterator[Path]:
yield from path.glob("*.yaml")
@@ -180,7 +171,7 @@ class Campaign:
@classmethod
def iter_campaign_defs(cls) -> Iterator[Path]:
yield from cls.iter_campaigns_in_dir(
Path(persistence.base_path()) / "Liberation/Campaigns"
Path(persistency.base_path()) / "Liberation/Campaigns"
)
yield from cls.iter_campaigns_in_dir(Path("resources/campaigns"))

View File

@@ -11,15 +11,11 @@ if TYPE_CHECKING:
from game.theater import ConflictTheater
DEFAULT_SQUADRON_SIZE = 12
@dataclass(frozen=True)
class SquadronConfig:
primary: FlightType
secondary: list[FlightType]
aircraft: list[str]
max_size: int
name: Optional[str]
nickname: Optional[str]
@@ -43,7 +39,6 @@ class SquadronConfig:
FlightType(data["primary"]),
secondary,
data.get("aircraft", []),
data.get("size", DEFAULT_SQUADRON_SIZE),
data.get("name", None),
data.get("nickname", None),
data.get("female_pilot_percentage", None),

View File

@@ -1,12 +1,13 @@
from __future__ import annotations
import dataclasses
import logging
from typing import Optional, TYPE_CHECKING
from typing import Optional, TYPE_CHECKING, Dict, Union
from game.squadrons import Squadron
from game.squadrons.squadrondef import SquadronDef
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
from ..ato.flighttype import FlightType
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
from ..dcs.aircrafttype import AircraftType
from ..theater import ControlPoint
@@ -43,12 +44,7 @@ class DefaultSquadronAssigner:
continue
squadron = Squadron.create_from(
squadron_def,
squadron_config.primary,
squadron_config.max_size,
control_point,
self.coalition,
self.game,
squadron_def, control_point, self.coalition, self.game
)
squadron.set_auto_assignable_mission_types(
squadron_config.auto_assignable
@@ -58,6 +54,7 @@ class DefaultSquadronAssigner:
def find_squadron_for(
self, config: SquadronConfig, control_point: ControlPoint
) -> Optional[SquadronDef]:
for preferred_aircraft in config.aircraft:
squadron_def = self.find_preferred_squadron(
preferred_aircraft, config.primary, control_point
@@ -65,14 +62,13 @@ class DefaultSquadronAssigner:
if squadron_def is not None:
return squadron_def
# If we didn't find any of the preferred types (if the list contains only
# squadrons or aircraft unavailable to the coalition) we should use any squadron
# If we didn't find any of the preferred types we should use any squadron
# compatible with the primary task.
squadron_def = self.find_squadron_for_task(config.primary, control_point)
if squadron_def is not None:
return squadron_def
# If we can't find any pre-made squadron matching the requirement, we should
# If we can't find any squadron matching the requirement, we should
# create one.
return self.air_wing.squadron_def_generator.generate_for_task(
config.primary, control_point
@@ -93,11 +89,7 @@ class DefaultSquadronAssigner:
try:
aircraft = AircraftType.named(preferred_aircraft)
except KeyError:
logging.warning(
"%s is neither a compatible squadron or a known aircraft type, "
"ignoring",
preferred_aircraft,
)
# No aircraft with this name.
return None
if aircraft not in self.coalition.faction.aircrafts:
@@ -120,7 +112,7 @@ class DefaultSquadronAssigner:
) -> bool:
if ignore_base_preference:
return control_point.can_operate(squadron.aircraft)
return squadron.operates_from(control_point) and squadron.capable_of(task)
return squadron.operates_from(control_point) and task in squadron.mission_types
def find_squadron_for_airframe(
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint

View File

@@ -1,53 +0,0 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, TYPE_CHECKING
from game.factions import Faction
if TYPE_CHECKING:
from game.factions.factions import Factions
class FactionRecommendation(ABC):
def __init__(self, name: str) -> None:
self.name = name
@abstractmethod
def register_campaign_specific_faction(self, factions: Factions) -> None:
...
@abstractmethod
def get_faction(self, factions: Factions) -> Faction:
...
@staticmethod
def from_field(
data: str | dict[str, Any] | None, player: bool
) -> FactionRecommendation:
if data is None:
name = "USA 2005" if player else "Russia 1990"
return BuiltinFactionRecommendation(name)
if isinstance(data, str):
return BuiltinFactionRecommendation(data)
return CampaignDefinedFactionRecommendation(Faction.from_dict(data))
class BuiltinFactionRecommendation(FactionRecommendation):
def register_campaign_specific_faction(self, factions: Factions) -> None:
pass
def get_faction(self, factions: Factions) -> Faction:
return factions.get_by_name(self.name)
class CampaignDefinedFactionRecommendation(FactionRecommendation):
def __init__(self, faction: Faction) -> None:
super().__init__(faction.name)
self.faction = faction
def register_campaign_specific_faction(self, factions: Factions) -> None:
factions.add_campaign_defined(self.faction)
def get_faction(self, factions: Factions) -> Faction:
return self.faction

View File

@@ -9,6 +9,7 @@ from game.dcs.aircrafttype import AircraftType
from game.squadrons.operatingbases import OperatingBases
from game.squadrons.squadrondef import SquadronDef
from game.theater import ControlPoint
from game.ato.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
if TYPE_CHECKING:
from game.factions.faction import Faction
@@ -24,7 +25,7 @@ class SquadronDefGenerator:
self, task: FlightType, control_point: ControlPoint
) -> Optional[SquadronDef]:
aircraft_choice: Optional[AircraftType] = None
for aircraft in AircraftType.priority_list_for_task(task):
for aircraft in aircraft_for_task(task):
if aircraft not in self.faction.aircrafts:
continue
if not control_point.can_operate(aircraft):
@@ -47,7 +48,7 @@ class SquadronDefGenerator:
role="Flying Squadron",
aircraft=aircraft,
livery=None,
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
mission_types=tuple(tasks_for_aircraft(aircraft)),
operating_bases=OperatingBases.default_for_aircraft(aircraft),
female_pilot_percentage=6,
pilot_pool=[],

View File

@@ -1,6 +1,5 @@
from __future__ import annotations
from datetime import datetime
from typing import Any, Optional, TYPE_CHECKING
from faker import Faker
@@ -158,14 +157,14 @@ class Coalition:
# is handled correctly.
self.transfers.perform_transfers()
def preinit_turn_0(self, squadrons_start_full: bool) -> None:
def preinit_turn_0(self) -> None:
"""Runs final Coalition initialization.
Final initialization occurs before Game.initialize_turn runs for turn 0.
"""
self.air_wing.populate_for_turn_0(squadrons_start_full)
self.air_wing.populate_for_turn_0()
def initialize_turn(self, is_turn_0: bool) -> None:
def initialize_turn(self) -> None:
"""Processes coalition-specific turn initialization.
For more information on turn initialization in general, see the documentation
@@ -182,10 +181,9 @@ class Coalition:
with logged_duration("Procurement of airlift assets"):
self.transfers.order_airlift_assets()
with logged_duration("Transport planning"):
self.transfers.plan_transports(self.game.conditions.start_time)
self.transfers.plan_transports()
if not is_turn_0 or not self.game.settings.enable_squadron_aircraft_limits:
self.plan_missions(self.game.conditions.start_time)
self.plan_missions()
self.plan_procurement()
def refund_outstanding_orders(self) -> None:
@@ -201,16 +199,16 @@ class Coalition:
for squadron in self.air_wing.iter_squadrons():
squadron.refund_orders()
def plan_missions(self, now: datetime) -> None:
def plan_missions(self) -> None:
color = "Blue" if self.player else "Red"
with MultiEventTracer() as tracer:
with tracer.trace(f"{color} mission planning"):
with tracer.trace(f"{color} mission identification"):
TheaterCommander(self.game, self.player).plan_missions(now, tracer)
TheaterCommander(self.game, self.player).plan_missions(tracer)
with tracer.trace(f"{color} mission scheduling"):
MissionScheduler(
self, self.game.settings.desired_player_mission_duration
).schedule_missions(now)
).schedule_missions()
def plan_procurement(self) -> None:
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much

View File

@@ -3,12 +3,12 @@ from __future__ import annotations
import logging
import random
from collections import defaultdict
from datetime import datetime, timedelta
from typing import Iterator, TYPE_CHECKING
from datetime import timedelta
from typing import Iterator, Dict, TYPE_CHECKING
from game.theater import MissionTarget
from game.ato.flighttype import FlightType
from game.ato.traveltime import TotEstimator
from game.theater import MissionTarget
if TYPE_CHECKING:
from game.coalition import Coalition
@@ -19,7 +19,7 @@ class MissionScheduler:
self.coalition = coalition
self.desired_mission_length = desired_mission_length
def schedule_missions(self, now: datetime) -> None:
def schedule_missions(self) -> None:
"""Identifies and plans mission for the turn."""
def start_time_generator(
@@ -35,7 +35,7 @@ class MissionScheduler:
FlightType.TARCAP,
}
previous_cap_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
non_dca_packages = [
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
]
@@ -47,7 +47,7 @@ class MissionScheduler:
margin=5 * 60,
)
for package in self.coalition.ato.packages:
tot = TotEstimator(package).earliest_tot(now)
tot = TotEstimator(package).earliest_tot()
if package.primary_task in dca_types:
previous_end_time = previous_cap_end_time[package.target]
if tot > previous_end_time:
@@ -65,7 +65,7 @@ class MissionScheduler:
continue
previous_cap_end_time[package.target] = departure_time
elif package.auto_asap:
package.set_tot_asap(now)
package.set_tot_asap()
else:
# But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at

View File

@@ -5,7 +5,6 @@ import operator
from collections.abc import Iterable, Iterator
from typing import TYPE_CHECKING, TypeVar
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
from game.theater import (
Airfield,
ControlPoint,
@@ -16,11 +15,12 @@ from game.theater import (
)
from game.theater.theatergroundobject import (
BuildingGroundObject,
IadsBuildingGroundObject,
IadsGroundObject,
NavalGroundObject,
IadsBuildingGroundObject,
)
from game.utils import meters, nautical_miles
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
if TYPE_CHECKING:
from game import Game
@@ -117,7 +117,7 @@ class ObjectiveFinder:
if isinstance(
ground_object, IadsBuildingGroundObject
) and not self.game.lua_plugin_manager.is_plugin_enabled("skynetiads"):
) and not self.game.settings.plugin_option("skynetiads"):
# Prevent strike targets on IADS Buildings when skynet features
# are disabled as they do not serve any purpose
continue
@@ -209,20 +209,22 @@ class ObjectiveFinder:
raise RuntimeError("Found no friendly control points. You probably lost.")
return farthest
def preferred_theater_refueling_control_point(self) -> ControlPoint | None:
def closest_friendly_control_point(self) -> ControlPoint:
"""Finds the friendly control point that is closest to any threats."""
threat_zones = self.game.threat_zone_for(not self.is_player)
closest = None
min_distance = meters(math.inf)
for cp in self.friendly_control_points():
if isinstance(cp, OffMapSpawn) or cp.is_fleet:
if isinstance(cp, OffMapSpawn):
continue
distance = threat_zones.distance_to_threat(cp.position)
if distance < min_distance:
closest = cp
min_distance = distance
if closest is None:
raise RuntimeError("Found no friendly control points. You probably lost.")
return closest
def enemy_control_points(self) -> Iterator[ControlPoint]:

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import datetime
from typing import Dict, Iterable, Optional, Set, TYPE_CHECKING
from game.ato.airtaaskingorder import AirTaskingOrder
@@ -133,7 +132,6 @@ class PackageFulfiller:
self,
mission: ProposedMission,
purchase_multiplier: int,
now: datetime,
tracer: MultiEventTracer,
) -> Optional[Package]:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
@@ -223,6 +221,6 @@ class PackageFulfiller:
if package.has_players and self.player_missions_asap:
package.auto_asap = True
package.set_tot_asap(now)
package.set_tot_asap()
return package

View File

@@ -31,7 +31,8 @@ class RangeType(IntEnum):
# TODO: Refactor so that we don't need to call up to the mission planner.
@dataclass
# Bypass type checker due to https://github.com/python/mypy/issues/5374
@dataclass # type: ignore
class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
target: MissionTargetT
flights: list[ProposedFlight] = field(init=False)
@@ -103,7 +104,6 @@ class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
self.package = fulfiller.plan_mission(
ProposedMission(self.target, self.flights),
self.purchase_multiplier,
state.context.now,
state.context.tracer,
)
return self.package is not None

View File

@@ -54,7 +54,6 @@ https://en.wikipedia.org/wiki/Hierarchical_task_network
"""
from __future__ import annotations
from datetime import datetime
from typing import TYPE_CHECKING
from game.ato.starttype import StartType
@@ -78,8 +77,8 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
self.game = game
self.player = player
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, now, tracer)
def plan_missions(self, tracer: MultiEventTracer) -> None:
state = TheaterState.from_game(self.game, self.player, tracer)
while True:
result = self.plan(state)
if result is None:

View File

@@ -5,7 +5,6 @@ import itertools
import math
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import datetime
from typing import Optional, TYPE_CHECKING, Union
from game.commander.battlepositions import BattlePositions
@@ -37,7 +36,6 @@ class PersistentContext:
coalition: Coalition
theater: ConflictTheater
turn: int
now: datetime
settings: Settings
tracer: MultiEventTracer
@@ -139,20 +137,14 @@ class TheaterState(WorldState["TheaterState"]):
@classmethod
def from_game(
cls, game: Game, player: bool, now: datetime, tracer: MultiEventTracer
cls, game: Game, player: bool, tracer: MultiEventTracer
) -> TheaterState:
coalition = game.coalition_for(player)
finder = ObjectiveFinder(game, player)
ordered_capturable_points = finder.prioritized_unisolated_points()
context = PersistentContext(
game.db,
coalition,
game.theater,
game.turn,
now,
game.settings,
tracer,
game.db, coalition, game.theater, game.turn, game.settings, tracer
)
# Plan enough rounds of CAP that the target has coverage over the expected
@@ -161,11 +153,6 @@ class TheaterState(WorldState["TheaterState"]):
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
barcap_rounds = math.ceil(mission_duration / barcap_duration)
refueling_targets: list[MissionTarget] = []
theater_refuling_point = finder.preferred_theater_refueling_control_point()
if theater_refuling_point is not None:
refueling_targets.append(theater_refuling_point)
return TheaterState(
context=context,
barcaps_needed={
@@ -175,7 +162,7 @@ class TheaterState(WorldState["TheaterState"]):
front_line_stances={f: None for f in finder.front_lines()},
vulnerable_front_lines=list(finder.front_lines()),
aewc_targets=[finder.farthest_friendly_control_point()],
refueling_targets=refueling_targets,
refueling_targets=[finder.closest_friendly_control_point()],
enemy_air_defenses=list(finder.enemy_air_defenses()),
threatening_air_defenses=[],
detecting_air_defenses=[],

View File

@@ -97,7 +97,6 @@ class WeaponType(Enum):
ARM = "ARM"
LGB = "LGB"
TGP = "TGP"
DECOY = "decoy"
UNKNOWN = "unknown"

View File

@@ -1,11 +1,10 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from functools import cache, cached_property
from functools import cached_property
from pathlib import Path
from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING, Type
import yaml
from dcs.helicopters import helicopter_map
@@ -21,7 +20,6 @@ from game.radio.channels import (
CommonRadioChannelAllocator,
FarmerRadioChannelAllocator,
HueyChannelNamer,
LegacyWarthogChannelNamer,
MirageChannelNamer,
MirageF1CEChannelNamer,
NoOpChannelAllocator,
@@ -33,7 +31,6 @@ from game.radio.channels import (
ViggenChannelNamer,
ViggenRadioChannelAllocator,
ViperChannelNamer,
WarthogChannelNamer,
)
from game.utils import (
Distance,
@@ -50,7 +47,6 @@ from game.utils import (
)
if TYPE_CHECKING:
from game.ato import FlightType
from game.missiongenerator.aircraft.flightdata import FlightData
from game.missiongenerator.missiondata import MissionData
from game.radio.radios import Radio, RadioFrequency, RadioRegistry
@@ -108,8 +104,6 @@ class RadioConfig:
"viggen": ViggenChannelNamer,
"viper": ViperChannelNamer,
"apache": ApacheChannelNamer,
"a10c-legacy": LegacyWarthogChannelNamer,
"a10c-ii": WarthogChannelNamer,
}[config.get("namer", "default")]
@@ -198,23 +192,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
# will be set to true for helos by default
can_carry_crates: bool
task_priorities: dict[FlightType, int]
# Set to True when aircraft mounts a targeting pod by default i.e. the pod does
# not take up a weapons station. If True, do not replace LGBs with dumb bombs
# when no TGP is mounted on any station.
has_built_in_target_pod: bool
_by_name: ClassVar[dict[str, AircraftType]] = {}
_by_unit_type: ClassVar[dict[type[FlyingType], list[AircraftType]]] = defaultdict(
list
)
@classmethod
def register(cls, unit_type: AircraftType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@property
def flyable(self) -> bool:
return self.dcs_unit_type.flyable
@@ -325,12 +302,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
def iter_props(self) -> Iterator[UnitProperty[Any]]:
return UnitProperty.for_aircraft(self.dcs_unit_type)
def capable_of(self, task: FlightType) -> bool:
return task in self.task_priorities
def task_priority(self, task: FlightType) -> int:
return self.task_priorities[task]
def __setstate__(self, state: dict[str, Any]) -> None:
# Update any existing models with new data on load.
updated = AircraftType.named(state["name"])
@@ -341,31 +312,17 @@ class AircraftType(UnitType[Type[FlyingType]]):
def named(cls, name: str) -> AircraftType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, AircraftType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
@classmethod
def iter_all(cls) -> Iterator[AircraftType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_name.values()
@classmethod
@cache
def priority_list_for_task(cls, task: FlightType) -> list[AircraftType]:
capable = []
for aircraft in cls.iter_all():
if aircraft.capable_of(task):
capable.append(aircraft)
return list(reversed(sorted(capable, key=lambda a: a.task_priority(task))))
def iter_task_capabilities(self) -> Iterator[FlightType]:
yield from self.task_priorities
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, AircraftType)
yield unit
@staticmethod
def each_dcs_type() -> Iterator[Type[FlyingType]]:
@@ -391,8 +348,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
@classmethod
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
from game.ato.flighttype import FlightType
data_path = Path("resources/units/aircraft") / f"{aircraft.id}.yaml"
if not data_path.exists():
logging.warning(f"No data for {aircraft.id}; it will not be available")
@@ -449,10 +404,6 @@ class AircraftType(UnitType[Type[FlyingType]]):
if prop_overrides is not None:
cls._set_props_overrides(prop_overrides, aircraft, data_path)
task_priorities: dict[FlightType, int] = {}
for task_name, priority in data.get("tasks", {}).items():
task_priorities[FlightType(task_name)] = priority
for variant in data.get("variants", [aircraft.id]):
yield AircraftType(
dcs_unit_type=aircraft,
@@ -484,9 +435,4 @@ class AircraftType(UnitType[Type[FlyingType]]):
unit_class=unit_class,
cabin_size=data.get("cabin_size", 10 if aircraft.helicopter else 0),
can_carry_crates=data.get("can_carry_crates", aircraft.helicopter),
task_priorities=task_priorities,
has_built_in_target_pod=data.get("has_built_in_target_pod", False),
)
def __hash__(self) -> int:
return hash(self.name)

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Any, ClassVar, Iterator, Optional, Type
from typing import Any, Iterator, Optional, Type
import yaml
from dcs.unittype import VehicleType
@@ -60,27 +59,21 @@ class GroundUnitType(UnitType[Type[VehicleType]]):
# Some units like few Launchers have to be placed backwards to be able to fire.
reversed_heading: bool = False
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
_by_unit_type: ClassVar[
dict[type[VehicleType], list[GroundUnitType]]
] = defaultdict(list)
@classmethod
def register(cls, unit_type: GroundUnitType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> GroundUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, GroundUnitType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, GroundUnitType)
yield unit
@staticmethod
def each_dcs_type() -> Iterator[Type[VehicleType]]:

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
import logging
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import ClassVar, Iterator, Type
from typing import Iterator, Type
import yaml
from dcs.ships import ship_map
@@ -16,27 +15,21 @@ from game.dcs.unittype import UnitType
@dataclass(frozen=True)
class ShipUnitType(UnitType[Type[ShipType]]):
_by_name: ClassVar[dict[str, ShipUnitType]] = {}
_by_unit_type: ClassVar[dict[type[ShipType], list[ShipUnitType]]] = defaultdict(
list
)
@classmethod
def register(cls, unit_type: ShipUnitType) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> ShipUnitType:
if not cls._loaded:
cls._load_all()
return cls._by_name[name]
unit = cls._by_name[name]
assert isinstance(unit, ShipUnitType)
return unit
@classmethod
def for_dcs_type(cls, dcs_unit_type: Type[ShipType]) -> Iterator[ShipUnitType]:
if not cls._loaded:
cls._load_all()
yield from cls._by_unit_type[dcs_unit_type]
for unit in cls._by_unit_type[dcs_unit_type]:
assert isinstance(unit, ShipUnitType)
yield unit
@staticmethod
def each_dcs_type() -> Iterator[Type[ShipType]]:

View File

@@ -1,9 +1,10 @@
from __future__ import annotations
from abc import ABC
from collections import defaultdict
from dataclasses import dataclass
from functools import cached_property
from typing import ClassVar, Generic, Iterator, Self, Type, TypeVar
from typing import Any, ClassVar, Generic, Iterator, Type, TypeVar
from dcs.unittype import UnitType as DcsUnitType
@@ -24,6 +25,10 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
price: int
unit_class: UnitClass
_by_name: ClassVar[dict[str, UnitType[Any]]] = {}
_by_unit_type: ClassVar[dict[Type[DcsUnitType], list[UnitType[Any]]]] = defaultdict(
list
)
_loaded: ClassVar[bool] = False
def __str__(self) -> str:
@@ -34,15 +39,16 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
return self.dcs_unit_type.id
@classmethod
def register(cls, unit_type: Self) -> None:
def register(cls, unit_type: UnitType[Any]) -> None:
cls._by_name[unit_type.name] = unit_type
cls._by_unit_type[unit_type.dcs_unit_type].append(unit_type)
@classmethod
def named(cls, name: str) -> UnitType[Any]:
raise NotImplementedError
@classmethod
def named(cls, name: str) -> Self:
raise NotImplementedError
@classmethod
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[Self]:
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
raise NotImplementedError
@staticmethod
@@ -50,7 +56,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
raise NotImplementedError
@classmethod
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[Self]:
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
raise NotImplementedError
@classmethod

View File

@@ -109,42 +109,16 @@ class StateData:
base_capture_events: List[str]
@classmethod
def from_json(cls, data: Dict[str, Any], unit_map: UnitMap) -> StateData:
def clean_unit_list(unit_list: List[Any]) -> List[str]:
# Cleans list of units in state.json by
# - Removing duplicates. Airfields emit a new "dead" event every time a bomb
# is dropped on them when they've already dead.
# - Normalise dead map objects (which are ints) to strings. The unit map
# only stores strings
units = set()
for unit in unit_list:
units.add(str(unit))
return list(units)
killed_aircraft = []
killed_ground_units = []
# Process killed units from S_EVENT_UNIT_LOST, S_EVENT_CRASH, S_EVENT_DEAD & S_EVENT_KILL
# Try to process every event that could indicate a unit was killed, even if it is
# inefficient and results in duplication as the logic DCS uses to trigger the various
# event types is not clear and may change over time.
killed_units = clean_unit_list(
data["unit_lost_events"]
+ data["kill_events"]
+ data["crash_events"]
+ data["dead_events"]
+ data["killed_ground_units"]
)
for unit in killed_units: # organize killed units into aircraft vs ground
if unit_map.flight(unit) is not None:
killed_aircraft.append(unit)
else:
killed_ground_units.append(unit)
def from_json(cls, data: Dict[str, Any]) -> StateData:
return cls(
mission_ended=data["mission_ended"],
killed_aircraft=killed_aircraft,
killed_ground_units=killed_ground_units,
killed_aircraft=data["killed_aircrafts"],
# Airfields emit a new "dead" event every time a bomb is dropped on
# them when they've already dead. Dedup.
#
# Also normalize dead map objects (which are ints) to strings. The unit map
# only stores strings.
killed_ground_units=list({str(u) for u in data["killed_ground_units"]}),
destroyed_statics=data["destroyed_objects_positions"],
base_capture_events=data["base_capture_events"],
)
@@ -154,7 +128,7 @@ class Debriefing:
def __init__(
self, state_data: Dict[str, Any], game: Game, unit_map: UnitMap
) -> None:
self.state_data = StateData.from_json(state_data, unit_map)
self.state_data = StateData.from_json(state_data)
self.game = game
self.unit_map = unit_map

View File

@@ -1 +1,4 @@
from .faction import Faction
from .factionloader import FactionLoader
FACTIONS = FactionLoader()

View File

@@ -4,32 +4,33 @@ import itertools
import logging
from dataclasses import dataclass, field
from functools import cached_property
from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING, Type
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
import dcs
from dcs.countries import country_dict
from dcs.unittype import ShipType, StaticType, UnitType as DcsUnitType
from dcs.unittype import ShipType, StaticType
from dcs.unittype import UnitType as DcsUnitType
from game.armedforces.forcegroup import ForceGroup
from game.data.building_data import (
DEFAULT_AVAILABLE_BUILDINGS,
IADS_BUILDINGS,
REQUIRED_BUILDINGS,
WW2_ALLIES_BUILDINGS,
WW2_FREE,
DEFAULT_AVAILABLE_BUILDINGS,
WW2_GERMANY_BUILDINGS,
WW2_FREE,
REQUIRED_BUILDINGS,
IADS_BUILDINGS,
)
from game.data.doctrine import (
COLDWAR_DOCTRINE,
Doctrine,
MODERN_DOCTRINE,
COLDWAR_DOCTRINE,
WWII_DOCTRINE,
)
from game.data.groups import GroupRole
from game.data.units import UnitClass
from game.data.groups import GroupRole
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.shipunittype import ShipUnitType
from game.armedforces.forcegroup import ForceGroup
from game.dcs.unittype import UnitType
if TYPE_CHECKING:
@@ -167,7 +168,7 @@ class Faction:
return sorted(air_defenses)
@classmethod
def from_dict(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.country = json.get("country", "/")
@@ -296,9 +297,6 @@ class Faction:
self.remove_aircraft("VSN_F104G")
self.remove_aircraft("VSN_F104S")
self.remove_aircraft("VSN_F104S_AG")
if not mod_settings.f4_phantom:
self.remove_aircraft("VSN_F4B")
self.remove_aircraft("VSN_F4C")
if not mod_settings.jas39_gripen:
self.remove_aircraft("JAS39Gripen")
self.remove_aircraft("JAS39Gripen_AG")

View File

@@ -0,0 +1,54 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Iterator, List, Optional, Type
from game import persistency
from game.factions.faction import Faction
FACTION_DIRECTORY = Path("./resources/factions/")
class FactionLoader:
def __init__(self) -> None:
self._factions: Optional[Dict[str, Faction]] = None
@property
def factions(self) -> Dict[str, Faction]:
self.initialize()
assert self._factions is not None
return self._factions
def initialize(self) -> None:
if self._factions is None:
self._factions = self.load_factions()
@staticmethod
def find_faction_files_in(path: Path) -> List[Path]:
return [f for f in path.glob("*.json") if f.is_file()]
@classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
files = cls.find_faction_files_in(
FACTION_DIRECTORY
) + cls.find_faction_files_in(user_faction_path)
factions = {}
for f in files:
try:
with f.open("r", encoding="utf-8") as fdata:
data = json.load(fdata)
factions[data["name"]] = Faction.from_json(data)
logging.info("Loaded faction : " + str(f))
except Exception:
logging.exception(f"Unable to load faction : {f}")
return factions
def __getitem__(self, name: str) -> Faction:
return self.factions[name]
def __iter__(self) -> Iterator[str]:
return iter(self.factions.keys())

View File

@@ -1,70 +0,0 @@
from __future__ import annotations
import itertools
import json
import logging
from collections.abc import Iterator
from pathlib import Path
import yaml
from game import persistence
from .faction import Faction
class Factions:
def __init__(self, factions: dict[str, Faction]) -> None:
self.factions = factions
self.campaign_defined_factions: dict[str, Faction] = {}
def get_by_name(self, name: str) -> Faction:
try:
return self.factions[name]
except KeyError:
return self.campaign_defined_factions[name]
def iter_faction_names(self) -> Iterator[str]:
# Campaign defined factions first so they show up at the top of the list in the
# UI.
return itertools.chain(self.campaign_defined_factions, self.factions)
def add_campaign_defined(self, faction: Faction) -> None:
if (
faction.name in self.factions
or faction.name in self.campaign_defined_factions
):
raise KeyError(f"Duplicate faction {faction.name}")
self.campaign_defined_factions[faction.name] = faction
def reset_campaign_defined(self) -> None:
self.campaign_defined_factions = {}
@staticmethod
def iter_faction_files_in(path: Path) -> Iterator[Path]:
yield from path.glob("*.json")
yield from path.glob("*.yaml")
@classmethod
def iter_faction_files(cls) -> Iterator[Path]:
yield from cls.iter_faction_files_in(Path("resources/factions/"))
yield from cls.iter_faction_files_in(
Path(persistence.base_path()) / "Liberation/Factions"
)
@classmethod
def load(cls) -> Factions:
factions = {}
for path in cls.iter_faction_files():
try:
with path.open("r", encoding="utf-8") as fdata:
if path.suffix == ".yaml":
data = yaml.safe_load(fdata)
else:
data = json.load(fdata)
faction = Faction.from_dict(data)
factions[faction.name] = faction
logging.info("Loaded faction from %s", path)
except Exception:
logging.exception(f"Unable to load faction from %s", path)
return Factions(factions)

View File

@@ -5,7 +5,9 @@ import logging
import math
from collections.abc import Iterator
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from uuid import UUID
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
from dcs.country import Country
@@ -15,16 +17,16 @@ from dcs.vehicles import AirDefence
from faker import Faker
from game.ato.closestairfields import ObjectiveDistanceCache
from game.ground_forces.ai_ground_planner import GroundPlanner
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
from game.utils import Distance
from . import naming
from . import naming, persistency
from .ato.flighttype import FlightType
from .campaignloader import CampaignAirWingConfig
from .coalition import Coalition
from .db.gamedb import GameDb
from .infos.information import Information
from .persistence import SaveManager
from .profiling import logged_duration
from .settings import Settings
from .theater import ConflictTheater
@@ -36,8 +38,7 @@ from .theater.theatergroundobject import (
)
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .timeofday import TimeOfDay
from .turnstate import TurnState
from .weather.conditions import Conditions
from .weather import Conditions
if TYPE_CHECKING:
from .ato.airtaaskingorder import AirTaskingOrder
@@ -81,6 +82,12 @@ AWACS_BUDGET_COST = 4
PLAYER_BUDGET_IMPORTANCE_LOG = 2
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2
class Game:
def __init__(
self,
@@ -91,24 +98,23 @@ class Game:
start_date: datetime,
start_time: time | None,
settings: Settings,
lua_plugin_manager: LuaPluginManager,
player_budget: float,
enemy_budget: float,
) -> None:
self.settings = settings
self.lua_plugin_manager = lua_plugin_manager
self.theater = theater
self.turn = 0
# NB: This is the *start* date. It is never updated.
self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.notes = ""
self.ground_planners: dict[UUID, GroundPlanner] = {}
self.informations: list[Information] = []
self.message("Game Start", "-" * 40)
# Culling Zones are for areas around points of interest that contain things we may not wish to cull.
self.__culling_zones: List[Point] = []
self.__destroyed_units: list[dict[str, Union[float, str]]] = []
self.save_manager = SaveManager(self)
self.savepath = ""
self.current_unit_id = 0
self.current_group_id = 0
self.name_generator = naming.namegen
@@ -223,13 +229,7 @@ class Game:
# We need to persist this state so that names generated after game load don't
# conflict with those generated before exit.
naming.namegen = self.name_generator
# The installed plugins may have changed between runs. We need to load the
# current configuration and patch in the options that were previously set.
new_plugin_manager = LuaPluginManager.load()
new_plugin_manager.update_with(self.lua_plugin_manager)
self.lua_plugin_manager = new_plugin_manager
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
self.compute_unculled_zones(GameUpdateEvents())
if not game_still_initializing:
@@ -290,7 +290,7 @@ class Game:
if self.turn > 1:
self.conditions = self.generate_conditions()
def begin_turn_0(self, squadrons_start_full: bool) -> None:
def begin_turn_0(self) -> None:
"""Initialization for the first turn of the game."""
from .sim import GameUpdateEvents
@@ -315,16 +315,12 @@ class Game:
# Rotate the whole TGO with the new heading
tgo.rotate(heading or tgo.heading)
self.blue.preinit_turn_0(squadrons_start_full)
self.red.preinit_turn_0(squadrons_start_full)
# TODO: Check for overfull bases.
self.blue.preinit_turn_0()
self.red.preinit_turn_0()
# We don't need to actually stream events for turn zero because we haven't given
# *any* state to the UI yet, so it will need to do a full draw once we do.
self.initialize_turn(GameUpdateEvents())
def save_last_turn_state(self) -> None:
self.save_manager.save_last_turn()
def pass_turn(self, no_action: bool = False) -> None:
"""Ends the current turn and initializes the new turn.
@@ -336,11 +332,7 @@ class Game:
from .server import EventStream
from .sim import GameUpdateEvents
if no_action:
# Only save the last turn state if the turn was skipped. Otherwise, we'll
# end up saving the game after we've already applied the results, making
# this useless...
self.save_manager.save_last_turn()
persistency.save_last_turn_state(self)
events = GameUpdateEvents()
@@ -353,7 +345,8 @@ class Game:
EventStream.put_nowait(events)
self.save_manager.save_start_of_turn()
# Autosave progress
persistency.autosave(self)
def check_win_loss(self) -> TurnState:
player_airbases = {
@@ -376,10 +369,7 @@ class Game:
self.red.bullseye = Bullseye(player_cp.position)
def initialize_turn(
self,
events: GameUpdateEvents,
for_red: bool = True,
for_blue: bool = True,
self, events: GameUpdateEvents, for_red: bool = True, for_blue: bool = True
) -> None:
"""Performs turn initialization for the specified players.
@@ -431,9 +421,17 @@ class Game:
# Plan Coalition specific turn
if for_blue:
self.blue.initialize_turn(self.turn == 0)
self.blue.initialize_turn()
if for_red:
self.red.initialize_turn(self.turn == 0)
self.red.initialize_turn()
# Plan GroundWar
self.ground_planners = {}
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
# Update cull zones
with logged_duration("Computing culling positions"):

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
import logging
from collections import defaultdict
from datetime import datetime
from typing import Optional, TYPE_CHECKING
from game.theater import ControlPoint
@@ -53,7 +52,7 @@ class GroundUnitOrders:
pending_units = 0
return pending_units
def process(self, game: Game, now: datetime) -> None:
def process(self, game: Game) -> None:
coalition = game.coalition_for(self.destination.captured)
ground_unit_source = self.find_ground_unit_source(game)
if ground_unit_source is None:
@@ -96,20 +95,15 @@ class GroundUnitOrders:
"still tried to transfer units to there"
)
ground_unit_source.base.commission_units(units_needing_transfer)
self.create_transfer(
coalition, ground_unit_source, units_needing_transfer, now
)
self.create_transfer(coalition, ground_unit_source, units_needing_transfer)
def create_transfer(
self,
coalition: Coalition,
source: ControlPoint,
units: dict[GroundUnitType, int],
now: datetime,
) -> None:
coalition.transfers.new_transfer(
TransferOrder(source, self.destination, units), now
)
coalition.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

View File

@@ -1,9 +1,9 @@
from __future__ import annotations
from collections import defaultdict
import itertools
import logging
import pickle
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from typing import Iterator
@@ -13,18 +13,18 @@ import yaml
from dcs import Point
from dcs.unitgroup import StaticGroup
from game import persistence
from game import persistency
from game.data.groups import GroupRole
from game.layout.layout import (
AntiAirLayout,
BuildingLayout,
DefensesLayout,
GroundForceLayout,
LayoutUnit,
NavalLayout,
TgoLayout,
TgoLayoutGroup,
TgoLayoutUnitGroup,
LayoutUnit,
AntiAirLayout,
BuildingLayout,
NavalLayout,
GroundForceLayout,
DefensesLayout,
)
from game.layout.layoutmapping import LayoutMapping
from game.profiling import logged_duration
@@ -63,7 +63,7 @@ class LayoutLoader:
"""This will load all pre-loaded layouts from a pickle file.
If pickle can not be loaded it will import and dump the layouts"""
# We use a pickle for performance reasons. Importing takes many seconds
file = Path(persistence.base_path()) / LAYOUT_DUMP
file = Path(persistency.base_path()) / LAYOUT_DUMP
if file.is_file():
# Load from pickle if existing
with file.open("rb") as f:
@@ -106,7 +106,7 @@ class LayoutLoader:
self._dump_templates()
def _dump_templates(self) -> None:
file = Path(persistence.base_path()) / LAYOUT_DUMP
file = Path(persistency.base_path()) / LAYOUT_DUMP
dump = (VERSION, self._layouts)
with file.open("wb") as fdata:
pickle.dump(dump, fdata)

View File

@@ -16,7 +16,7 @@ def init_logging(version: str) -> None:
log_config = resources / "default_logging.yaml"
if (custom_log_config := resources / "logging.yaml").exists():
log_config = custom_log_config
with log_config.open(encoding="utf-8") as log_file:
with log_config.open() as log_file:
logging.config.dictConfig(yaml.safe_load(log_file))
logging.info(f"DCS Liberation {version}")

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