mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
335 Commits
develop-6.
...
develop-8.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4da4956df8 | ||
|
|
618159c1fa | ||
|
|
d8c662e7f8 | ||
|
|
12c41b57c9 | ||
|
|
85a27845bc | ||
|
|
e3f6347e16 | ||
|
|
fffe1b6e94 | ||
|
|
5a7a730e23 | ||
|
|
576f777320 | ||
|
|
87f7fe5307 | ||
|
|
e1434378a8 | ||
|
|
e03b0d99d8 | ||
|
|
e4eb3dec1b | ||
|
|
b365016496 | ||
|
|
c359b3f7fc | ||
|
|
302613069e | ||
|
|
5a22b62e3b | ||
|
|
001e7dfed9 | ||
|
|
cf985d3d37 | ||
|
|
1044a1f45f | ||
|
|
9fe31859d3 | ||
|
|
09417322e7 | ||
|
|
136a9b5f02 | ||
|
|
02f22d4930 | ||
|
|
ca133b9fd1 | ||
|
|
b1af6dfbe1 | ||
|
|
647d1f57f9 | ||
|
|
b250fe2f1e | ||
|
|
3be57bf6bb | ||
|
|
3c8d0b023e | ||
|
|
adceb3a224 | ||
|
|
ab02cd34c5 | ||
|
|
c74b603d81 | ||
|
|
5815401e73 | ||
|
|
f463fe50f2 | ||
|
|
f60bf62897 | ||
|
|
9d43eb8f03 | ||
|
|
8f0ca08b89 | ||
|
|
0534f66b30 | ||
|
|
1162e0aa29 | ||
|
|
36c4bb88be | ||
|
|
dc6624a159 | ||
|
|
8b55331326 | ||
|
|
33ca77e3d1 | ||
|
|
b92b01b245 | ||
|
|
b18b371904 | ||
|
|
9c7e16d121 | ||
|
|
87e869d963 | ||
|
|
4a059a4f8b | ||
|
|
674254e55b | ||
|
|
9fd0e06c05 | ||
|
|
ecaf84ea55 | ||
|
|
e4028cb013 | ||
|
|
c45ac50370 | ||
|
|
6640609caf | ||
|
|
e44b6b416b | ||
|
|
8a861d3da5 | ||
|
|
380d6551be | ||
|
|
4cb035b955 | ||
|
|
e50be9bbde | ||
|
|
ec49a10135 | ||
|
|
23e3630169 | ||
|
|
e20ab5fbc0 | ||
|
|
4fd2bb131b | ||
|
|
42a7102948 | ||
|
|
d271ff17c2 | ||
|
|
cb61dfccc4 | ||
|
|
56f93c76eb | ||
|
|
36cb3a386c | ||
|
|
c25e830e6c | ||
|
|
5d08990cd0 | ||
|
|
2a45cd8899 | ||
|
|
90b880ec3c | ||
|
|
5f0c570d65 | ||
|
|
ce102fcc50 | ||
|
|
30c792c15a | ||
|
|
2f45b856d6 | ||
|
|
31d2b756ab | ||
|
|
b5cf889c09 | ||
|
|
19958f91ca | ||
|
|
c775a898a4 | ||
|
|
535244f6f3 | ||
|
|
9d1d3bdcfa | ||
|
|
36eef2b1b9 | ||
|
|
7788425c5c | ||
|
|
ee0c21b3e5 | ||
|
|
54cd619f75 | ||
|
|
051940e23c | ||
|
|
4fbd7defa3 | ||
|
|
90bda9383d | ||
|
|
7798e2970c | ||
|
|
410c25b331 | ||
|
|
cff74525d6 | ||
|
|
8b7f107044 | ||
|
|
c365a0d739 | ||
|
|
1f4fd0fd04 | ||
|
|
4bb60cb500 | ||
|
|
fe96a415be | ||
|
|
6699289bf7 | ||
|
|
a85d3243fb | ||
|
|
7f2607cf08 | ||
|
|
e50ee976ed | ||
|
|
29ffb526f2 | ||
|
|
e024013093 | ||
|
|
257dabe4fa | ||
|
|
406fb61fa4 | ||
|
|
49dfa95c61 | ||
|
|
c80e5b259f | ||
|
|
64e2213f28 | ||
|
|
ced93afd49 | ||
|
|
f719a5ec34 | ||
|
|
6f4ac1dc39 | ||
|
|
f831c8efdd | ||
|
|
e3c6b03603 | ||
|
|
7a2e8279cd | ||
|
|
24e72475b4 | ||
|
|
f10350dac4 | ||
|
|
f068976749 | ||
|
|
4b4c45e90f | ||
|
|
527eac1f4a | ||
|
|
92c3087187 | ||
|
|
8dc3fca290 | ||
|
|
0d18b57074 | ||
|
|
b745e7c8ec | ||
|
|
800ca598ef | ||
|
|
212813e31d | ||
|
|
571fe21d57 | ||
|
|
cd952312b7 | ||
|
|
23982fdac6 | ||
|
|
8724b458a8 | ||
|
|
ef64899701 | ||
|
|
51e4dc5c22 | ||
|
|
ac5edeb936 | ||
|
|
f7364d04ed | ||
|
|
483bf73213 | ||
|
|
0b87f90d4f | ||
|
|
202fe1109b | ||
|
|
5a8863b07e | ||
|
|
889e1f5da2 | ||
|
|
418d78f99b | ||
|
|
a4b5bc198c | ||
|
|
fcb2f21d36 | ||
|
|
15abf3d6fe | ||
|
|
a8b7aca4fb | ||
|
|
799dbfa99c | ||
|
|
eb31a0f038 | ||
|
|
78e2da9196 | ||
|
|
ca96a232f0 | ||
|
|
03671bbfb0 | ||
|
|
97c4168d13 | ||
|
|
e0edfa68b1 | ||
|
|
97c238a4bb | ||
|
|
a6c5b03212 | ||
|
|
5b148a74aa | ||
|
|
33242048e7 | ||
|
|
4f7932ad8a | ||
|
|
8158cc7112 | ||
|
|
50d7a3e46f | ||
|
|
b6b9a22668 | ||
|
|
bd2ec12e0f | ||
|
|
752a90cddb | ||
|
|
004594639e | ||
|
|
6943adf6df | ||
|
|
5ad57d2878 | ||
|
|
acb2d01d92 | ||
|
|
15fa73a514 | ||
|
|
7f94b34277 | ||
|
|
4d2ed64a70 | ||
|
|
e444761059 | ||
|
|
c9e4b5eba4 | ||
|
|
c14c7cc73d | ||
|
|
5e459c2390 | ||
|
|
4ee6de2c84 | ||
|
|
a7d2eca209 | ||
|
|
57a4a7c282 | ||
|
|
b4c5236d8b | ||
|
|
de2a779715 | ||
|
|
352c2ddc56 | ||
|
|
76e6aff9d7 | ||
|
|
b4c02767ac | ||
|
|
aa2a888ed0 | ||
|
|
b50d82feff | ||
|
|
cce9592ac8 | ||
|
|
b69def652e | ||
|
|
6df83485e1 | ||
|
|
e297fcbff8 | ||
|
|
2f2ebff674 | ||
|
|
06b74c4ca6 | ||
|
|
7ddfc5e5ad | ||
|
|
f86709ebd0 | ||
|
|
7266de42f5 | ||
|
|
47831d43b5 | ||
|
|
cf47dd82d7 | ||
|
|
081c97583b | ||
|
|
77f1706cbb | ||
|
|
664efa3ace | ||
|
|
b6059f692e | ||
|
|
4bf8f25d31 | ||
|
|
e2c6d6788c | ||
|
|
1c20bc3966 | ||
|
|
c31d76ec83 | ||
|
|
ada8f9f8ee | ||
|
|
1b72598803 | ||
|
|
4bc8bf52e7 | ||
|
|
0d257a2c3b | ||
|
|
0ba602d3aa | ||
|
|
38d18ba767 | ||
|
|
94b8aa7213 | ||
|
|
1ac36d03da | ||
|
|
dca256364a | ||
|
|
652e7d8d7b | ||
|
|
42e9a6294b | ||
|
|
f3d2952579 | ||
|
|
ee1d4cd3e4 | ||
|
|
4e067eaaa8 | ||
|
|
4a0975b21b | ||
|
|
dd9ad2f0be | ||
|
|
d1fe267072 | ||
|
|
28ba100864 | ||
|
|
99ea06c0d5 | ||
|
|
fa070b2126 | ||
|
|
5405632434 | ||
|
|
ca2cec5d7d | ||
|
|
8150176fc6 | ||
|
|
5c72e0754a | ||
|
|
af2e195f90 | ||
|
|
8523c11357 | ||
|
|
b860d72c2d | ||
|
|
bcb7d059c0 | ||
|
|
bc0dacf974 | ||
|
|
2d9b0177ec | ||
|
|
2fed84c676 | ||
|
|
a73c06223d | ||
|
|
e129c02109 | ||
|
|
67dae80b76 | ||
|
|
088b69a6ef | ||
|
|
db64f37a95 | ||
|
|
af6c42f49b | ||
|
|
4503170075 | ||
|
|
c07f343d0e | ||
|
|
725f6c55a5 | ||
|
|
364742a98b | ||
|
|
7b35a749e2 | ||
|
|
23ac510d26 | ||
|
|
ba10298dbc | ||
|
|
c6a8aeac1d | ||
|
|
b41ef0ab13 | ||
|
|
c33a0d5deb | ||
|
|
70b9d4c174 | ||
|
|
1462bedd97 | ||
|
|
937bacacb7 | ||
|
|
e8824e5d03 | ||
|
|
e030cfebb8 | ||
|
|
eea98b01f6 | ||
|
|
f8c1d291ed | ||
|
|
0df268f331 | ||
|
|
be2ad226f4 | ||
|
|
a4df23361e | ||
|
|
0f34946127 | ||
|
|
575470ae1b | ||
|
|
31c59e7380 | ||
|
|
4b542b70ae | ||
|
|
9cb641bddf | ||
|
|
7a8b3591cd | ||
|
|
fd2ba6b2b2 | ||
|
|
ac6cc39616 | ||
|
|
0be9e1985a | ||
|
|
a1af4e563a | ||
|
|
7167e84a8f | ||
|
|
7eeb84de47 | ||
|
|
45aabf369b | ||
|
|
e396a21791 | ||
|
|
c6635a4885 | ||
|
|
306971230b | ||
|
|
e0c13846a7 | ||
|
|
4aa42e6573 | ||
|
|
24a04fb8c6 | ||
|
|
321de8d4ec | ||
|
|
5db82f733f | ||
|
|
d65fbf299c | ||
|
|
7c2bb3bd85 | ||
|
|
f9903f1e19 | ||
|
|
4a4935f165 | ||
|
|
09f92cc5e4 | ||
|
|
887e5997c2 | ||
|
|
f88a50dd07 | ||
|
|
3f12a5ae3d | ||
|
|
f2946817bf | ||
|
|
935a9b0631 | ||
|
|
c0dc411102 | ||
|
|
55037626a4 | ||
|
|
fc3e72bacf | ||
|
|
0fd0f0e7c0 | ||
|
|
22503d4e95 | ||
|
|
de9236e93a | ||
|
|
66523301aa | ||
|
|
9a81121ac1 | ||
|
|
a245ba80c3 | ||
|
|
9a1860fc5e | ||
|
|
f3f5ab70ea | ||
|
|
6ce7638fdc | ||
|
|
7f916d55e7 | ||
|
|
43ea019091 | ||
|
|
6025cad716 | ||
|
|
9365aea724 | ||
|
|
28859a8a9c | ||
|
|
304fd7ea80 | ||
|
|
5e345263a7 | ||
|
|
445ee25bbf | ||
|
|
20937815f8 | ||
|
|
3863b8ef40 | ||
|
|
d91ccaa70f | ||
|
|
7673ca5481 | ||
|
|
774a37a7d2 | ||
|
|
905094f63f | ||
|
|
fd5b7ba49d | ||
|
|
1b828b95b3 | ||
|
|
54546aaefb | ||
|
|
ded5fc8b1d | ||
|
|
68fc4f6950 | ||
|
|
5d07238ab8 | ||
|
|
5e7e5e2636 | ||
|
|
ca5c0055d1 | ||
|
|
e208df16b2 | ||
|
|
11632b0ef1 | ||
|
|
b0bc46f539 | ||
|
|
627ed45065 | ||
|
|
fc9ad5b519 | ||
|
|
40ddad1d9a | ||
|
|
eb997db703 | ||
|
|
e53dc5b80b | ||
|
|
ab64655f05 | ||
|
|
e1b530e4fc | ||
|
|
4414853e45 | ||
|
|
35adcd2c7f |
8
.coveragerc
Normal file
8
.coveragerc
Normal file
@@ -0,0 +1,8 @@
|
||||
[report]
|
||||
exclude_lines =
|
||||
pragma: no cover
|
||||
if TYPE_CHECKING:
|
||||
|
||||
[run]
|
||||
branch = True
|
||||
source = game,pydcs_extensions,qt_ui,resources/tools
|
||||
76
.gitattributes
vendored
76
.gitattributes
vendored
@@ -14,3 +14,79 @@
|
||||
*.pyo binary export-ignore
|
||||
*.pyd binary
|
||||
unshipped_data/arcgis_maps/ filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# https://github.com/alexkaratarakis/gitattributes/blob/master/Common.gitattributes
|
||||
# Documents
|
||||
*.bibtex text diff=bibtex
|
||||
*.doc diff=astextplain
|
||||
*.DOC diff=astextplain
|
||||
*.docx diff=astextplain
|
||||
*.DOCX diff=astextplain
|
||||
*.dot diff=astextplain
|
||||
*.DOT diff=astextplain
|
||||
*.pdf diff=astextplain
|
||||
*.PDF diff=astextplain
|
||||
*.rtf diff=astextplain
|
||||
*.RTF diff=astextplain
|
||||
*.md text diff=markdown
|
||||
*.mdx text diff=markdown
|
||||
*.tex text diff=tex
|
||||
*.adoc text
|
||||
*.textile text
|
||||
*.mustache text
|
||||
*.csv text
|
||||
*.tab text
|
||||
*.tsv text
|
||||
*.txt text
|
||||
*.sql text
|
||||
*.epub diff=astextplain
|
||||
|
||||
# Graphics
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.tif binary
|
||||
*.tiff binary
|
||||
*.ico binary
|
||||
# SVG treated as text by default.
|
||||
*.svg text
|
||||
# If you want to treat it as binary,
|
||||
# use the following line instead.
|
||||
# *.svg binary
|
||||
*.eps binary
|
||||
|
||||
# Scripts
|
||||
*.bash text eol=lf
|
||||
*.fish text eol=lf
|
||||
*.sh text eol=lf
|
||||
*.zsh text eol=lf
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
*.cmd text eol=crlf
|
||||
*.ps1 text eol=crlf
|
||||
|
||||
# Serialisation
|
||||
*.json text
|
||||
*.toml text
|
||||
*.xml text
|
||||
*.yaml text
|
||||
*.yml text
|
||||
|
||||
# Archives
|
||||
*.7z binary
|
||||
*.gz binary
|
||||
*.tar binary
|
||||
*.tgz binary
|
||||
*.zip binary
|
||||
|
||||
# Text files where line endings should be preserved
|
||||
*.patch -text
|
||||
|
||||
#
|
||||
# Exclude files from exporting
|
||||
#
|
||||
|
||||
.gitattributes export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitkeep export-ignore
|
||||
22
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
22
.github/ISSUE_TEMPLATE/bug-report.yml
vendored
@@ -31,12 +31,13 @@ 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:
|
||||
- 5.2.0
|
||||
- 7.1.0
|
||||
- Development build
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Build information
|
||||
description: The build information from the Help -> Report an issue window.
|
||||
description:
|
||||
The build information from the Help -> Report an issue window.
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description
|
||||
@@ -52,18 +53,17 @@ 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` file found in
|
||||
`%USERPROFILE%/Saved Games/DCS/Liberation/Saves`), so most bugs filed
|
||||
without saved games will be closed without investigation.
|
||||
`.liberation.zip` 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:
|
||||
|
||||
|
||||
The Liberation log file. The log file is located at
|
||||
`<Liberation install directory>/logs/liberation.log`. The log often
|
||||
includes data about non-fatal errors that could be the root cause of the
|
||||
problem.
|
||||
The Liberation log file. The log file is located at `<Liberation install
|
||||
directory>/logs/liberation.log`. The log often includes data about
|
||||
non-fatal errors that could be the root cause of the problem.
|
||||
|
||||
|
||||
The `liberation_nextturn.miz` or a track file. This should always be
|
||||
@@ -76,8 +76,8 @@ body:
|
||||
investigating any issues with end-of-turn results processing.
|
||||
|
||||
|
||||
You can attach files to the bug by dragging and dropping the file
|
||||
into this text box. GitHub will not allow uploads of all file types, so
|
||||
You can attach files to the bug by dragging and dropping the file into
|
||||
this text box. GitHub will not allow uploads of all file types, so
|
||||
attach a zip of the files if needed.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
21
.github/ISSUE_TEMPLATE/new-game-bug.yml
vendored
@@ -39,12 +39,13 @@ 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:
|
||||
- 5.2.0
|
||||
- 7.1.0
|
||||
- Development build
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Build information
|
||||
description: The build information from the Help -> Report an issue window.
|
||||
description:
|
||||
The build information from the Help -> Report an issue window.
|
||||
- type: input
|
||||
attributes:
|
||||
label: Campaign name
|
||||
@@ -59,8 +60,8 @@ body:
|
||||
label: Blue faction
|
||||
description: >
|
||||
The name of the blue faction you selected. If the bug only occurs with a
|
||||
custom faction (or modifications to a stock faction), upload the
|
||||
faction file as an attachment to the bug description field.
|
||||
custom faction (or modifications to a stock faction), upload the faction
|
||||
file as an attachment to the bug description field.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -68,8 +69,8 @@ body:
|
||||
label: Red faction
|
||||
description: >
|
||||
The name of the red faction you selected. If the bug only occurs with a
|
||||
custom faction (or modifications to a stock faction), upload the
|
||||
faction file as an attachment to the bug description field.
|
||||
custom faction (or modifications to a stock faction), upload the faction
|
||||
file as an attachment to the bug description field.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -102,11 +103,11 @@ body:
|
||||
attributes:
|
||||
label: Log file
|
||||
description: >
|
||||
Attach the Liberation log file. The log file is located at
|
||||
`<Liberation install directory>/logs/liberation.log`.
|
||||
Attach the Liberation log file. The log file is located at `<Liberation
|
||||
install directory>/logs/liberation.log`.
|
||||
|
||||
|
||||
You can attach files to the bug by dragging and dropping the file
|
||||
into this text box.
|
||||
You can attach files to the bug by dragging and dropping the file into
|
||||
this text box.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -6,7 +6,7 @@ runs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: "3.10"
|
||||
python-version: "3.11"
|
||||
cache: pip
|
||||
|
||||
- name: Install environment
|
||||
@@ -19,5 +19,3 @@ 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
|
||||
|
||||
1
.github/workflows/lint.yml
vendored
1
.github/workflows/lint.yml
vendored
@@ -11,6 +11,7 @@ jobs:
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
version: ~=22.12
|
||||
src: "."
|
||||
options: "--check"
|
||||
|
||||
|
||||
22
.github/workflows/test.yml
vendored
22
.github/workflows/test.yml
vendored
@@ -15,4 +15,24 @@ jobs:
|
||||
- name: run tests
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
pytest tests
|
||||
pytest --cov --cov-report=xml tests
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
ts-tests:
|
||||
name: Typescript tests
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up JS environment
|
||||
uses: ./.github/actions/setup-liberation-js
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
cd client
|
||||
npm test -- --coverage
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,11 +1,15 @@
|
||||
*.pyc
|
||||
__pycache__
|
||||
build/**
|
||||
# Sphinx
|
||||
docs/_build
|
||||
resources/payloads/*.lua
|
||||
venv
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
/.coverage
|
||||
/coverage.xml
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 22.6.0
|
||||
rev: 22.12.0
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
13
.readthedocs.yaml
Normal file
13
.readthedocs.yaml
Normal file
@@ -0,0 +1,13 @@
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
@@ -8,13 +8,19 @@
|
||||
|
||||
[](https://discord.gg/bKrtrkJ)
|
||||
|
||||
[](https://codecov.io/gh/dcs-liberation/dcs_liberation)
|
||||
[](https://github.com/dcs-liberation/dcs_liberation)
|
||||
[](https://github.com/dcs-liberation/dcs_liberation/issues)
|
||||

|
||||
|
||||
## 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.
|
||||
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.**
|
||||
|
||||

|
||||
|
||||
@@ -29,7 +35,6 @@ 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/)
|
||||
|
||||
|
||||
137
changelog.md
137
changelog.md
@@ -1,3 +1,140 @@
|
||||
# 8.1.0
|
||||
|
||||
Saves from 8.0.0 are compatible with 8.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
|
||||
* **[UI]** Flight loadout/properties tab is now scrollable.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed liveries for premade squadrons all being off-by-one.
|
||||
* **[UI]** Fixed numbering of waypoints in the map and flight dialog (first waypoint is now 0 rather than 1).
|
||||
|
||||
# 8.0.0
|
||||
|
||||
Saves from 7.x are not compatible with 8.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map.
|
||||
* **[UI]** Limited size of overfull airbase display and added scrollbar.
|
||||
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
|
||||
* **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays.
|
||||
* **[UI]** Added basic game over dialog.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fix bug introduced in 7.0 where map strike target deaths are no longer tracked.
|
||||
* **[Mission Generation]** Fix crash during mission generation caused by out of date DCS data for the Gazelle.
|
||||
* **[Mission Generation]** Fix crash during mission generation when DCS beacon data is inconsistent.
|
||||
|
||||
# 7.1.0
|
||||
|
||||
Saves from 7.0.0 are compatible with 7.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for Normandy 2 airfields.
|
||||
* **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer.
|
||||
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
|
||||
* **[Mission Generation]** Added settings for controlling number of tactical commander, observer, JTAC, and game master slots.
|
||||
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
|
||||
* **[New Game Wizard]** The air wing configuration dialog will check for and reject overfull airbases before continuing when the new squadron rules are used.
|
||||
* **[New Game Wizard]** Closing the air wing configuration dialog will now cancel and return to the new game wizard rather than reverting changes and continuing.
|
||||
* **[New Game Wizard]** A warning will be displayed next to the new squadron rules button if the campaign predates the new rules and will likely require user intervention before continuing.
|
||||
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mission Planning]** BAI is once again plannable against missile sites and coastal defense batteries.
|
||||
* **[UI]** Fixed formatting of departure time in flight details dialog.
|
||||
|
||||
# 7.0.0
|
||||
|
||||
Saves from 6.x are not compatible with 7.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.5.40170.
|
||||
* **[Engine]** Saved games are now a zip file of save assets for easier bug reporting. The new extension is .liberation.zip. Drag and drop that file into bug reports.
|
||||
* **[Campaign]** Added options to limit squadron sizes and to begin all squadrons at maximum strength. Maximum squadron size is defined during air wing configuration with default values provided by the campaign.
|
||||
* **[Campaign]** Added handling for more DCS death events. This probably does not catch any deaths that weren't previously tracked, but it should record them sooner, which will improve results for game crashes or other early exits.
|
||||
* **[Campaign AI]** The campaign AI now prefers fulfilling missions with squadrons which have a matching primary task. Previously distance from target held a stronger influence than task preference. Primary tasks for squadrons are set by campaign designers but are user-configurable.
|
||||
* **[Flight Planning]** Package TOT and composition can be modified after advancing time in Liberation.
|
||||
* **[Mission Generation]** Units on the front line are now hidden on MFDs.
|
||||
* **[Mission Generation]** Preset radio channels will now be configured for both A-10C modules.
|
||||
* **[Mission Generation]** The A-10C II now uses separate radios for inter- and intra-flight comms (similar to other modern aircraft).
|
||||
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
|
||||
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
|
||||
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
|
||||
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
|
||||
* **[Modding]** Add support for VSN F-4B and F-4C mod.
|
||||
* **[Modding]** Added support for AI C-47 mod.
|
||||
* **[Modding]** Custom factions can now be defined in YAML as well as JSON. JSON support may be removed in the future if having both formats causes confusion.
|
||||
* **[Modding]** Campaigns which require custom factions can now define those factions directly in the campaign YAML. See Operation Aliied Sword for an example.
|
||||
* **[Modding]** The `mission_types` field in squadron files has been removed. Squadron task capability is now determined by airframe, and the auto-assignable list has always been overridden by the campaign settings.
|
||||
* **[Modding]** Aircraft task capabilities and preferred aircraft for each task are now moddable in the aircraft unit yaml files. Each aircraft has a weight per task. Higher weights are given higher preference.
|
||||
* **[Modding]** Wind speed generation inputs are now moddable. See https://dcs-liberation.rtfd.io/en/latest/modding/weather.html.
|
||||
* **[New Game Wizard]** Choices for some options will be remembered for the next new game. Not all settings will be preserved, as many are campaign dependent.
|
||||
* **[New Game Wizard]** Lua plugins can now be set while creating a new game.
|
||||
* **[New Game Wizard]** Squadrons can be directly replaced with a preset during air wing configuration rather than needing to remove and create a new squadron.
|
||||
* **[New Game Wizard]** Squadron liveries can now be selected during air wing configuration.
|
||||
* **[Squadrons]** Squadron-specific mission capability lists no longer restrict players from assigning missions outside the squadron's preferences.
|
||||
* **[UI]** The orientation of objects like SAMs, EWRs, garrisons, and ships can now be manually adjusted.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed a longstanding bug where oversized airlifts could corrupt a save with empty convoys.
|
||||
* **[Campaign]** Aircraft with built-in TGPs but without an external pod will no longer degrade automatic loadouts to iron bombs.
|
||||
* **[Engine]** Fixed crash in startup caused by a corrupted Liberation preferences file.
|
||||
* **[Flight Planning]** AEW&C missions are now plannable over FOBs and LHAs.
|
||||
* **[Flight Planning]** BAI is no longer plannable against buildings.
|
||||
* **[Modding]** Fixed an issue where Falklands campaigns created or edited with new versions of DCS could not be loaded.
|
||||
* **[Modding]** Fixed decoding of campaign yaml files to use UTF-8 rather than the system locale's default. It's now possible to use "Bf 109 K-4 Kurfürst" as a preferred aircraft type.
|
||||
* **[Mission Generation]** Planes will no longer spawn in helipads that are not also designated for fixed wing parking.
|
||||
* **[Mission Generation]** Potentially an issue where ground war planning game state could become corrupted, preventing mission generation.
|
||||
* **[Mission Generation]** Refueling tasks will now only be created for flights that have a tanker in their package.
|
||||
* **[Mission Generation]** Fixed missing Tanker task on recovery tanker missions.
|
||||
* **[UI]** Fixed error when resetting air wing configuration during game setup.
|
||||
* **[UI]** Fixed flight plan recreation when changing mission type with "Recreate as" flight options.
|
||||
* **[UI]** Fixed failure to launch UI when Liberation persistent preferences file was corrupt.
|
||||
|
||||
# 6.1.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Data]** Fixed unit ID for the KS-19 AAA. KS-19 would not previously generate correctly in missions. A new game is required for this fix to take effect.
|
||||
* **[Flight Planning]** Automatic flight planning will no longer accidentally plan a recovery tanker instead of a theater refueling package. This fixes a potential crash during mission generation when opfor plans a refueling task at a sunk carrier. You'll need to skip the current turn to force opfor to replan their flights to get the fix.
|
||||
* **[Mission Generation]** Using heliports (airports without any runways) will no longer cause mission generation to fail.
|
||||
* **[Mission Generation]** Prevent helicopters from spawning into collisions at FARPs when more than one flight uses the same FARP.
|
||||
|
||||
# 6.1.0
|
||||
|
||||
Saves from 6.0.0 are compatible with 6.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.8.1.34437, including Blackshark 3.
|
||||
* **[Factions]** Defaulted bluefor modern to use Georgian and Ukrainian liveries for Russian aircraft.
|
||||
* **[Factions]** Added Peru.
|
||||
* **[Flight Planning]** AEW&C and Refueling flights are now plannable on LHA carriers.
|
||||
* **[Flight Planning]** Refueling flights planned on aircraft carriers will act as a recovery tanker for the carrier.
|
||||
* **[Loadouts]** Adjusted F-15E loadouts.
|
||||
* **[Mission Generation]** The previous turn will now be saved as last_turn.liberation when submitting mission results. This is often essential for debugging bug reports. **Include this file in the bug report whenever it is available.**
|
||||
* **[Modding]** Added support for the HMS Ariadne, Achilles, and Castle class.
|
||||
* **[Modding]** Added HMS Invincible to the game data as a helicopter carrier.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Flight Planning]** Fixes CAS flights not having landing waypoints.
|
||||
* **[Mission Generation]** Airbase and FOB capture is no longer blocked by grounded aircraft / helicopters.
|
||||
* **[Squadrons]** Fixed the livery for the VF-33 F-14A squadron.
|
||||
* **[Theaters]** Fixed Channel campaigns not having data for land/sea/obstacle boundaries, causing front lines to extend into forests and water. Requires a new campaign to get the fix.
|
||||
* **[UI]** Fixed an issue where manual submit of mission results did not end the mission correctly.
|
||||
|
||||
# 6.0.0
|
||||
|
||||
Saves from 5.x are not compatible with 6.0.
|
||||
|
||||
149
client/package-lock.json
generated
149
client/package-lock.json
generated
@@ -41,6 +41,7 @@
|
||||
"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",
|
||||
@@ -3914,9 +3915,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sideway/formula": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sideway/pinpoint": {
|
||||
@@ -5623,9 +5624,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -6204,9 +6205,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/babel-loader/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -8412,9 +8413,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.1.tgz",
|
||||
"integrity": "sha512-jdyZMwCQ5Oj4c5+BTnkxPgDZO/BJzh/ADDmKebayyzNwjVX1AFCeGkOfxNx0mHi2+8BKC5VxUYiw3TIvoT7vhw==",
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
|
||||
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -10670,9 +10671,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/http-deceiver": {
|
||||
@@ -10816,7 +10817,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": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=",
|
||||
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"harmony-reflect": "^1.4.6"
|
||||
@@ -13481,12 +13482,6 @@
|
||||
"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",
|
||||
@@ -13519,13 +13514,10 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/json5": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"bin": {
|
||||
"json5": "lib/cli.js"
|
||||
},
|
||||
@@ -19401,9 +19393,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths/node_modules/json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -19856,9 +19848,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -19896,9 +19888,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.69.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz",
|
||||
"integrity": "sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A==",
|
||||
"version": "5.76.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
|
||||
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
@@ -19906,24 +19898,24 @@
|
||||
"@webassemblyjs/ast": "1.11.1",
|
||||
"@webassemblyjs/wasm-edit": "1.11.1",
|
||||
"@webassemblyjs/wasm-parser": "1.11.1",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-import-assertions": "^1.7.6",
|
||||
"browserslist": "^4.14.5",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"enhanced-resolve": "^5.10.0",
|
||||
"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-better-errors": "^1.0.2",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"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.3.1",
|
||||
"watchpack": "^2.4.0",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
"bin": {
|
||||
@@ -23678,9 +23670,9 @@
|
||||
}
|
||||
},
|
||||
"@sideway/formula": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz",
|
||||
"integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==",
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz",
|
||||
"integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==",
|
||||
"dev": true
|
||||
},
|
||||
"@sideway/pinpoint": {
|
||||
@@ -25020,9 +25012,9 @@
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.0.tgz",
|
||||
"integrity": "sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ==",
|
||||
"version": "8.8.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
|
||||
"integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn-globals": {
|
||||
@@ -25449,9 +25441,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -27135,9 +27127,9 @@
|
||||
}
|
||||
},
|
||||
"enhanced-resolve": {
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.9.1.tgz",
|
||||
"integrity": "sha512-jdyZMwCQ5Oj4c5+BTnkxPgDZO/BJzh/ADDmKebayyzNwjVX1AFCeGkOfxNx0mHi2+8BKC5VxUYiw3TIvoT7vhw==",
|
||||
"version": "5.12.0",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
|
||||
"integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"graceful-fs": "^4.2.4",
|
||||
@@ -28835,9 +28827,9 @@
|
||||
}
|
||||
},
|
||||
"http-cache-semantics": {
|
||||
"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==",
|
||||
"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==",
|
||||
"dev": true
|
||||
},
|
||||
"http-deceiver": {
|
||||
@@ -28947,7 +28939,7 @@
|
||||
"identity-obj-proxy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz",
|
||||
"integrity": "sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ=",
|
||||
"integrity": "sha512-00n6YnVHKrinT9t0d9+5yZC6UBNJANpYEQvL2LlX6Ab9lnmxzIRcEmTPuyGScvl1+jKuCICX1Z0Ab1pPKKdikA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"harmony-reflect": "^1.4.6"
|
||||
@@ -30892,12 +30884,6 @@
|
||||
"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",
|
||||
@@ -30930,13 +30916,10 @@
|
||||
"optional": true
|
||||
},
|
||||
"json5": {
|
||||
"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"
|
||||
}
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
||||
"dev": true
|
||||
},
|
||||
"jsonfile": {
|
||||
"version": "6.1.0",
|
||||
@@ -35297,9 +35280,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.0"
|
||||
@@ -35647,9 +35630,9 @@
|
||||
}
|
||||
},
|
||||
"watchpack": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.3.1.tgz",
|
||||
"integrity": "sha512-x0t0JuydIo8qCNctdDrn1OzH/qDzk2+rdCOC3YzumZ42fiMqmQ7T3xQurykYMhYfHaPHTp4ZxAx2NfUo1K6QaA==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
|
||||
"integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
@@ -35681,9 +35664,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"webpack": {
|
||||
"version": "5.69.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.69.1.tgz",
|
||||
"integrity": "sha512-+VyvOSJXZMT2V5vLzOnDuMz5GxEqLk7hKWQ56YxPW/PQRUuKimPqmEIJOx8jHYeyo65pKbapbW464mvsKbaj4A==",
|
||||
"version": "5.76.1",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz",
|
||||
"integrity": "sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-scope": "^3.7.3",
|
||||
@@ -35691,24 +35674,24 @@
|
||||
"@webassemblyjs/ast": "1.11.1",
|
||||
"@webassemblyjs/wasm-edit": "1.11.1",
|
||||
"@webassemblyjs/wasm-parser": "1.11.1",
|
||||
"acorn": "^8.4.1",
|
||||
"acorn": "^8.7.1",
|
||||
"acorn-import-assertions": "^1.7.6",
|
||||
"browserslist": "^4.14.5",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.8.3",
|
||||
"enhanced-resolve": "^5.10.0",
|
||||
"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-better-errors": "^1.0.2",
|
||||
"json-parse-even-better-errors": "^2.3.1",
|
||||
"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.3.1",
|
||||
"watchpack": "^2.4.0",
|
||||
"webpack-sources": "^3.2.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -69,9 +69,18 @@
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import App from "./App";
|
||||
import { store } from "./app/store";
|
||||
import { setupStore } from "./app/store";
|
||||
import { render } from "@testing-library/react";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
test("app renders", () => {
|
||||
render(
|
||||
<Provider store={store}>
|
||||
<Provider store={setupStore()}>
|
||||
<App />
|
||||
</Provider>
|
||||
);
|
||||
|
||||
@@ -3,36 +3,48 @@ import combatReducer from "../api/combatSlice";
|
||||
import controlPointsReducer from "../api/controlPointsSlice";
|
||||
import flightsReducer from "../api/flightsSlice";
|
||||
import frontLinesReducer from "../api/frontLinesSlice";
|
||||
import iadsNetworkReducer from "../api/iadsNetworkSlice";
|
||||
import mapReducer from "../api/mapSlice";
|
||||
import navMeshReducer from "../api/navMeshSlice";
|
||||
import supplyRoutesReducer from "../api/supplyRoutesSlice";
|
||||
import tgosReducer from "../api/tgosSlice";
|
||||
import iadsNetworkReducer from "../api/iadsNetworkSlice";
|
||||
import threatZonesReducer from "../api/threatZonesSlice";
|
||||
import unculledZonesReducer from "../api/unculledZonesSlice";
|
||||
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
|
||||
import unculledZonesReducer from "../api/unculledZonesSlice";
|
||||
import {
|
||||
Action,
|
||||
PreloadedState,
|
||||
ThunkAction,
|
||||
combineReducers,
|
||||
configureStore,
|
||||
} from "@reduxjs/toolkit";
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
combat: combatReducer,
|
||||
controlPoints: controlPointsReducer,
|
||||
flights: flightsReducer,
|
||||
frontLines: frontLinesReducer,
|
||||
map: mapReducer,
|
||||
navmeshes: navMeshReducer,
|
||||
supplyRoutes: supplyRoutesReducer,
|
||||
iadsNetwork: iadsNetworkReducer,
|
||||
tgos: tgosReducer,
|
||||
threatZones: threatZonesReducer,
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
unculledZones: unculledZonesReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(baseApi.middleware),
|
||||
const rootReducer = combineReducers({
|
||||
combat: combatReducer,
|
||||
controlPoints: controlPointsReducer,
|
||||
flights: flightsReducer,
|
||||
frontLines: frontLinesReducer,
|
||||
map: mapReducer,
|
||||
navmeshes: navMeshReducer,
|
||||
supplyRoutes: supplyRoutesReducer,
|
||||
iadsNetwork: iadsNetworkReducer,
|
||||
tgos: tgosReducer,
|
||||
threatZones: threatZonesReducer,
|
||||
[baseApi.reducerPath]: baseApi.reducer,
|
||||
unculledZones: unculledZonesReducer,
|
||||
});
|
||||
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export function setupStore(preloadedState?: PreloadedState<RootState>) {
|
||||
return configureStore({
|
||||
reducer: rootReducer,
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat(baseApi.middleware),
|
||||
preloadedState: preloadedState,
|
||||
});
|
||||
}
|
||||
|
||||
export type AppStore = ReturnType<typeof setupStore>;
|
||||
export type AppDispatch = AppStore["dispatch"];
|
||||
export type RootState = ReturnType<typeof rootReducer>;
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
|
||||
53
client/src/components/aircraft/Aircraft.test.tsx
Normal file
53
client/src/components/aircraft/Aircraft.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
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),
|
||||
})
|
||||
);
|
||||
});
|
||||
53
client/src/components/aircraftlayer/AircraftLayer.test.tsx
Normal file
53
client/src/components/aircraftlayer/AircraftLayer.test.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import AircraftLayer from "./AircraftLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockLayerGroup = jest.fn();
|
||||
const mockMarker = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Marker: (props: any) => {
|
||||
mockMarker(props);
|
||||
},
|
||||
}));
|
||||
|
||||
test("layer is empty by default", async () => {
|
||||
renderWithProviders(<AircraftLayer />);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockMarker).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("layer has aircraft if non-empty", async () => {
|
||||
renderWithProviders(<AircraftLayer />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: false,
|
||||
sidc: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockMarker).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import AirDefenseRangeLayer, { colorFor } from "./AirDefenseRangeLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockLayerGroup = jest.fn();
|
||||
const mockCircle = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Circle: (props: any) => {
|
||||
mockCircle(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("colorFor", () => {
|
||||
it("has a unique color for each configuration", () => {
|
||||
const params = [
|
||||
[false, false],
|
||||
[false, true],
|
||||
[true, false],
|
||||
[true, true],
|
||||
];
|
||||
var colors = new Set<string>();
|
||||
for (const [blue, detection] of params) {
|
||||
colors.add(colorFor(blue, detection));
|
||||
}
|
||||
expect(colors.size).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AirDefenseRangeLayer", () => {
|
||||
it("draws nothing when there are no TGOs", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} />);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not draw wrong range types", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
tgos: {
|
||||
tgos: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
control_point_name: "Bar",
|
||||
category: "AA",
|
||||
blue: false,
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
units: [],
|
||||
threat_ranges: [],
|
||||
detection_ranges: [20],
|
||||
dead: false,
|
||||
sidc: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("draws threat ranges", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
tgos: {
|
||||
tgos: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
control_point_name: "Bar",
|
||||
category: "AA",
|
||||
blue: true,
|
||||
position: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
units: [],
|
||||
threat_ranges: [10],
|
||||
detection_ranges: [20],
|
||||
dead: false,
|
||||
sidc: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
radius: 10,
|
||||
color: colorFor(true, false),
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("draws detection ranges", () => {
|
||||
renderWithProviders(<AirDefenseRangeLayer blue={true} detection />, {
|
||||
preloadedState: {
|
||||
tgos: {
|
||||
tgos: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
control_point_name: "Bar",
|
||||
category: "AA",
|
||||
blue: true,
|
||||
position: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
units: [],
|
||||
threat_ranges: [10],
|
||||
detection_ranges: [20],
|
||||
dead: false,
|
||||
sidc: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 10,
|
||||
lng: 20,
|
||||
},
|
||||
radius: 20,
|
||||
color: colorFor(true, true),
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ interface TgoRangeCirclesProps {
|
||||
detection?: boolean;
|
||||
}
|
||||
|
||||
function colorFor(blue: boolean, detection: boolean) {
|
||||
export function colorFor(blue: boolean, detection: boolean) {
|
||||
if (blue) {
|
||||
return detection ? "#bb89ff" : "#0084ff";
|
||||
}
|
||||
|
||||
132
client/src/components/combat/Combat.test.tsx
Normal file
132
client/src/components/combat/Combat.test.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import Combat from "./Combat";
|
||||
import { LatLng } from "leaflet";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockPolygon = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
Polygon: (props: any) => {
|
||||
mockPolygon(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("Combat", () => {
|
||||
describe("footprint", () => {
|
||||
it("is not interactive", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: null,
|
||||
target_positions: null,
|
||||
footprint: [[new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)]],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolygon).toBeCalledWith(
|
||||
expect.objectContaining({ interactive: false })
|
||||
);
|
||||
});
|
||||
|
||||
// Fails because we don't handle multi-poly combat footprints correctly.
|
||||
it.skip("renders single polygons", () => {
|
||||
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: null,
|
||||
target_positions: null,
|
||||
footprint: [boundary],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolygon).toBeCalledWith(
|
||||
expect.objectContaining({ positions: boundary })
|
||||
);
|
||||
});
|
||||
|
||||
// Fails because we don't handle multi-poly combat footprints correctly.
|
||||
it.skip("renders multiple polygons", () => {
|
||||
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: null,
|
||||
target_positions: null,
|
||||
footprint: [boundary, boundary],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolygon).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("lines", () => {
|
||||
it("is not interactive", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(1, 0)],
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toBeCalledWith(
|
||||
expect.objectContaining({ interactive: false })
|
||||
);
|
||||
});
|
||||
|
||||
it("renders single line", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1)],
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: [new LatLng(0, 0), new LatLng(0, 1)],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("renders multiple lines", () => {
|
||||
renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1), new LatLng(1, 0)],
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toBeCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("renders nothing if no footprint or targets", () => {
|
||||
const { container } = renderWithProviders(
|
||||
<Combat
|
||||
combat={{
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: null,
|
||||
footprint: null,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
});
|
||||
48
client/src/components/combatlayer/CombatLayer.test.tsx
Normal file
48
client/src/components/combatlayer/CombatLayer.test.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import CombatLayer from "./CombatLayer";
|
||||
import { LatLng } from "leaflet";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CombatLayer", () => {
|
||||
it("renders each combat", () => {
|
||||
renderWithProviders(<CombatLayer />, {
|
||||
preloadedState: {
|
||||
combat: {
|
||||
combat: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1)],
|
||||
footprint: null,
|
||||
},
|
||||
bar: {
|
||||
id: "foo",
|
||||
flight_position: new LatLng(0, 0),
|
||||
target_positions: [new LatLng(0, 1)],
|
||||
footprint: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("renders LayerGroup but no contents if no combat", () => {
|
||||
renderWithProviders(<CombatLayer />);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import ControlPointsLayer from "./ControlPointsLayer";
|
||||
import { LatLng } from "leaflet";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockMarker = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Marker: (props: any) => {
|
||||
mockMarker(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("ControlPointsLayer", () => {
|
||||
it("renders each control point", () => {
|
||||
renderWithProviders(<ControlPointsLayer />, {
|
||||
preloadedState: {
|
||||
controlPoints: {
|
||||
controlPoints: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
name: "Foo",
|
||||
blue: true,
|
||||
position: new LatLng(0, 0),
|
||||
mobile: false,
|
||||
sidc: "",
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
name: "Bar",
|
||||
blue: false,
|
||||
position: new LatLng(1, 0),
|
||||
mobile: false,
|
||||
sidc: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockMarker).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it("renders LayerGroup but no contents if no combat", () => {
|
||||
renderWithProviders(<ControlPointsLayer />);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
expect(mockMarker).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import CullingExclusionZones from "./CullingExclusionZones";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockCircle = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
const mockLayerControlOverlay = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
LayersControl: {
|
||||
Overlay: (props: PropsWithChildren<any>) => {
|
||||
mockLayerControlOverlay(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
},
|
||||
Circle: (props: any) => {
|
||||
mockCircle(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("CullingExclusionZones", () => {
|
||||
it("is empty there are no exclusion zones", () => {
|
||||
renderWithProviders(<CullingExclusionZones />);
|
||||
expect(mockCircle).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerControlOverlay).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe("zone circles", () => {
|
||||
it("are drawn in the correct locations", () => {
|
||||
renderWithProviders(<CullingExclusionZones />, {
|
||||
preloadedState: {
|
||||
unculledZones: {
|
||||
zones: [
|
||||
{
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
radius: 10,
|
||||
},
|
||||
{
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
radius: 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockCircle).toHaveBeenCalledTimes(2);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
radius: 10,
|
||||
})
|
||||
);
|
||||
expect(mockCircle).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
center: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
radius: 2,
|
||||
})
|
||||
);
|
||||
});
|
||||
it("are not interactive", () => {});
|
||||
});
|
||||
});
|
||||
@@ -30,18 +30,10 @@ const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => {
|
||||
|
||||
export default function CullingExclusionZones() {
|
||||
const data = useAppSelector(selectUnculledZones).zones;
|
||||
var cez = <></>;
|
||||
|
||||
if (!data) {
|
||||
console.log("Empty response when loading culling exclusion zones");
|
||||
} else {
|
||||
cez = (
|
||||
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<LayersControl.Overlay name="Culling exclusion zones">
|
||||
{cez}
|
||||
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
|
||||
</LayersControl.Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
405
client/src/components/flightplanslayer/FlightPlansLayer.test.tsx
Normal file
405
client/src/components/flightplanslayer/FlightPlansLayer.test.tsx
Normal file
@@ -0,0 +1,405 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import FlightPlansLayer from "./FlightPlansLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
// The waypoints in test data below should all use `should_make: false`. Markers
|
||||
// need useMap() to check the zoom level to decide if they should be drawn or
|
||||
// not, and we don't have good options here for mocking that behavior.
|
||||
describe("FlightPlansLayer", () => {
|
||||
describe("unselected flights", () => {
|
||||
it("are drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn if wrong coalition", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: false,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn when only selected flights are to be drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe("selected flights", () => {
|
||||
it("are drawn", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: "foo",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn twice", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: true,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: "foo",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
it("are not drawn if red", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
|
||||
preloadedState: {
|
||||
flights: {
|
||||
flights: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
blue: false,
|
||||
sidc: "",
|
||||
waypoints: [
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 0,
|
||||
lng: 0,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
position: {
|
||||
lat: 1,
|
||||
lng: 1,
|
||||
},
|
||||
altitude_ft: 0,
|
||||
altitude_reference: "MSL",
|
||||
is_movable: true,
|
||||
should_mark: false,
|
||||
include_in_path: true,
|
||||
timing: "",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
selected: "foo",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
it("are not drawn if there are no flights", () => {
|
||||
renderWithProviders(<FlightPlansLayer blue={true} />);
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
32
client/src/components/frontline/FrontLine.test.tsx
Normal file
32
client/src/components/frontline/FrontLine.test.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import FrontLine from "./FrontLine";
|
||||
import { PolylineProps } from "react-leaflet";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
Polyline: (props: PolylineProps) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
describe("FrontLine", () => {
|
||||
it("is drawn in the correct location", () => {
|
||||
const extents = [
|
||||
{ lat: 0, lng: 0 },
|
||||
{ lat: 1, lng: 0 },
|
||||
];
|
||||
renderWithProviders(
|
||||
<FrontLine
|
||||
front={{
|
||||
id: "",
|
||||
extents: extents,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: extents,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import FrontLinesLayer from "./FrontLinesLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polyline: (props: any) => {
|
||||
mockPolyline(props);
|
||||
},
|
||||
}));
|
||||
|
||||
// The waypoints in test data below should all use `should_make: false`. Markers
|
||||
// need useMap() to check the zoom level to decide if they should be drawn or
|
||||
// not, and we don't have good options here for mocking that behavior.
|
||||
describe("FrontLinesLayer", () => {
|
||||
it("draws nothing when there are no front lines", () => {
|
||||
renderWithProviders(<FrontLinesLayer />);
|
||||
expect(mockPolyline).not.toHaveBeenCalled();
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("draws front lines", () => {
|
||||
const extents = [
|
||||
{ lat: 0, lng: 0 },
|
||||
{ lat: 1, lng: 1 },
|
||||
];
|
||||
renderWithProviders(<FrontLinesLayer />, {
|
||||
preloadedState: {
|
||||
frontLines: {
|
||||
fronts: {
|
||||
foo: {
|
||||
id: "foo",
|
||||
extents: extents,
|
||||
},
|
||||
bar: {
|
||||
id: "bar",
|
||||
extents: extents,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: extents,
|
||||
})
|
||||
);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,7 @@ import { useAppSelector } from "../../app/hooks";
|
||||
import FrontLine from "../frontline";
|
||||
import { LayerGroup } from "react-leaflet";
|
||||
|
||||
export default function SupplyRoutesLayer() {
|
||||
export default function FrontLinesLayer() {
|
||||
const fronts = useAppSelector(selectFrontLines).fronts;
|
||||
return (
|
||||
<LayerGroup>
|
||||
|
||||
125
client/src/components/navmesh/NavMeshLayer.test.tsx
Normal file
125
client/src/components/navmesh/NavMeshLayer.test.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import NavMeshLayer from "./NavMeshLayer";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolygon = jest.fn();
|
||||
const mockLayerGroup = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
LayerGroup: (props: PropsWithChildren<any>) => {
|
||||
mockLayerGroup(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Polygon: (props: any) => {
|
||||
mockPolygon(props);
|
||||
},
|
||||
}));
|
||||
|
||||
// The waypoints in test data below should all use `should_make: false`. Markers
|
||||
// need useMap() to check the zoom level to decide if they should be drawn or
|
||||
// not, and we don't have good options here for mocking that behavior.
|
||||
describe("NavMeshLayer", () => {
|
||||
it("draws blue meshes", () => {
|
||||
const poly1 = [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
];
|
||||
const poly2 = [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: -1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
];
|
||||
renderWithProviders(<NavMeshLayer blue={true} />, {
|
||||
preloadedState: {
|
||||
navmeshes: {
|
||||
blue: [
|
||||
{
|
||||
poly: poly1,
|
||||
threatened: false,
|
||||
},
|
||||
{
|
||||
poly: poly2,
|
||||
threatened: true,
|
||||
},
|
||||
],
|
||||
red: [
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 2 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolygon).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolygon).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fillColor: "#00ff00",
|
||||
positions: poly1,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
expect(mockPolygon).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
fillColor: "#ff0000",
|
||||
positions: poly2,
|
||||
interactive: false,
|
||||
})
|
||||
);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
it("draws red navmesh", () => {
|
||||
renderWithProviders(<NavMeshLayer blue={false} />, {
|
||||
preloadedState: {
|
||||
navmeshes: {
|
||||
blue: [
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: false,
|
||||
},
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: -1 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: true,
|
||||
},
|
||||
],
|
||||
red: [
|
||||
{
|
||||
poly: [
|
||||
[
|
||||
{ lat: -1, lng: 0 },
|
||||
{ lat: 0, lng: 2 },
|
||||
{ lat: 1, lng: 0 },
|
||||
],
|
||||
],
|
||||
threatened: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(mockPolygon).toHaveBeenCalledTimes(1);
|
||||
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
16
client/src/components/splitlines/SplitLines.test.tsx
Normal file
16
client/src/components/splitlines/SplitLines.test.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import SplitLines from "./SplitLines";
|
||||
import { screen } from "@testing-library/dom";
|
||||
import { render } from "@testing-library/react";
|
||||
|
||||
describe("SplitLines", () => {
|
||||
it("joins items with line break tags", () => {
|
||||
render(
|
||||
<div data-testid={"container"}>
|
||||
<SplitLines items={["foo", "bar", "baz"]} />
|
||||
</div>
|
||||
);
|
||||
|
||||
const container = screen.getByTestId("container");
|
||||
expect(container).toContainHTML("foo<br />bar<br />baz<br />");
|
||||
});
|
||||
});
|
||||
159
client/src/components/supplyroute/SupplyRoute.test.tsx
Normal file
159
client/src/components/supplyroute/SupplyRoute.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { renderWithProviders } from "../../testutils";
|
||||
import SupplyRoute, { RouteColor } from "./SupplyRoute";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { PropsWithChildren } from "react";
|
||||
|
||||
const mockPolyline = jest.fn();
|
||||
jest.mock("react-leaflet", () => ({
|
||||
Polyline: (props: PropsWithChildren<any>) => {
|
||||
mockPolyline(props);
|
||||
return <>{props.children}</>;
|
||||
},
|
||||
Tooltip: (props: PropsWithChildren<any>) => {
|
||||
return <p data-testid="tooltip">{props.children}</p>;
|
||||
},
|
||||
}));
|
||||
|
||||
describe("SupplyRoute", () => {
|
||||
it("is red when inactive and owned by opfor", () => {
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: [],
|
||||
front_active: false,
|
||||
is_sea: false,
|
||||
blue: false,
|
||||
active_transports: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
color: RouteColor.Red,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("is blue when inactive and owned by bluefor", () => {
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: [],
|
||||
front_active: false,
|
||||
is_sea: false,
|
||||
blue: true,
|
||||
active_transports: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
color: RouteColor.Blue,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("is orange when contested", () => {
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: [],
|
||||
front_active: true,
|
||||
is_sea: false,
|
||||
blue: false,
|
||||
active_transports: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
color: RouteColor.Contested,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("is highlighted when the route has active transports", () => {
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: [],
|
||||
front_active: false,
|
||||
is_sea: false,
|
||||
blue: false,
|
||||
active_transports: ["foo"],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
color: RouteColor.Highlight,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("is drawn in the right place", () => {
|
||||
const points = [
|
||||
{ lat: 0, lng: 0 },
|
||||
{ lat: 1, lng: 1 },
|
||||
];
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: points,
|
||||
front_active: false,
|
||||
is_sea: false,
|
||||
blue: false,
|
||||
active_transports: ["foo"],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
expect(mockPolyline).toHaveBeenCalledTimes(2);
|
||||
expect(mockPolyline).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
positions: points,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("has a tooltip describing an inactive supply route", () => {
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: [],
|
||||
front_active: false,
|
||||
is_sea: false,
|
||||
blue: false,
|
||||
active_transports: [],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltip = screen.getByTestId("tooltip");
|
||||
expect(tooltip).toHaveTextContent("This supply route is inactive.");
|
||||
});
|
||||
|
||||
it("has a tooltip describing active supply routes", () => {
|
||||
renderWithProviders(
|
||||
<SupplyRoute
|
||||
route={{
|
||||
id: "",
|
||||
points: [],
|
||||
front_active: false,
|
||||
is_sea: false,
|
||||
blue: false,
|
||||
active_transports: ["foo", "bar"],
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const tooltip = screen.getByTestId("tooltip");
|
||||
expect(tooltip).toContainHTML("foo<br />bar");
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,13 @@ import { Polyline as LPolyline } from "leaflet";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Polyline, Tooltip } from "react-leaflet";
|
||||
|
||||
export enum RouteColor {
|
||||
Blue = "#2d3e50",
|
||||
Contested = "#c85050",
|
||||
Highlight = "#ffffff",
|
||||
Red = "#8c1414",
|
||||
}
|
||||
|
||||
interface SupplyRouteProps {
|
||||
route: SupplyRouteModel;
|
||||
}
|
||||
@@ -26,18 +33,22 @@ function ActiveSupplyRouteHighlight(props: SupplyRouteProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Polyline positions={props.route.points} color={"#ffffff"} weight={2} />
|
||||
<Polyline
|
||||
positions={props.route.points}
|
||||
color={RouteColor.Highlight}
|
||||
weight={2}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function colorFor(route: SupplyRouteModel) {
|
||||
if (route.front_active) {
|
||||
return "#c85050";
|
||||
return RouteColor.Contested;
|
||||
}
|
||||
if (route.blue) {
|
||||
return "#2d3e50";
|
||||
return RouteColor.Blue;
|
||||
}
|
||||
return "#8c1414";
|
||||
return RouteColor.Red;
|
||||
}
|
||||
|
||||
export default function SupplyRoute(props: SupplyRouteProps) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import App from "./App";
|
||||
import { store } from "./app/store";
|
||||
import { setupStore } from "./app/store";
|
||||
import { SocketProvider } from "./components/socketprovider/socketprovider";
|
||||
import "./index.css";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
@@ -12,7 +12,7 @@ const root = ReactDOM.createRoot(
|
||||
);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<Provider store={setupStore()}>
|
||||
<SocketProvider>
|
||||
<App />
|
||||
</SocketProvider>
|
||||
|
||||
30
client/src/testutils/index.tsx
Normal file
30
client/src/testutils/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
// https://redux.js.org/usage/writing-tests
|
||||
import { setupStore } from "../app/store";
|
||||
import type { AppStore, RootState } from "../app/store";
|
||||
import type { PreloadedState } from "@reduxjs/toolkit";
|
||||
import { render } from "@testing-library/react";
|
||||
import type { RenderOptions } from "@testing-library/react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Provider } from "react-redux";
|
||||
|
||||
// This type interface extends the default options for render from RTL, as well
|
||||
// as allows the user to specify other things such as initialState, store.
|
||||
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
|
||||
preloadedState?: PreloadedState<RootState>;
|
||||
store?: AppStore;
|
||||
}
|
||||
|
||||
export function renderWithProviders(
|
||||
ui: React.ReactElement,
|
||||
{
|
||||
preloadedState = {},
|
||||
// Automatically create a store instance if no store was passed in
|
||||
store = setupStore(preloadedState),
|
||||
...renderOptions
|
||||
}: ExtendedRenderOptions = {}
|
||||
) {
|
||||
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
|
||||
}
|
||||
8
codecov.yaml
Normal file
8
codecov.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
informational: true
|
||||
project:
|
||||
default:
|
||||
informational: true
|
||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# 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)
|
||||
34
docs/conf.py
Normal file
34
docs/conf.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
|
||||
project = "DCS Liberation"
|
||||
copyright = "2023, DCS Liberation Team"
|
||||
author = "DCS Liberation Team"
|
||||
release = "8.1.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
|
||||
8
docs/dev/design/index.rst
Normal file
8
docs/dev/design/index.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Design docs
|
||||
===========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
turnless.md
|
||||
8
docs/dev/index.rst
Normal file
8
docs/dev/index.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
Developer documentation
|
||||
=======================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
design/index.rst
|
||||
6
docs/game/index.rst
Normal file
6
docs/game/index.rst
Normal file
@@ -0,0 +1,6 @@
|
||||
Manual
|
||||
======
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
16
docs/index.rst
Normal file
16
docs/index.rst
Normal file
@@ -0,0 +1,16 @@
|
||||
DCS Liberation
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
game/index.rst
|
||||
modding/index.rst
|
||||
dev/index.rst
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
35
docs/make.bat
Normal file
35
docs/make.bat
Normal file
@@ -0,0 +1,35 @@
|
||||
@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
|
||||
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
10
docs/modding/index.rst
Normal file
10
docs/modding/index.rst
Normal file
@@ -0,0 +1,10 @@
|
||||
Modding guide
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: Contents:
|
||||
|
||||
fuel-consumption-measurement.md
|
||||
layouts.rst
|
||||
weather.rst
|
||||
397
docs/modding/layouts.rst
Normal file
397
docs/modding/layouts.rst
Normal file
@@ -0,0 +1,397 @@
|
||||
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
|
||||
76
docs/modding/weather.rst
Normal file
76
docs/modding/weather.rst
Normal file
@@ -0,0 +1,76 @@
|
||||
#######
|
||||
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.
|
||||
2
docs/requirements.txt
Normal file
2
docs/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
myst-parser
|
||||
sphinx_rtd_theme
|
||||
@@ -1,627 +0,0 @@
|
||||
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,
|
||||
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,
|
||||
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_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
|
||||
@@ -171,6 +171,15 @@ 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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
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) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.tot
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
@@ -68,7 +68,11 @@ class AirAssaultFlightPlan(StandardFlightPlan[AirAssaultLayout], UiZoneDisplay):
|
||||
return meters(2500)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
def ui_zone(self) -> UiZone:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.theater.missiontarget import MissionTarget
|
||||
@@ -67,16 +67,20 @@ class AirliftFlightPlan(StandardFlightPlan[AirliftLayout]):
|
||||
# drop-off waypoint.
|
||||
return self.layout.drop_off or self.layout.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | 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) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class CasLayout(PatrollingLayout):
|
||||
yield self.target
|
||||
yield self.patrol_end
|
||||
yield from self.nav_from
|
||||
yield self.departure
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from .flightplan import FlightPlan, Layout
|
||||
@@ -42,16 +42,20 @@ class CustomFlightPlan(FlightPlan[CustomLayout]):
|
||||
return waypoint
|
||||
return self.layout.departure
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.package.time_over_target
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
@@ -37,16 +37,20 @@ class FerryFlightPlan(StandardFlightPlan[FerryLayout]):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.arrival
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | 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) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@ generating the waypoints for the mission.
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from abc import ABC
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from functools import cached_property
|
||||
from typing import Any, Generic, TYPE_CHECKING, TypeGuard, TypeVar
|
||||
|
||||
@@ -21,6 +21,7 @@ from .planningerror import PlanningError
|
||||
from ..flightwaypointtype import FlightWaypointType
|
||||
from ..starttype import StartType
|
||||
from ..traveltime import GroundSpeed, TravelTime
|
||||
from ...savecompat import has_save_compat_for
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import FuelConsumption
|
||||
@@ -62,6 +63,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
def __init__(self, flight: Flight, layout: LayoutT) -> None:
|
||||
self.flight = flight
|
||||
self.layout = layout
|
||||
self.tot_offset = self.default_tot_offset()
|
||||
|
||||
@has_save_compat_for(7)
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
if "tot_offset" not in state:
|
||||
state["tot_offset"] = self.default_tot_offset()
|
||||
self.__dict__.update(state)
|
||||
|
||||
@property
|
||||
def package(self) -> Package:
|
||||
@@ -149,7 +157,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
raise NotImplementedError
|
||||
|
||||
@property
|
||||
def tot(self) -> timedelta:
|
||||
def tot(self) -> datetime:
|
||||
return self.package.time_over_target + self.tot_offset
|
||||
|
||||
@cached_property
|
||||
@@ -195,8 +203,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
|
||||
)
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
"""This flight's offset from the package's TOT.
|
||||
|
||||
Positive values represent later TOTs. An offset of -2 minutes is used
|
||||
@@ -215,7 +222,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
|
||||
for previous_waypoint, waypoint in self.edges(until=destination):
|
||||
total += self.travel_time_between_waypoints(previous_waypoint, waypoint)
|
||||
return total
|
||||
|
||||
# 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()))
|
||||
|
||||
def travel_time_between_waypoints(
|
||||
self, a: FlightWaypoint, b: FlightWaypoint
|
||||
@@ -224,10 +237,10 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
a.position, b.position, self.speed_between_waypoints(a, b)
|
||||
)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
raise NotImplementedError
|
||||
|
||||
def request_escort_at(self) -> FlightWaypoint | None:
|
||||
@@ -250,34 +263,20 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
if waypoint == end:
|
||||
return
|
||||
|
||||
def takeoff_time(self) -> timedelta:
|
||||
def takeoff_time(self) -> datetime:
|
||||
return self.tot - self._travel_time_to_waypoint(self.tot_waypoint)
|
||||
|
||||
def startup_time(self) -> timedelta:
|
||||
start_time = (
|
||||
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
||||
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()
|
||||
)
|
||||
|
||||
# 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 startup_time(self) -> datetime:
|
||||
return (
|
||||
self.takeoff_time() - self.estimate_startup() - self.estimate_ground_ops()
|
||||
)
|
||||
|
||||
def estimate_startup(self) -> timedelta:
|
||||
if self.flight.start_type is StartType.COLD:
|
||||
@@ -297,7 +296,17 @@ class FlightPlan(ABC, Generic[LayoutT]):
|
||||
return timedelta(minutes=8)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
@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:
|
||||
"""The time that the mission is complete and the flight RTBs."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
from typing import Any, TYPE_CHECKING, Type
|
||||
|
||||
from game.ato import FlightType
|
||||
from game.theater.controlpoint import NavalControlPoint
|
||||
from game.theater.frontline import FrontLine
|
||||
from .aewc import AewcFlightPlan
|
||||
from .airassault import AirAssaultFlightPlan
|
||||
from .airlift import AirliftFlightPlan
|
||||
@@ -19,6 +21,7 @@ from .ocarunway import OcaRunwayFlightPlan
|
||||
from .packagerefueling import PackageRefuelingFlightPlan
|
||||
from .planningerror import PlanningError
|
||||
from .sead import SeadFlightPlan
|
||||
from .shiprecoverytanker import RecoveryTankerFlightPlan
|
||||
from .strike import StrikeFlightPlan
|
||||
from .sweep import SweepFlightPlan
|
||||
from .tarcap import TarCapFlightPlan
|
||||
@@ -26,15 +29,19 @@ from .theaterrefueling import TheaterRefuelingFlightPlan
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.ato import Flight
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
class FlightPlanBuilderTypes:
|
||||
@staticmethod
|
||||
def for_flight(flight: Flight) -> Type[IBuilder[Any, Any]]:
|
||||
if flight.flight_type is FlightType.REFUELING:
|
||||
if flight.package.target.is_friendly(flight.squadron.player) or isinstance(
|
||||
flight.package.target, FrontLine
|
||||
target = flight.package.target
|
||||
if target.is_friendly(flight.squadron.player) and isinstance(
|
||||
target, NavalControlPoint
|
||||
):
|
||||
return RecoveryTankerFlightPlan.builder_type()
|
||||
if target.is_friendly(flight.squadron.player) or isinstance(
|
||||
target, FrontLine
|
||||
):
|
||||
return TheaterRefuelingFlightPlan.builder_type()
|
||||
return PackageRefuelingFlightPlan.builder_type()
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, 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) -> timedelta:
|
||||
def join_time(self) -> datetime:
|
||||
...
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def split_time(self) -> timedelta:
|
||||
def split_time(self) -> datetime:
|
||||
...
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | 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) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
return self.join_time - TravelTime.between_points(
|
||||
self.layout.hold.position,
|
||||
self.layout.join.position,
|
||||
@@ -97,7 +97,11 @@ class FormationFlightPlan(LoiterFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.split_time
|
||||
|
||||
@self_type_guard
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
from abc import ABC
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from dcs import Point
|
||||
@@ -25,10 +25,6 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def package_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {
|
||||
@@ -50,13 +46,6 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.targets[0]
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
try:
|
||||
return -self.lead_time
|
||||
except AttributeError:
|
||||
return timedelta()
|
||||
|
||||
@property
|
||||
def target_area_waypoint(self) -> FlightWaypoint:
|
||||
return FlightWaypoint(
|
||||
@@ -91,14 +80,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
return total
|
||||
|
||||
@property
|
||||
def join_time(self) -> timedelta:
|
||||
def join_time(self) -> datetime:
|
||||
travel_time = self.travel_time_between_waypoints(
|
||||
self.layout.join, self.layout.ingress
|
||||
)
|
||||
return self.ingress_time - travel_time
|
||||
|
||||
@property
|
||||
def split_time(self) -> timedelta:
|
||||
def split_time(self) -> datetime:
|
||||
travel_time_ingress = self.travel_time_between_waypoints(
|
||||
self.layout.ingress, self.target_area_waypoint
|
||||
)
|
||||
@@ -115,14 +104,14 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
|
||||
)
|
||||
|
||||
@property
|
||||
def ingress_time(self) -> timedelta:
|
||||
def ingress_time(self) -> datetime:
|
||||
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) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.ingress:
|
||||
return self.ingress_time
|
||||
elif waypoint in self.layout.targets:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, 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) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
...
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Type
|
||||
|
||||
from dcs import Point
|
||||
@@ -39,7 +39,7 @@ class PackageRefuelingFlightPlan(RefuelingFlightPlan):
|
||||
)
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
altitude = self.flight.unit_type.patrol_altitude
|
||||
|
||||
if altitude is None:
|
||||
|
||||
@@ -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 timedelta
|
||||
from datetime import datetime, 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) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> timedelta:
|
||||
def patrol_end_time(self) -> datetime:
|
||||
# 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) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.patrol_start:
|
||||
return self.patrol_start_time
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.patrol_end:
|
||||
return self.patrol_end_time
|
||||
return None
|
||||
@@ -90,7 +90,11 @@ class PatrollingFlightPlan(StandardFlightPlan[LayoutT], UiZoneDisplay, ABC):
|
||||
return self.layout.patrol_start
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
def mission_begin_on_station_time(self) -> datetime:
|
||||
return self.patrol_start_time
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.patrol_end_time
|
||||
|
||||
@self_type_guard
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import feet
|
||||
@@ -43,15 +43,19 @@ class RtbFlightPlan(StandardFlightPlan[RtbLayout]):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.abort_location
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
return timedelta()
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
|
||||
class Builder(IBuilder[RtbFlightPlan, RtbLayout]):
|
||||
|
||||
@@ -16,9 +16,8 @@ class SeadFlightPlan(FormationAttackFlightPlan):
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=1)
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=1)
|
||||
|
||||
|
||||
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):
|
||||
|
||||
95
game/ato/flightplans/shiprecoverytanker.py
Normal file
95
game/ato/flightplans/shiprecoverytanker.py
Normal file
@@ -0,0 +1,95 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, 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.waypointbuilder import WaypointBuilder
|
||||
from game.ato.flightwaypoint import FlightWaypoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RecoveryTankerLayout(StandardLayout):
|
||||
nav_to: list[FlightWaypoint]
|
||||
recovery_ship: FlightWaypoint
|
||||
nav_from: list[FlightWaypoint]
|
||||
|
||||
def iter_waypoints(self) -> Iterator[FlightWaypoint]:
|
||||
yield self.departure
|
||||
yield from self.nav_to
|
||||
yield self.recovery_ship
|
||||
yield from self.nav_from
|
||||
yield self.arrival
|
||||
if self.divert is not None:
|
||||
yield self.divert
|
||||
yield self.bullseye
|
||||
|
||||
|
||||
class RecoveryTankerFlightPlan(StandardFlightPlan[RecoveryTankerLayout]):
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
|
||||
@property
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
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:
|
||||
return self.patrol_end_time
|
||||
|
||||
@property
|
||||
def patrol_start_time(self) -> datetime:
|
||||
return self.package.time_over_target
|
||||
|
||||
@property
|
||||
def patrol_end_time(self) -> datetime:
|
||||
return self.tot + timedelta(hours=2)
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.tot
|
||||
return None
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.tot_waypoint:
|
||||
return self.mission_departure_time
|
||||
return None
|
||||
|
||||
|
||||
class Builder(IBuilder[RecoveryTankerFlightPlan, RecoveryTankerLayout]):
|
||||
def layout(self) -> RecoveryTankerLayout:
|
||||
|
||||
builder = WaypointBuilder(self.flight, self.coalition)
|
||||
|
||||
# TODO: Propagate the ship position to the Tanker's TOT,
|
||||
# so that we minimize the tanker's need to catch up to the carrier.
|
||||
recovery_ship = self.package.target.position
|
||||
recovery_tanker = builder.recovery_tanker(recovery_ship)
|
||||
|
||||
# We don't have per aircraft cruise altitudes, so just reuse patrol altitude?
|
||||
tanker_type = self.flight.unit_type
|
||||
nav_cruise_altitude = tanker_type.preferred_patrol_altitude
|
||||
|
||||
return RecoveryTankerLayout(
|
||||
departure=builder.takeoff(self.flight.departure),
|
||||
nav_to=builder.nav_path(
|
||||
self.flight.departure.position, recovery_ship, nav_cruise_altitude
|
||||
),
|
||||
nav_from=builder.nav_path(
|
||||
recovery_ship, self.flight.arrival.position, nav_cruise_altitude
|
||||
),
|
||||
recovery_ship=recovery_tanker,
|
||||
arrival=builder.land(self.flight.arrival),
|
||||
divert=builder.divert(self.flight.divert),
|
||||
bullseye=builder.bullseye(),
|
||||
)
|
||||
|
||||
def build(self) -> RecoveryTankerFlightPlan:
|
||||
return RecoveryTankerFlightPlan(self.flight, self.layout())
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterator, TYPE_CHECKING, Type
|
||||
|
||||
from dcs import Point
|
||||
@@ -38,10 +38,6 @@ class SweepLayout(LoiterLayout):
|
||||
|
||||
|
||||
class SweepFlightPlan(LoiterFlightPlan):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=5)
|
||||
|
||||
@staticmethod
|
||||
def builder_type() -> Type[Builder]:
|
||||
return Builder
|
||||
@@ -54,42 +50,46 @@ class SweepFlightPlan(LoiterFlightPlan):
|
||||
def tot_waypoint(self) -> FlightWaypoint:
|
||||
return self.layout.sweep_end
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=5)
|
||||
|
||||
@property
|
||||
def sweep_start_time(self) -> timedelta:
|
||||
def sweep_start_time(self) -> datetime:
|
||||
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) -> timedelta:
|
||||
def sweep_end_time(self) -> datetime:
|
||||
return self.tot
|
||||
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def tot_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | 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) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
|
||||
if waypoint == self.layout.hold:
|
||||
return self.push_time
|
||||
return None
|
||||
|
||||
@property
|
||||
def push_time(self) -> timedelta:
|
||||
def push_time(self) -> datetime:
|
||||
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),
|
||||
)
|
||||
|
||||
def mission_departure_time(self) -> timedelta:
|
||||
@property
|
||||
def mission_begin_on_station_time(self) -> datetime | None:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> datetime:
|
||||
return self.sweep_end_time
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import random
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING, Type
|
||||
|
||||
from game.utils import Distance, Speed, feet
|
||||
@@ -34,10 +34,6 @@ class TarCapLayout(PatrollingLayout):
|
||||
|
||||
|
||||
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
@property
|
||||
def lead_time(self) -> timedelta:
|
||||
return timedelta(minutes=2)
|
||||
|
||||
@property
|
||||
def patrol_duration(self) -> timedelta:
|
||||
# Note that this duration only has an effect if there are no
|
||||
@@ -64,24 +60,23 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
|
||||
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
|
||||
return {self.layout.patrol_start, self.layout.patrol_end}
|
||||
|
||||
@property
|
||||
def tot_offset(self) -> timedelta:
|
||||
return -self.lead_time
|
||||
def default_tot_offset(self) -> timedelta:
|
||||
return -timedelta(minutes=2)
|
||||
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> timedelta | None:
|
||||
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | 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) -> timedelta:
|
||||
def patrol_start_time(self) -> datetime:
|
||||
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) -> timedelta:
|
||||
def patrol_end_time(self) -> datetime:
|
||||
end = self.package.escort_end_time
|
||||
if end is not None:
|
||||
return end
|
||||
|
||||
@@ -23,7 +23,7 @@ from game.theater import (
|
||||
TheaterGroundObject,
|
||||
TheaterUnit,
|
||||
)
|
||||
from game.utils import Distance, meters, nautical_miles
|
||||
from game.utils import Distance, feet, meters, nautical_miles
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
@@ -204,6 +204,19 @@ class WaypointBuilder:
|
||||
pretty_name="Refuel",
|
||||
)
|
||||
|
||||
def recovery_tanker(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
|
||||
return FlightWaypoint(
|
||||
"RECOVERY",
|
||||
FlightWaypointType.RECOVERY_TANKER,
|
||||
position,
|
||||
feet(6000),
|
||||
alt_type,
|
||||
description="Recovery tanker for aircraft carriers",
|
||||
pretty_name="Recovery",
|
||||
)
|
||||
|
||||
def split(self, position: Point) -> FlightWaypoint:
|
||||
alt_type: AltitudeReference = "BARO"
|
||||
if self.is_helo:
|
||||
|
||||
@@ -21,6 +21,36 @@ 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
|
||||
|
||||
@@ -20,11 +20,12 @@ class Uninitialized(FlightState):
|
||||
def on_game_tick(
|
||||
self, events: GameUpdateEvents, time: datetime, duration: timedelta
|
||||
) -> None:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
self.reinitialize(time)
|
||||
self.flight.state.on_game_tick(events, time, duration)
|
||||
|
||||
@property
|
||||
def is_waiting_for_start(self) -> bool:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
return True
|
||||
|
||||
def estimate_position(self) -> Point:
|
||||
raise RuntimeError("Attempted to simulate flight that is not fully initialized")
|
||||
@@ -35,7 +36,6 @@ 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} in {delay}"
|
||||
return f"{action} at {self.flight.flight_plan.startup_time():%H:%M:%S}"
|
||||
|
||||
@@ -18,19 +18,17 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class WaitingForStart(AtDeparture):
|
||||
def __init__(
|
||||
self,
|
||||
flight: Flight,
|
||||
settings: Settings,
|
||||
start_time: datetime,
|
||||
) -> None:
|
||||
def __init__(self, flight: Flight, settings: Settings) -> 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:
|
||||
|
||||
@@ -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.
|
||||
* 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.
|
||||
* resources/units/aircraft/*.yaml: Assign aircraft weight for the new task type in
|
||||
the `tasks` dict for all capable aircraft.
|
||||
|
||||
You may also need to update:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
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: timedelta | None = None
|
||||
departure_time: timedelta | None = None
|
||||
tot: datetime | None = None
|
||||
departure_time: datetime | None = None
|
||||
|
||||
@property
|
||||
def x(self) -> float:
|
||||
|
||||
@@ -49,3 +49,4 @@ class FlightWaypointType(IntEnum):
|
||||
REFUEL = 29 # Should look for nearby tanker to refuel from.
|
||||
CARGO_STOP = 30 # Stopover landing point using the LandingReFuAr waypoint type
|
||||
INGRESS_AIR_ASSAULT = 31
|
||||
RECOVERY_TANKER = 32
|
||||
|
||||
@@ -92,6 +92,9 @@ 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:
|
||||
|
||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from game.db import Database
|
||||
@@ -33,8 +33,11 @@ class Package:
|
||||
self.auto_asap = auto_asap
|
||||
self.flights: list[Flight] = []
|
||||
|
||||
# Desired TOT as an offset from mission start.
|
||||
self.time_over_target: timedelta = timedelta()
|
||||
# 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
|
||||
self.waypoints: PackageWaypoints | None = None
|
||||
|
||||
@property
|
||||
@@ -62,7 +65,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) -> Optional[timedelta]:
|
||||
def escort_start_time(self) -> datetime | None:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.request_escort_at()
|
||||
@@ -81,7 +84,7 @@ class Package:
|
||||
return None
|
||||
|
||||
@property
|
||||
def escort_end_time(self) -> Optional[timedelta]:
|
||||
def escort_end_time(self) -> datetime | None:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
waypoint = flight.flight_plan.dismiss_escort_at()
|
||||
@@ -103,7 +106,7 @@ class Package:
|
||||
return None
|
||||
|
||||
@property
|
||||
def mission_departure_time(self) -> Optional[timedelta]:
|
||||
def mission_departure_time(self) -> datetime | None:
|
||||
times = []
|
||||
for flight in self.flights:
|
||||
times.append(flight.flight_plan.mission_departure_time)
|
||||
@@ -111,8 +114,19 @@ class Package:
|
||||
return max(times)
|
||||
return None
|
||||
|
||||
def set_tot_asap(self) -> None:
|
||||
self.time_over_target = TotEstimator(self).earliest_tot()
|
||||
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 add_flight(self, flight: Flight) -> None:
|
||||
"""Adds a flight to the package."""
|
||||
@@ -204,3 +218,14 @@ 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)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from datetime import timedelta
|
||||
from datetime import datetime, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from dcs.mapping import Point
|
||||
@@ -56,44 +55,24 @@ class TotEstimator:
|
||||
def __init__(self, package: Package) -> None:
|
||||
self.package = package
|
||||
|
||||
def earliest_tot(self) -> timedelta:
|
||||
def earliest_tot(self, now: datetime) -> datetime:
|
||||
if not self.package.flights:
|
||||
return timedelta(0)
|
||||
return now
|
||||
|
||||
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()))
|
||||
return max(self.earliest_tot_for_flight(f, now) for f in self.package.flights)
|
||||
|
||||
@staticmethod
|
||||
def earliest_tot_for_flight(flight: Flight) -> timedelta:
|
||||
"""Estimate the fastest time from mission start to the target position.
|
||||
def earliest_tot_for_flight(flight: Flight, now: datetime) -> datetime:
|
||||
"""Estimate the earliest time the flight can reach the target position.
|
||||
|
||||
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.
|
||||
The interpretation of the TOT depends on the flight plan type. See the various
|
||||
FlightPlan implementations for details.
|
||||
|
||||
Args:
|
||||
flight: The flight to get the earliest TOT time for.
|
||||
flight: The flight to get the earliest TOT for.
|
||||
now: The current mission time.
|
||||
|
||||
Returns:
|
||||
The earliest possible TOT for the given flight in seconds. Returns 0
|
||||
if an ingress point cannot be found.
|
||||
The earliest possible TOT for the given flight.
|
||||
"""
|
||||
# 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
|
||||
return now + flight.flight_plan.minimum_duration_from_start_to_tot()
|
||||
|
||||
@@ -5,22 +5,24 @@ import logging
|
||||
from collections.abc import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Tuple
|
||||
from typing import Any, Dict, TYPE_CHECKING, Tuple
|
||||
|
||||
import yaml
|
||||
from packaging.version import Version
|
||||
|
||||
from game import persistency
|
||||
from game import persistence
|
||||
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
|
||||
@@ -40,8 +42,8 @@ class Campaign:
|
||||
#: selecting a campaign that is not up to date.
|
||||
version: Tuple[int, int]
|
||||
|
||||
recommended_player_faction: str
|
||||
recommended_enemy_faction: str
|
||||
recommended_player_faction: FactionRecommendation
|
||||
recommended_enemy_faction: FactionRecommendation
|
||||
recommended_start_date: datetime.date | None
|
||||
recommended_start_time: datetime.time | None
|
||||
|
||||
@@ -57,10 +59,9 @@ class Campaign:
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> Campaign:
|
||||
with path.open() as campaign_file:
|
||||
with path.open(encoding="utf-8") 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)
|
||||
@@ -93,8 +94,12 @@ class Campaign:
|
||||
data.get("authors", "???"),
|
||||
data.get("description", ""),
|
||||
(version.major, version.minor),
|
||||
data.get("recommended_player_faction", "USA 2005"),
|
||||
data.get("recommended_enemy_faction", "Russia 1990"),
|
||||
FactionRecommendation.from_field(
|
||||
data.get("recommended_player_faction"), player=True
|
||||
),
|
||||
FactionRecommendation.from_field(
|
||||
data.get("recommended_enemy_faction"), player=False
|
||||
),
|
||||
start_date,
|
||||
start_time,
|
||||
data.get("recommended_player_money", DEFAULT_BUDGET),
|
||||
@@ -163,6 +168,10 @@ 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")
|
||||
@@ -171,7 +180,7 @@ class Campaign:
|
||||
@classmethod
|
||||
def iter_campaign_defs(cls) -> Iterator[Path]:
|
||||
yield from cls.iter_campaigns_in_dir(
|
||||
Path(persistency.base_path()) / "Liberation/Campaigns"
|
||||
Path(persistence.base_path()) / "Liberation/Campaigns"
|
||||
)
|
||||
yield from cls.iter_campaigns_in_dir(Path("resources/campaigns"))
|
||||
|
||||
|
||||
@@ -11,11 +11,15 @@ 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]
|
||||
@@ -39,6 +43,7 @@ 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),
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
from typing import Optional, TYPE_CHECKING, Dict, Union
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.squadrons import Squadron
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from ..ato.flighttype import FlightType
|
||||
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
|
||||
from ..ato.flighttype import FlightType
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
from ..theater import ControlPoint
|
||||
|
||||
@@ -44,7 +43,12 @@ class DefaultSquadronAssigner:
|
||||
continue
|
||||
|
||||
squadron = Squadron.create_from(
|
||||
squadron_def, control_point, self.coalition, self.game
|
||||
squadron_def,
|
||||
squadron_config.primary,
|
||||
squadron_config.max_size,
|
||||
control_point,
|
||||
self.coalition,
|
||||
self.game,
|
||||
)
|
||||
squadron.set_auto_assignable_mission_types(
|
||||
squadron_config.auto_assignable
|
||||
@@ -54,7 +58,6 @@ 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
|
||||
@@ -62,13 +65,14 @@ class DefaultSquadronAssigner:
|
||||
if squadron_def is not None:
|
||||
return squadron_def
|
||||
|
||||
# If we didn't find any of the preferred types we should use any squadron
|
||||
# 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
|
||||
# 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 squadron matching the requirement, we should
|
||||
# If we can't find any pre-made squadron matching the requirement, we should
|
||||
# create one.
|
||||
return self.air_wing.squadron_def_generator.generate_for_task(
|
||||
config.primary, control_point
|
||||
@@ -89,7 +93,11 @@ class DefaultSquadronAssigner:
|
||||
try:
|
||||
aircraft = AircraftType.named(preferred_aircraft)
|
||||
except KeyError:
|
||||
# No aircraft with this name.
|
||||
logging.warning(
|
||||
"%s is neither a compatible squadron or a known aircraft type, "
|
||||
"ignoring",
|
||||
preferred_aircraft,
|
||||
)
|
||||
return None
|
||||
|
||||
if aircraft not in self.coalition.faction.aircrafts:
|
||||
@@ -112,7 +120,7 @@ class DefaultSquadronAssigner:
|
||||
) -> bool:
|
||||
if ignore_base_preference:
|
||||
return control_point.can_operate(squadron.aircraft)
|
||||
return squadron.operates_from(control_point) and task in squadron.mission_types
|
||||
return squadron.operates_from(control_point) and squadron.capable_of(task)
|
||||
|
||||
def find_squadron_for_airframe(
|
||||
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
|
||||
|
||||
53
game/campaignloader/factionrecommendation.py
Normal file
53
game/campaignloader/factionrecommendation.py
Normal file
@@ -0,0 +1,53 @@
|
||||
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
|
||||
@@ -9,7 +9,6 @@ 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
|
||||
@@ -25,7 +24,7 @@ class SquadronDefGenerator:
|
||||
self, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
aircraft_choice: Optional[AircraftType] = None
|
||||
for aircraft in aircraft_for_task(task):
|
||||
for aircraft in AircraftType.priority_list_for_task(task):
|
||||
if aircraft not in self.faction.aircrafts:
|
||||
continue
|
||||
if not control_point.can_operate(aircraft):
|
||||
@@ -48,7 +47,7 @@ class SquadronDefGenerator:
|
||||
role="Flying Squadron",
|
||||
aircraft=aircraft,
|
||||
livery=None,
|
||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||
auto_assignable_mission_types=set(aircraft.iter_task_capabilities()),
|
||||
operating_bases=OperatingBases.default_for_aircraft(aircraft),
|
||||
female_pilot_percentage=6,
|
||||
pilot_pool=[],
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from faker import Faker
|
||||
@@ -157,14 +158,14 @@ class Coalition:
|
||||
# is handled correctly.
|
||||
self.transfers.perform_transfers()
|
||||
|
||||
def preinit_turn_0(self) -> None:
|
||||
def preinit_turn_0(self, squadrons_start_full: bool) -> None:
|
||||
"""Runs final Coalition initialization.
|
||||
|
||||
Final initialization occurs before Game.initialize_turn runs for turn 0.
|
||||
"""
|
||||
self.air_wing.populate_for_turn_0()
|
||||
self.air_wing.populate_for_turn_0(squadrons_start_full)
|
||||
|
||||
def initialize_turn(self) -> None:
|
||||
def initialize_turn(self, is_turn_0: bool) -> None:
|
||||
"""Processes coalition-specific turn initialization.
|
||||
|
||||
For more information on turn initialization in general, see the documentation
|
||||
@@ -181,9 +182,10 @@ class Coalition:
|
||||
with logged_duration("Procurement of airlift assets"):
|
||||
self.transfers.order_airlift_assets()
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
self.transfers.plan_transports(self.game.conditions.start_time)
|
||||
|
||||
self.plan_missions()
|
||||
if not is_turn_0 or not self.game.settings.enable_squadron_aircraft_limits:
|
||||
self.plan_missions(self.game.conditions.start_time)
|
||||
self.plan_procurement()
|
||||
|
||||
def refund_outstanding_orders(self) -> None:
|
||||
@@ -199,16 +201,16 @@ class Coalition:
|
||||
for squadron in self.air_wing.iter_squadrons():
|
||||
squadron.refund_orders()
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
def plan_missions(self, now: datetime) -> 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(tracer)
|
||||
TheaterCommander(self.game, self.player).plan_missions(now, tracer)
|
||||
with tracer.trace(f"{color} mission scheduling"):
|
||||
MissionScheduler(
|
||||
self, self.game.settings.desired_player_mission_duration
|
||||
).schedule_missions()
|
||||
).schedule_missions(now)
|
||||
|
||||
def plan_procurement(self) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
|
||||
|
||||
@@ -3,12 +3,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, Dict, TYPE_CHECKING
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Iterator, 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) -> None:
|
||||
def schedule_missions(self, now: datetime) -> 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, timedelta] = defaultdict(timedelta)
|
||||
previous_cap_end_time: dict[MissionTarget, datetime] = defaultdict(now.replace)
|
||||
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()
|
||||
tot = TotEstimator(package).earliest_tot(now)
|
||||
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()
|
||||
package.set_tot_asap(now)
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
|
||||
@@ -5,6 +5,7 @@ import operator
|
||||
from collections.abc import Iterable, Iterator
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
|
||||
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
from game.theater import (
|
||||
Airfield,
|
||||
ControlPoint,
|
||||
@@ -15,12 +16,11 @@ from game.theater import (
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
IadsGroundObject,
|
||||
NavalGroundObject,
|
||||
IadsBuildingGroundObject,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from game.ato.closestairfields import ClosestAirfields, ObjectiveDistanceCache
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
@@ -117,7 +117,7 @@ class ObjectiveFinder:
|
||||
|
||||
if isinstance(
|
||||
ground_object, IadsBuildingGroundObject
|
||||
) and not self.game.settings.plugin_option("skynetiads"):
|
||||
) and not self.game.lua_plugin_manager.is_plugin_enabled("skynetiads"):
|
||||
# Prevent strike targets on IADS Buildings when skynet features
|
||||
# are disabled as they do not serve any purpose
|
||||
continue
|
||||
@@ -209,22 +209,20 @@ class ObjectiveFinder:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return farthest
|
||||
|
||||
def closest_friendly_control_point(self) -> ControlPoint:
|
||||
def preferred_theater_refueling_control_point(self) -> ControlPoint | None:
|
||||
"""Finds the friendly control point that is closest to any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
closest = None
|
||||
min_distance = meters(math.inf)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
if isinstance(cp, OffMapSpawn) or cp.is_fleet:
|
||||
continue
|
||||
distance = threat_zones.distance_to_threat(cp.position)
|
||||
if distance < min_distance:
|
||||
closest = cp
|
||||
min_distance = distance
|
||||
|
||||
if closest is None:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return closest
|
||||
|
||||
def enemy_control_points(self) -> Iterator[ControlPoint]:
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -132,6 +133,7 @@ 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."""
|
||||
@@ -221,6 +223,6 @@ class PackageFulfiller:
|
||||
|
||||
if package.has_players and self.player_missions_asap:
|
||||
package.auto_asap = True
|
||||
package.set_tot_asap()
|
||||
package.set_tot_asap(now)
|
||||
|
||||
return package
|
||||
|
||||
@@ -31,8 +31,7 @@ class RangeType(IntEnum):
|
||||
|
||||
|
||||
# TODO: Refactor so that we don't need to call up to the mission planner.
|
||||
# Bypass type checker due to https://github.com/python/mypy/issues/5374
|
||||
@dataclass # type: ignore
|
||||
@dataclass
|
||||
class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
target: MissionTargetT
|
||||
flights: list[ProposedFlight] = field(init=False)
|
||||
@@ -104,6 +103,7 @@ 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
|
||||
|
||||
@@ -54,6 +54,7 @@ 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
|
||||
@@ -77,8 +78,8 @@ class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
def plan_missions(self, tracer: MultiEventTracer) -> None:
|
||||
state = TheaterState.from_game(self.game, self.player, tracer)
|
||||
def plan_missions(self, now: datetime, tracer: MultiEventTracer) -> None:
|
||||
state = TheaterState.from_game(self.game, self.player, now, tracer)
|
||||
while True:
|
||||
result = self.plan(state)
|
||||
if result is None:
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -36,6 +37,7 @@ class PersistentContext:
|
||||
coalition: Coalition
|
||||
theater: ConflictTheater
|
||||
turn: int
|
||||
now: datetime
|
||||
settings: Settings
|
||||
tracer: MultiEventTracer
|
||||
|
||||
@@ -137,14 +139,20 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
|
||||
@classmethod
|
||||
def from_game(
|
||||
cls, game: Game, player: bool, tracer: MultiEventTracer
|
||||
cls, game: Game, player: bool, now: datetime, 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, game.settings, tracer
|
||||
game.db,
|
||||
coalition,
|
||||
game.theater,
|
||||
game.turn,
|
||||
now,
|
||||
game.settings,
|
||||
tracer,
|
||||
)
|
||||
|
||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||
@@ -153,6 +161,11 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
|
||||
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||
|
||||
refueling_targets: list[MissionTarget] = []
|
||||
theater_refuling_point = finder.preferred_theater_refueling_control_point()
|
||||
if theater_refuling_point is not None:
|
||||
refueling_targets.append(theater_refuling_point)
|
||||
|
||||
return TheaterState(
|
||||
context=context,
|
||||
barcaps_needed={
|
||||
@@ -162,7 +175,7 @@ class TheaterState(WorldState["TheaterState"]):
|
||||
front_line_stances={f: None for f in finder.front_lines()},
|
||||
vulnerable_front_lines=list(finder.front_lines()),
|
||||
aewc_targets=[finder.farthest_friendly_control_point()],
|
||||
refueling_targets=[finder.closest_friendly_control_point()],
|
||||
refueling_targets=refueling_targets,
|
||||
enemy_air_defenses=list(finder.enemy_air_defenses()),
|
||||
threatening_air_defenses=[],
|
||||
detecting_air_defenses=[],
|
||||
|
||||
@@ -97,6 +97,7 @@ class WeaponType(Enum):
|
||||
ARM = "ARM"
|
||||
LGB = "LGB"
|
||||
TGP = "TGP"
|
||||
DECOY = "decoy"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from functools import cache, cached_property
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterator, Optional, TYPE_CHECKING, Type
|
||||
from typing import Any, ClassVar, Dict, Iterator, Optional, TYPE_CHECKING, Type
|
||||
|
||||
import yaml
|
||||
from dcs.helicopters import helicopter_map
|
||||
@@ -20,6 +21,7 @@ from game.radio.channels import (
|
||||
CommonRadioChannelAllocator,
|
||||
FarmerRadioChannelAllocator,
|
||||
HueyChannelNamer,
|
||||
LegacyWarthogChannelNamer,
|
||||
MirageChannelNamer,
|
||||
MirageF1CEChannelNamer,
|
||||
NoOpChannelAllocator,
|
||||
@@ -31,6 +33,7 @@ from game.radio.channels import (
|
||||
ViggenChannelNamer,
|
||||
ViggenRadioChannelAllocator,
|
||||
ViperChannelNamer,
|
||||
WarthogChannelNamer,
|
||||
)
|
||||
from game.utils import (
|
||||
Distance,
|
||||
@@ -47,6 +50,7 @@ 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
|
||||
@@ -104,6 +108,8 @@ class RadioConfig:
|
||||
"viggen": ViggenChannelNamer,
|
||||
"viper": ViperChannelNamer,
|
||||
"apache": ApacheChannelNamer,
|
||||
"a10c-legacy": LegacyWarthogChannelNamer,
|
||||
"a10c-ii": WarthogChannelNamer,
|
||||
}[config.get("namer", "default")]
|
||||
|
||||
|
||||
@@ -192,6 +198,23 @@ 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
|
||||
@@ -302,6 +325,12 @@ 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"])
|
||||
@@ -312,17 +341,31 @@ class AircraftType(UnitType[Type[FlyingType]]):
|
||||
def named(cls, name: str) -> AircraftType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
unit = cls._by_name[name]
|
||||
assert isinstance(unit, AircraftType)
|
||||
return unit
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
for unit in cls._by_unit_type[dcs_unit_type]:
|
||||
assert isinstance(unit, AircraftType)
|
||||
yield unit
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
def each_dcs_type() -> Iterator[Type[FlyingType]]:
|
||||
@@ -348,6 +391,8 @@ 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")
|
||||
@@ -404,6 +449,10 @@ 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,
|
||||
@@ -435,4 +484,9 @@ 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)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterator, Optional, Type
|
||||
from typing import Any, ClassVar, Iterator, Optional, Type
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import VehicleType
|
||||
@@ -59,21 +60,27 @@ 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()
|
||||
unit = cls._by_name[name]
|
||||
assert isinstance(unit, GroundUnitType)
|
||||
return unit
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
for unit in cls._by_unit_type[dcs_unit_type]:
|
||||
assert isinstance(unit, GroundUnitType)
|
||||
yield unit
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def each_dcs_type() -> Iterator[Type[VehicleType]]:
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Type
|
||||
from typing import ClassVar, Iterator, Type
|
||||
|
||||
import yaml
|
||||
from dcs.ships import ship_map
|
||||
@@ -15,21 +16,27 @@ 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()
|
||||
unit = cls._by_name[name]
|
||||
assert isinstance(unit, ShipUnitType)
|
||||
return unit
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: Type[ShipType]) -> Iterator[ShipUnitType]:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
for unit in cls._by_unit_type[dcs_unit_type]:
|
||||
assert isinstance(unit, ShipUnitType)
|
||||
yield unit
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def each_dcs_type() -> Iterator[Type[ShipType]]:
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import Any, ClassVar, Generic, Iterator, Type, TypeVar
|
||||
from typing import ClassVar, Generic, Iterator, Self, Type, TypeVar
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
@@ -25,10 +24,6 @@ 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:
|
||||
@@ -39,16 +34,15 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@classmethod
|
||||
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]:
|
||||
def register(cls, unit_type: Self) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
||||
def named(cls, name: str) -> Self:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def for_dcs_type(cls, dcs_unit_type: DcsUnitTypeT) -> Iterator[Self]:
|
||||
raise NotImplementedError
|
||||
|
||||
@staticmethod
|
||||
@@ -56,7 +50,7 @@ class UnitType(ABC, Generic[DcsUnitTypeT]):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[UnitType[Any]]:
|
||||
def _each_variant_of(cls, unit: DcsUnitTypeT) -> Iterator[Self]:
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -109,16 +109,42 @@ class StateData:
|
||||
base_capture_events: List[str]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: Dict[str, Any]) -> StateData:
|
||||
def from_json(cls, data: Dict[str, Any], unit_map: UnitMap) -> StateData:
|
||||
def clean_unit_list(unit_list: List[Any]) -> List[str]:
|
||||
# Cleans list of units in state.json by
|
||||
# - Removing duplicates. Airfields emit a new "dead" event every time a bomb
|
||||
# is dropped on them when they've already dead.
|
||||
# - Normalise dead map objects (which are ints) to strings. The unit map
|
||||
# only stores strings
|
||||
units = set()
|
||||
for unit in unit_list:
|
||||
units.add(str(unit))
|
||||
return list(units)
|
||||
|
||||
killed_aircraft = []
|
||||
killed_ground_units = []
|
||||
|
||||
# Process killed units from S_EVENT_UNIT_LOST, S_EVENT_CRASH, S_EVENT_DEAD & S_EVENT_KILL
|
||||
# Try to process every event that could indicate a unit was killed, even if it is
|
||||
# inefficient and results in duplication as the logic DCS uses to trigger the various
|
||||
# event types is not clear and may change over time.
|
||||
killed_units = clean_unit_list(
|
||||
data["unit_lost_events"]
|
||||
+ data["kill_events"]
|
||||
+ data["crash_events"]
|
||||
+ data["dead_events"]
|
||||
+ data["killed_ground_units"]
|
||||
)
|
||||
for unit in killed_units: # organize killed units into aircraft vs ground
|
||||
if unit_map.flight(unit) is not None:
|
||||
killed_aircraft.append(unit)
|
||||
else:
|
||||
killed_ground_units.append(unit)
|
||||
|
||||
return cls(
|
||||
mission_ended=data["mission_ended"],
|
||||
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"]}),
|
||||
killed_aircraft=killed_aircraft,
|
||||
killed_ground_units=killed_ground_units,
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"],
|
||||
)
|
||||
@@ -128,7 +154,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)
|
||||
self.state_data = StateData.from_json(state_data, unit_map)
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
from .faction import Faction
|
||||
from .factionloader import FactionLoader
|
||||
|
||||
FACTIONS = FactionLoader()
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user