Compare commits

..

59 Commits

Author SHA1 Message Date
Raffson
801e9efe8c Fix heli spawn/landing at FOB/FARP
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2719.

(cherry picked from commit 34de855b8f889b72406c101d0cea385988e24bf9)
(cherry picked from commit b41ef0ab13)
2023-02-05 14:41:47 -08:00
Dan Albert
5708fe625b Don't generate runway data for heliports.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2710.

(cherry picked from commit c33a0d5deb)
2023-02-05 14:41:47 -08:00
Dan Albert
1dda0c9497 Pin version of black used in GHA.
Black rolls out style changes every year, and using "stable" means that
the check run on PRs might start formatting differently than we do
locally, or require a reformat of the codebase to make a PR submittable.

Pin to the version that we've been using. We should update to 23 at some
point, but we want to do that deliberately.

(cherry picked from commit 937bacacb7)
2023-02-05 12:15:50 -08:00
Dan Albert
56d7641251 Fix line endings.
(cherry-picked from commit e8824e5d03)
2023-02-05 12:15:50 -08:00
Dan Albert
938d6b4bdf Fix invalid tanker planning.
All three refueling missions share a common task type and differentiate
their behavior based on the type and allegiance of the package target.
This means that if the theater commander identifies a carrier as the
best location for a theater refueling task, a recovery tanker will be
planned by mistake.

If this happens on a sunken carrier, mission generation will fail
because a recovery tanker cannot be generated for a sunken carrier.

Fix the crash and the misplanned theater tanker by preventing the
commander from choosing fleet control points as refueling targets.

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

(cherry picked from commit eea98b01f6)
2023-01-28 14:05:57 -08:00
Dan Albert
5caf07f476 Fix unit ID of the KS-19.
It seems like this unit has never worked because of the unit ID
mismatch.

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

(cherry picked from commit 0df268f331)
2023-01-28 12:10:38 -08:00
Dan Albert
d1d4fde6a3 Update game version to 6.1.1. 2023-01-28 12:10:38 -08:00
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
676 changed files with 9016 additions and 13760 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:
- 6.1.1
- 6.0.0
- Development build
- type: textarea
attributes:
@@ -53,9 +53,9 @@ 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` (or `.liberation.zip`, for 7.x) file found in
`%USERPROFILE%/Saved Games/DCS/Liberation/Saves`), so most bugs filed
without saved games will be closed without investigation.
`.liberation` file found in `%USERPROFILE%/Saved
Games/DCS/Liberation/Saves`), so most bugs filed without saved games
will be closed without investigation.
Other useful files to include are:
@@ -74,9 +74,7 @@ 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. **If you
include this file, also include `last_turn.liberation`** (unless the
save is from 7.x or newer, which includes that information in the save
automatically).
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:
- 6.1.1
- 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

@@ -15,24 +15,4 @@ jobs:
- name: run tests
run: |
./venv/scripts/activate
pytest --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

3
.gitignore vendored
View File

@@ -1,14 +1,11 @@
*.pyc
__pycache__
build/**
# Sphinx
docs/_build
resources/payloads/*.lua
venv
.DS_Store
.vscode/settings.json
dist/**
/.coverage
# 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

@@ -14,12 +14,7 @@
## 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)
@@ -34,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,54 +1,3 @@
# 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

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,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,8 +0,0 @@
coverage:
status:
patch:
default:
informational: false
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 = "7.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
@@ -149,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
@@ -215,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
@@ -230,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:
@@ -256,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:
@@ -289,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
@@ -91,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
)
@@ -115,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

@@ -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
@@ -59,42 +59,37 @@ class SweepFlightPlan(LoiterFlightPlan):
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
@@ -68,20 +68,20 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
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

@@ -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

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

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,41 +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"]
)
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"],
)
@@ -153,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

@@ -7,6 +7,7 @@ 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
@@ -16,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
@@ -37,7 +38,7 @@ from .theater.theatergroundobject import (
)
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .timeofday import TimeOfDay
from .weather.conditions import Conditions
from .weather import Conditions
if TYPE_CHECKING:
from .ato.airtaaskingorder import AirTaskingOrder
@@ -97,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
@@ -229,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:
@@ -296,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
@@ -321,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.
@@ -342,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()
@@ -359,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 = {
@@ -382,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.
@@ -437,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}")

View File

@@ -10,11 +10,12 @@ from dcs.unit import Skill
from dcs.unitgroup import FlyingGroup
from game.ato import Flight, FlightType
from game.ato.flightplans.shiprecoverytanker import RecoveryTankerFlightPlan
from game.callsigns import callsign_for_support_unit
from game.data.weapons import Pylon, WeaponType as WeaponTypeEnum
from game.missiongenerator.missiondata import MissionData, AwacsInfo, TankerInfo
from game.missiongenerator.lasercoderegistry import LaserCodeRegistry
from game.missiongenerator.logisticsgenerator import LogisticsGenerator
from game.missiongenerator.missiondata import AwacsInfo, MissionData, TankerInfo
from game.radio.radios import RadioFrequency, RadioRegistry
from game.radio.tacan import TacanBand, TacanRegistry, TacanUsage
from game.runways import RunwayData
@@ -24,6 +25,8 @@ from .aircraftbehavior import AircraftBehavior
from .aircraftpainter import AircraftPainter
from .flightdata import FlightData
from .waypoints import WaypointGenerator
from ...ato.flightplans.aewc import AewcFlightPlan
from ...ato.flightplans.theaterrefueling import TheaterRefuelingFlightPlan
if TYPE_CHECKING:
from game import Game
@@ -79,18 +82,14 @@ class FlightGroupConfigurator:
if self.flight.flight_type in [
FlightType.TRANSPORT,
FlightType.AIR_ASSAULT,
] and self.game.lua_plugin_manager.is_plugin_enabled("ctld"):
] and self.game.settings.plugin_option("ctld"):
transfer = None
if self.flight.flight_type == FlightType.TRANSPORT:
coalition = self.game.coalition_for(player=self.flight.blue)
transfer = coalition.transfers.transfer_for_flight(self.flight)
self.mission_data.logistics.append(
LogisticsGenerator(
self.flight,
self.group,
self.mission,
self.game.lua_plugin_manager,
transfer,
self.flight, self.group, self.mission, self.game.settings, transfer
).generate_logistics()
)
@@ -98,6 +97,7 @@ class FlightGroupConfigurator:
self.flight,
self.group,
self.mission,
self.game.conditions.start_time,
self.time,
self.game.settings,
self.mission_data,
@@ -149,7 +149,7 @@ class FlightGroupConfigurator:
def register_air_support(self, channel: RadioFrequency) -> None:
callsign = callsign_for_support_unit(self.group)
if self.flight.flight_type is FlightType.AEWC:
if isinstance(self.flight.flight_plan, AewcFlightPlan):
self.mission_data.awacs.append(
AwacsInfo(
group_name=str(self.group.name),
@@ -161,7 +161,9 @@ class FlightGroupConfigurator:
blue=self.flight.departure.captured,
)
)
elif self.flight.flight_type is FlightType.REFUELING:
elif isinstance(
self.flight.flight_plan, TheaterRefuelingFlightPlan
) or isinstance(self.flight.flight_plan, RecoveryTankerFlightPlan):
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir)
self.mission_data.tankers.append(
TankerInfo(
@@ -170,8 +172,8 @@ class FlightGroupConfigurator:
variant=self.flight.unit_type.name,
freq=channel,
tacan=tacan,
start_time=self.flight.flight_plan.mission_begin_on_station_time,
end_time=self.flight.flight_plan.mission_departure_time,
start_time=self.flight.flight_plan.patrol_start_time,
end_time=self.flight.flight_plan.patrol_end_time,
blue=self.flight.departure.captured,
)
)

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from datetime import datetime
from datetime import timedelta
from typing import Any, Iterable, Union
from dcs import Mission
@@ -28,20 +28,18 @@ class PydcsWaypointBuilder:
group: FlyingGroup[Any],
flight: Flight,
mission: Mission,
now: datetime,
elapsed_mission_time: timedelta,
mission_data: MissionData,
unit_map: UnitMap,
generated_waypoint_idx: int,
) -> None:
self.waypoint = waypoint
self.group = group
self.package = flight.package
self.flight = flight
self.mission = mission
self.now = now
self.elapsed_mission_time = elapsed_mission_time
self.mission_data = mission_data
self.unit_map = unit_map
self.generated_waypoint_idx = generated_waypoint_idx
def build(self) -> MovingPoint:
waypoint = self.group.add_waypoint(
@@ -70,10 +68,10 @@ class PydcsWaypointBuilder:
def add_tasks(self, waypoint: MovingPoint) -> None:
pass
def set_waypoint_tot(self, waypoint: MovingPoint, tot: datetime) -> None:
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
self.waypoint.tot = tot
if not self._viggen_client_tot():
waypoint.ETA = int((tot - self.now).total_seconds())
waypoint.ETA = int((tot - self.elapsed_mission_time).total_seconds())
waypoint.ETA_locked = True
waypoint.speed_locked = False

View File

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

View File

@@ -1,5 +1,5 @@
from dcs.point import MovingPoint
from dcs.task import ActivateBeaconCommand, RecoveryTanker, Tanker
from dcs.task import ActivateBeaconCommand, RecoveryTanker
from game.ato import FlightType
from game.utils import feet, knots
@@ -10,12 +10,6 @@ class RecoveryTankerBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
assert self.flight.flight_type == FlightType.REFUELING
# Tanker task required in conjunction with RecoveryTanker task.
# See link below for details.
# https://github.com/dcs-liberation/dcs_liberation/issues/2771
waypoint.add_task(Tanker())
group_id = self._get_carrier_group_id()
speed = knots(250).meters_per_second
altitude = feet(6000).meters

View File

@@ -1,12 +1,9 @@
from dcs.point import MovingPoint
from dcs.task import RefuelingTaskAction
from game.ato import FlightType
from .pydcswaypointbuilder import PydcsWaypointBuilder
class RefuelPointBuilder(PydcsWaypointBuilder):
def add_tasks(self, waypoint: MovingPoint) -> None:
if self.package.has_flight_with_task(FlightType.REFUELING):
waypoint.add_task(RefuelingTaskAction())
waypoint.add_task(RefuelingTaskAction())
return super().add_tasks(waypoint)

View File

@@ -3,15 +3,13 @@ import logging
from dcs.point import MovingPoint
from dcs.task import (
AttackGroup,
ControlledTask,
EngageGroup,
Expend,
OptECMUsing,
SwitchWaypoint,
WeaponType as DcsWeaponType,
)
from game.ato.flightstate import InFlight
from game.data.weapons import WeaponType
from game.theater import TheaterGroundObject
from .pydcswaypointbuilder import PydcsWaypointBuilder
@@ -47,28 +45,8 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
engage_task.params["groupAttack"] = True
engage_task.params["expend"] = Expend.All.value
waypoint.tasks.append(engage_task)
elif self.flight.loadout.has_weapon_of_type(WeaponType.DECOY):
# Special handling for DECOY weapon types:
# - Specify that DECOY weapon type is used in AttackGroup task so that
# the flight actually launches the decoy. See link below for details
# https://github.com/dcs-liberation/dcs_liberation/issues/2780
# - Set a stop condition of 120 seconds so that the flight does not continue
# press the engagement as a DCS limitation means the RTB on winchester
# does not work well with decoys. See link below for details.
# https://github.com/dcs-liberation/dcs_liberation/issues/2781
# This stop condition will allow the SwitchWaypoint task defined below
# to kick in.
attack_task = AttackGroup(
miz_group.id,
weapon_type=DcsWeaponType.Decoy,
group_attack=True,
expend=Expend.All,
)
attack_task_control = ControlledTask(attack_task)
attack_task_control.stop_after_duration(120)
waypoint.tasks.append(attack_task_control)
else:
# All non ARM and non DECOY types will use the normal AttackGroup Task
# All non ARM types like Decoys will use the normal AttackGroup Task
attack_task = AttackGroup(
miz_group.id,
weapon_type=DcsWeaponType.Guided,
@@ -80,14 +58,3 @@ class SeadIngressBuilder(PydcsWaypointBuilder):
# Preemptively use ECM to better avoid getting swatted.
ecm_option = OptECMUsing(value=OptECMUsing.Values.UseIfDetectedLockByRadar)
waypoint.tasks.append(ecm_option)
# For DECOY type flights, setup a waypoint task to skip the target waypoint after
# the attack task is complete. This is achieved using a switch waypoint task from the
# INGRESS point to the SPLIT point. This tasking prevents the flights continuing to
# overfly the target. See link below for the details of this issue
# https://github.com/dcs-liberation/dcs_liberation/issues/2781
if self.flight.loadout.has_weapon_of_type(WeaponType.DECOY):
switch_waypoint_task = SwitchWaypoint(
self.generated_waypoint_idx, self.generated_waypoint_idx + 2
)
waypoint.tasks.append(switch_waypoint_task)

View File

@@ -25,13 +25,13 @@ from game.settings import Settings
from game.unitmap import UnitMap
from game.utils import pairwise
from .baiingress import BaiIngressBuilder
from .landingzone import LandingZoneBuilder
from .casingress import CasIngressBuilder
from .deadingress import DeadIngressBuilder
from .default import DefaultWaypointBuilder
from .holdpoint import HoldPointBuilder
from .joinpoint import JoinPointBuilder
from .landingpoint import LandingPointBuilder
from .landingzone import LandingZoneBuilder
from .ocaaircraftingress import OcaAircraftIngressBuilder
from .ocarunwayingress import OcaRunwayIngressBuilder
from .pydcswaypointbuilder import PydcsWaypointBuilder, TARGET_WAYPOINTS
@@ -50,6 +50,7 @@ class WaypointGenerator:
flight: Flight,
group: FlyingGroup[Any],
mission: Mission,
turn_start_time: datetime,
time: datetime,
settings: Settings,
mission_data: MissionData,
@@ -58,6 +59,7 @@ class WaypointGenerator:
self.flight = flight
self.group = group
self.mission = mission
self.elapsed_mission_time = time - turn_start_time
self.time = time
self.settings = settings
self.mission_data = mission_data
@@ -80,7 +82,7 @@ class WaypointGenerator:
# us, but we do need to configure the tasks for it so that mid-
# mission aircraft starting at a waypoint with tasks behave
# correctly.
self.builder_for_waypoint(point, 1).add_tasks(self.group.points[0])
self.builder_for_waypoint(point).add_tasks(self.group.points[0])
if not self.flight.state.has_passed_waypoint(point):
filtered_points.append(point)
else:
@@ -110,10 +112,7 @@ class WaypointGenerator:
]
for idx, point in enumerate(filtered_points):
# We add 2 to idx to get the generated waypoint index as
# 1) pydcs seems to decrement the index by 1 and
# 2) DCS starts the first waypoint at index 1 as 0 is the starting position
self.builder_for_waypoint(point, idx + 2).build()
self.builder_for_waypoint(point).build()
# Set here rather than when the FlightData is created so they waypoints
# have their TOTs and fuel minimums set. Once we're more confident in our fuel
@@ -123,9 +122,7 @@ class WaypointGenerator:
self._estimate_min_fuel_for(waypoints)
return mission_start_time, waypoints
def builder_for_waypoint(
self, waypoint: FlightWaypoint, generated_waypoint_index: int
) -> PydcsWaypointBuilder:
def builder_for_waypoint(self, waypoint: FlightWaypoint) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.INGRESS_BAI: BaiIngressBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
@@ -153,10 +150,9 @@ class WaypointGenerator:
self.group,
self.flight,
self.mission,
self.time,
self.elapsed_mission_time,
self.mission_data,
self.unit_map,
generated_waypoint_index,
)
def _estimate_min_fuel_for(self, waypoints: list[FlightWaypoint]) -> None:
@@ -186,29 +182,12 @@ class WaypointGenerator:
a.min_fuel = min_fuel
def set_takeoff_time(self, waypoint: FlightWaypoint) -> timedelta:
force_delay = False
if isinstance(self.flight.state, WaitingForStart):
delay = self.flight.state.time_remaining(self.time)
elif (
# The first two clauses capture the flight states that we want to adjust. We
# don't want to delay any flights that are already in flight or on the
# runway.
not self.flight.state.in_flight
and self.flight.state.spawn_type is not StartType.RUNWAY
and self.flight.departure.is_fleet
and not self.flight.client_count
):
# https://github.com/dcs-liberation/dcs_liberation/issues/1309
# Without a delay, AI aircraft will be spawned on the sixpack, which other
# AI planes of course want to taxi through, deadlocking the carrier deck.
# Delaying AI carrier deck spawns by one second for some reason causes DCS
# to spawn those aircraft elsewhere, avoiding the traffic jam.
delay = timedelta(seconds=1)
force_delay = True
else:
delay = timedelta()
if force_delay or self.should_delay_flight():
if self.should_delay_flight():
if self.should_activate_late():
# Late activation causes the aircraft to not be spawned
# until triggered.

View File

@@ -15,7 +15,7 @@ from dcs.task import (
)
from dcs.unittype import UnitType
from game.ato import FlightType
from game.ato.ai_flight_planner_db import AEWC_CAPABLE
from game.callsigns import callsign_for_support_unit
from game.naming import namegen
from game.radio.radios import RadioRegistry
@@ -165,7 +165,7 @@ class AirSupportGenerator:
possible_awacs = [
a
for a in self.game.faction_for(player=True).aircrafts
if a.capable_of(FlightType.AEWC)
if a in AEWC_CAPABLE
]
if not possible_awacs:

View File

@@ -5,6 +5,7 @@ from __future__ import annotations
import os
from dataclasses import dataclass
from datetime import timedelta
from typing import Dict, List, TYPE_CHECKING
from dcs.mission import Mission
@@ -126,9 +127,11 @@ class MissionInfoGenerator:
def format_waypoint_time(waypoint: FlightWaypoint, depart_prefix: str) -> str:
if waypoint.tot is not None:
return f"{waypoint.tot.time()} "
time = timedelta(seconds=int(waypoint.tot.total_seconds()))
return f"T+{time} "
elif waypoint.departure_time is not None:
return f"{depart_prefix} {waypoint.departure_time.time()} "
time = timedelta(seconds=int(waypoint.departure_time.total_seconds()))
return f"{depart_prefix} T+{time} "
return ""

View File

@@ -3,11 +3,7 @@ from typing import Optional
from dcs.mission import Mission
from game.weather.atmosphericconditions import AtmosphericConditions
from game.weather.clouds import Clouds
from game.weather.conditions import Conditions
from game.weather.fog import Fog
from game.weather.wind import WindConditions
from game.weather import Clouds, Fog, Conditions, WindConditions, AtmosphericConditions
class EnvironmentGenerator:

View File

@@ -141,7 +141,7 @@ class FlotGenerator:
# If the option fc3LaserCode is enabled, force all JTAC
# laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs.
# Otherwise use 1688 for the first JTAC, 1687 for the second etc.
if self.game.lua_plugin_manager.is_option_enabled("ctld", "fc3LaserCode"):
if self.game.settings.plugins["plugins.ctld.fc3LaserCode"]:
code = 1113
else:
code = self.laser_code_registry.get_next_laser_code()
@@ -215,7 +215,7 @@ class FlotGenerator:
u = random.choices(
manpads, weights=[m.spawn_weight for m in manpads]
)[0]
infantry_group = self.mission.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, u),
u.dcs_unit_type,
@@ -224,7 +224,6 @@ class FlotGenerator:
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
infantry_group.hidden_on_mfd = True
return
possible_infantry_units = set(faction.infantry_with_class(UnitClass.INFANTRY))
@@ -241,7 +240,7 @@ class FlotGenerator:
weights=[u.spawn_weight for u in infantry_choices],
k=INFANTRY_GROUP_SIZE,
)
infantry_group = self.mission.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, units[0]),
units[0].dcs_unit_type,
@@ -250,11 +249,10 @@ class FlotGenerator:
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
infantry_group.hidden_on_mfd = True
for unit in units[1:]:
position = infantry_position.random_point_within(55, 5)
infantry_group = self.mission.vehicle_group(
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, unit),
unit.dcs_unit_type,
@@ -263,7 +261,6 @@ class FlotGenerator:
heading=forward_heading.degrees,
move_formation=PointAction.OffRoad,
)
infantry_group.hidden_on_mfd = True
def _set_reform_waypoint(
self, dcs_group: VehicleGroup, forward_heading: Heading
@@ -774,7 +771,6 @@ class FlotGenerator:
group_size=count,
heading=heading.degrees,
)
group.hidden_on_mfd = True
self.unit_map.add_front_line_units(group, cp, unit_type)

View File

@@ -44,7 +44,7 @@ from game.runways import RunwayData
from game.theater import TheaterGroundObject, TheaterUnit
from game.theater.bullseye import Bullseye
from game.utils import Distance, UnitSystem, meters, mps, pounds
from game.weather.weather import Weather
from game.weather import Weather
from .aircraft.flightdata import FlightData
from .airsupportgenerator import AwacsInfo, TankerInfo
from .briefinggenerator import CommInfo, JtacInfo, MissionInfoGenerator
@@ -254,11 +254,11 @@ class FlightPlanBuilder:
]
)
@staticmethod
def _format_time(time: datetime.datetime | None) -> str:
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None:
return ""
return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
local_time = self.start_time + time
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
def _format_alt(self, alt: Distance) -> str:
return f"{self.units.distance_short(alt):.0f}"
@@ -583,11 +583,11 @@ class SupportPage(KneeboardPage):
)
return f"{channel_name}\n{frequency}"
@staticmethod
def _format_time(time: datetime.datetime | None) -> str:
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
if time is None:
return ""
return f"{time.strftime('%H:%M:%S')}{'Z' if time.tzinfo is not None else ''}"
local_time = self.start_time + time
return f"{local_time.strftime('%H:%M:%S')}{'Z' if local_time.tzinfo is not None else ''}"
class SeadTaskPage(KneeboardPage):

View File

@@ -8,7 +8,7 @@ from game.ato import Flight
from game.ato.flightplans.airassault import AirAssaultFlightPlan
from game.ato.flightwaypointtype import FlightWaypointType
from game.missiongenerator.missiondata import CargoInfo, LogisticsInfo
from game.plugins import LuaPluginManager
from game.settings.settings import Settings
from game.transfers import TransferOrder
ZONE_RADIUS = 300
@@ -21,14 +21,14 @@ class LogisticsGenerator:
flight: Flight,
group: FlyingGroup[Any],
mission: Mission,
lua_plugin_manager: LuaPluginManager,
settings: Settings,
transfer: Optional[TransferOrder] = None,
) -> None:
self.flight = flight
self.group = group
self.transfer = transfer
self.mission = mission
self.lua_plugin_manager = lua_plugin_manager
self.settings = settings
def generate_logistics(self) -> LogisticsInfo:
# Add Logisitcs info for the flight
@@ -89,8 +89,8 @@ class LogisticsGenerator:
for cargo_unit_type, amount in self.transfer.units.items()
]
if pickup_point is not None and self.lua_plugin_manager.is_option_enabled(
"ctld", "logisticunit"
if pickup_point is not None and self.settings.plugin_option(
"ctld.logisticunit"
):
# Spawn logisticsunit at pickup zones
country = self.mission.country(self.flight.country)

View File

@@ -4,7 +4,7 @@ import logging
import os
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Optional, TYPE_CHECKING
from typing import TYPE_CHECKING, Optional
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
@@ -13,9 +13,11 @@ from dcs.triggers import TriggerStart
from game.ato import FlightType
from game.dcs.aircrafttype import AircraftType
from game.plugins import LuaPluginManager
from game.theater import TheaterGroundObject
from game.theater.iadsnetwork.iadsrole import IadsRole
from game.utils import escape_string_for_lua
from .missiondata import MissionData
if TYPE_CHECKING:
@@ -205,26 +207,16 @@ class LuaGenerator:
self.mission.triggerrules.triggers.append(trigger)
def inject_lua_trigger(self, contents: str, comment: str) -> None:
"""Creates the trigger for running the text script at mission start."""
trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(contents)))
self.mission.triggerrules.triggers.append(trigger)
def bypass_plugin_script(self, mnemonic: str) -> None:
"""Records a script has having been intentionally ignored.
It's not clear why this is needed. It looks like this might be a holdover from
when mission generation was driven by a singleton and we needed to avoid
double-loading plugins if the generator ran twice (take off, cancel, take off)?
For now, this prevents duplicates from being handled twice.
"""
self.plugin_scripts.append(mnemonic)
def inject_plugin_script(
self, plugin_mnemonic: str, script: str, script_mnemonic: str
) -> None:
""" "Creates a trigger for running the script file at mission start."""
if script_mnemonic in self.plugin_scripts:
logging.debug(f"Skipping already loaded {script} for {plugin_mnemonic}")
return
@@ -245,7 +237,7 @@ class LuaGenerator:
self.mission.triggerrules.triggers.append(trigger)
def inject_plugins(self) -> None:
for plugin in self.game.lua_plugin_manager.iter_plugins():
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(self)
plugin.inject_configuration(self)

View File

@@ -1,11 +1,11 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from datetime import timedelta
from typing import Optional, TYPE_CHECKING
from game.dcs.aircrafttype import AircraftType
from game.missiongenerator.aircraft.flightdata import FlightData
from game.runways import RunwayData
if TYPE_CHECKING:
@@ -31,8 +31,8 @@ class AwacsInfo(GroupInfo):
"""AWACS information for the kneeboard."""
depature_location: Optional[str]
start_time: datetime | None
end_time: datetime | None
start_time: Optional[timedelta]
end_time: Optional[timedelta]
@dataclass
@@ -41,8 +41,8 @@ class TankerInfo(GroupInfo):
variant: str
tacan: TacanChannel
start_time: datetime | None
end_time: datetime | None
start_time: Optional[timedelta]
end_time: Optional[timedelta]
@dataclass

View File

@@ -13,7 +13,6 @@ from dcs.countries import country_dict
from game.atcdata import AtcData
from game.dcs.beacons import Beacons
from game.dcs.helpers import unit_type_from_name
from game.ground_forces.ai_ground_planner import GroundPlanner
from game.missiongenerator.aircraft.aircraftgenerator import (
AircraftGenerator,
)
@@ -43,8 +42,7 @@ from .visualsgenerator import VisualsGenerator
if TYPE_CHECKING:
from game import Game
from game.theater import ControlPoint
from game.ground_forces.ai_ground_planner import CombatGroup
COMBINED_ARMS_SLOTS = 1
@@ -191,33 +189,8 @@ class MissionGenerator:
# No need to reserve ILS or TACAN because those are in the
# beacon list.
def _find_combat_groups_between(
self,
planners: dict[ControlPoint, GroundPlanner],
origin: ControlPoint,
target: ControlPoint,
) -> list[CombatGroup]:
try:
planner = planners[origin]
except KeyError as ex:
raise KeyError(f"No ground planner found at {origin}") from ex
try:
return planner.units_per_cp[target.id]
except KeyError as ex:
raise KeyError(
f"Ground planner at {origin} does not target {target}"
) from ex
def generate_ground_conflicts(self) -> None:
"""Generate FLOTs and JTACs for each active front line."""
planners: dict[ControlPoint, GroundPlanner] = {}
for control_point in self.game.theater.controlpoints:
if control_point.has_frontline:
planner = GroundPlanner(control_point, self.game)
planners[control_point] = planner
planner.plan_groundwar()
for front_line in self.game.theater.conflicts():
player_cp = front_line.blue_cp
enemy_cp = front_line.red_cp
@@ -225,12 +198,16 @@ class MissionGenerator:
front_line, self.game.theater
)
# Generate frontline ops
player_gp = self.game.ground_planners[player_cp.id].units_per_cp[
enemy_cp.id
]
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
ground_conflict_gen = FlotGenerator(
self.mission,
conflict,
self.game,
self._find_combat_groups_between(planners, player_cp, enemy_cp),
self._find_combat_groups_between(planners, enemy_cp, player_cp),
player_gp,
enemy_gp,
player_cp.stances[enemy_cp.id],
enemy_cp.stances[player_cp.id],
self.unit_map,

View File

@@ -103,7 +103,7 @@ class GroundObjectGenerator:
# Special handling for scenery objects
self.add_trigger_zone_for_scenery(unit)
if (
self.game.lua_plugin_manager.is_plugin_enabled("skynetiads")
self.game.settings.plugin_option("skynetiads")
and self.game.theater.iads_network.advanced_iads
and isinstance(group, IadsGroundGroup)
and group.iads_role.participate

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