mirror of
https://github.com/dcs-liberation/dcs_liberation.git
synced 2025-11-10 14:22:26 +00:00
Compare commits
2 Commits
5.2.0
...
develop_2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c1f3e8116 | ||
|
|
df00059d3f |
@@ -1,2 +0,0 @@
|
||||
# Black
|
||||
a47bef1f1336fd264d0b175f4421758339a30acb
|
||||
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
40
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,40 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if the issue has already been reported.
|
||||
|
||||
If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional information**
|
||||
|
||||
We will usually need more information for debugging. Include as much of the following as you are able:
|
||||
|
||||
- DCS Liberation save file (the `.liberation` file you save from the DCS Liberation window). By default these are located in your DCS saved games directory (`%USERPROFILE%/Saved Games/DCS`).
|
||||
- The generated mission file (the `.miz` file that you load in DCS to play the turn). By default these are located in your missions directory (`%USERPROFILE%/Saved Games/DCS/Missions`).
|
||||
- A tacview track file, especially when demonstrating an issue with AI behavior. By default these are located in your Tacview tracks directory (`%USERPROFILE%/Documents/Tacview`).
|
||||
- The state.json file from the finished mission when the problem is related to results processing. By default these are located in your Liberation install directory.
|
||||
|
||||
**Version information (please complete the following information):**
|
||||
- DCS Liberation [e.g. 2.3.1]:
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
28
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
28
.github/ISSUE_TEMPLATE/campaign_update.md
vendored
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: Campaign update submission
|
||||
about: Submit an update to a campaign you maintain.
|
||||
title: 'Update for <campaign name>'
|
||||
labels: campaign-update-submission
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
This form should only be used for submitted updated miz/json files for campaigns
|
||||
distributed with Liberation. If you are _requesting_ an update to a campaign, see
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance. If the
|
||||
campaign has an owner, it will be updated before release. If it does not, you can
|
||||
volunteer to own it.
|
||||
|
||||
If you are not the owner of the campaign listed on
|
||||
https://github.com/dcs-liberation/dcs_liberation/wiki/Campaign-maintenance, please start
|
||||
there.
|
||||
|
||||
Otherwise, delete everything above the line below and fill out the following form. Note:
|
||||
GitHub does not accept .miz files. You can either rename the file to .miz.txt or add the
|
||||
file to a .zip file.
|
||||
|
||||
---
|
||||
|
||||
* Campaign name:
|
||||
* Files:
|
||||
* Update summary (optional):
|
||||
1
.github/ISSUE_TEMPLATE/config.yml
vendored
1
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Before filing, please search the issue tracker to see if this feature has already been requested.
|
||||
|
||||
If requesting a DCS AI feature, check If reporting a DCS AI bug, check https://github.com/dcs-liberation/dcs_liberation#dcs-bugs.
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
12
.github/ISSUE_TEMPLATE/mod_support.md
vendored
12
.github/ISSUE_TEMPLATE/mod_support.md
vendored
@@ -1,12 +0,0 @@
|
||||
---
|
||||
name: Mod support request
|
||||
about: Request Liberation support for new mods, or updates to existing mods
|
||||
title: Add/update <mod name>
|
||||
labels: mod support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
* Mod name:
|
||||
* Mod URL:
|
||||
* Update or new mod?
|
||||
13
.github/workflows/black.yml
vendored
13
.github/workflows/black.yml
vendored
@@ -1,13 +0,0 @@
|
||||
name: Lint
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-python@v2
|
||||
- uses: psf/black@stable
|
||||
with:
|
||||
args: ". --check"
|
||||
23
.github/workflows/build.yml
vendored
23
.github/workflows/build.yml
vendored
@@ -11,10 +11,10 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
@@ -27,25 +27,6 @@ jobs:
|
||||
# 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
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy game
|
||||
|
||||
- name: mypy gen
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: mypy tests
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy tests
|
||||
|
||||
- name: update build number
|
||||
run: |
|
||||
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
|
||||
33
.github/workflows/release.yml
vendored
33
.github/workflows/release.yml
vendored
@@ -13,10 +13,10 @@ jobs:
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.9
|
||||
- name: Set up Python 3.8
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
@@ -29,20 +29,6 @@ jobs:
|
||||
# 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
|
||||
|
||||
- name: Finalize version
|
||||
run: |
|
||||
New-Item -ItemType file resources\final
|
||||
|
||||
- name: mypy game
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy game
|
||||
|
||||
- name: mypy gen
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
mypy gen
|
||||
|
||||
- name: Build binaries
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
@@ -53,6 +39,11 @@ jobs:
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref }}
|
||||
run: |
|
||||
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
|
||||
(Get-Content .\installer\dcs_liberation.iss) -replace "{{version}}",$version | Out-File .\build\installer.iss
|
||||
cd .\installer
|
||||
iscc.exe ..\build\installer.iss
|
||||
cd ..
|
||||
Copy-Item .\changelog.md .\dist
|
||||
|
||||
- uses: actions/upload-artifact@v2
|
||||
@@ -95,7 +86,15 @@ jobs:
|
||||
body_path: releasenotes.md
|
||||
draft: false
|
||||
prerelease: ${{ steps.version.outputs.prerelease }}
|
||||
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ./dcs_liberation.exe
|
||||
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.exe
|
||||
asset_content_type: application/exe
|
||||
|
||||
- uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
||||
33
.github/workflows/test.yml
vendored
33
.github/workflows/test.yml
vendored
@@ -1,33 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install environment
|
||||
run: |
|
||||
python -m venv ./venv
|
||||
|
||||
- name: Install dependencies
|
||||
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
|
||||
|
||||
- name: run tests
|
||||
run: |
|
||||
./venv/scripts/activate
|
||||
pytest tests
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -5,21 +5,20 @@ resources/payloads/*.lua
|
||||
venv
|
||||
logs.txt
|
||||
.DS_Store
|
||||
.vscode/settings.json
|
||||
dist/**
|
||||
a.py
|
||||
resources/tools/a.miz
|
||||
tests/**
|
||||
# User-specific stuff
|
||||
.idea/
|
||||
.env
|
||||
env/
|
||||
|
||||
/kneeboards
|
||||
/liberation_preferences.json
|
||||
/state.json
|
||||
|
||||
/logs/
|
||||
logs/liberation.log
|
||||
|
||||
qt_ui/logs/liberation.log
|
||||
|
||||
*.psd
|
||||
resources/scripts/plugins/*
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "pydcs"]
|
||||
path = pydcs
|
||||
url = https://github.com/pydcs/dcs
|
||||
branch = master
|
||||
@@ -1,6 +0,0 @@
|
||||
repos:
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 20.8b1
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
4
.vscode/settings.json
vendored
Normal file
4
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe",
|
||||
"vsintellicode.python.completionsEnabled": true
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||
level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at khopa.studio@gmail.com. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see
|
||||
https://www.contributor-covenant.org/faq
|
||||
@@ -1,26 +0,0 @@
|
||||
First, note that we have a code of conduct, please follow it in all your interactions with the project.
|
||||
|
||||
## Contributing as a non-developer
|
||||
|
||||
* Report bugs by opening issues here on Github.
|
||||
* Help others users on Discord by answering their questions.
|
||||
* Raise awareness about the project, by making a video and/or a tutorial.
|
||||
|
||||
Should you report a bug, please use the search bar at the top of the page to see if it has already been reported.
|
||||
Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
|
||||
|
||||
## Making content for Liberation
|
||||
|
||||
You can create new campaigns : See [campaign creation wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Campaigns).
|
||||
You can also improve existing campaigns.
|
||||
|
||||
You can then submit new campaigns on the "campaigns" channel on Discord, or by making a pull request if you are comfortable with git.
|
||||
|
||||
## Develop new features
|
||||
|
||||
If you want to develop a new feature, we recommend you first open an issue describing the new feature and discuss it with us on Discord before starting development.
|
||||
However, feel free to work on any existing issue.
|
||||
|
||||
## Pull requests
|
||||
|
||||
Please submit your pull requests on the **develop** branch. We expect a description of its content, and when applicable, a reference to the issue(s) it is resolving.
|
||||
165
LICENSE
165
LICENSE
@@ -1,165 +0,0 @@
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
45
README.md
45
README.md
@@ -1,48 +1,29 @@
|
||||
[](https://shdwp.github.io/ukraine/)
|
||||
|
||||
(Github Readme Banner and Splash screen Artwork by Andriy Dankovych, CC BY-SA 4.0)
|
||||

|
||||
|
||||
[](https://www.paypal.com/paypalme/KhopaDCSL)
|
||||
[](https://patreon.com/khopa)
|
||||
|
||||
[](https://github.com/dcs-liberation/dcs_liberation/releases)
|
||||
[](https://github.com/Khopa/dcs_liberation/releases)
|
||||
|
||||
[](https://discord.gg/bKrtrkJ)
|
||||
|
||||
[](https://github.com/dcs-liberation/dcs_liberation)
|
||||
[](https://github.com/dcs-liberation/dcs_liberation/issues)
|
||||

|
||||
[](https://github.com/Khopa/dcs_liberation)
|
||||
[](https://github.com/Khopa/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.
|
||||
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player semi dynamic campaign.
|
||||
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
|
||||
|
||||

|
||||

|
||||
|
||||
## Downloads
|
||||
|
||||
Latest release is available here : https://github.com/dcs-liberation/dcs_liberation/releases
|
||||
|
||||
To download preview builds of the next version of DCS Liberation, see https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds.
|
||||
|
||||
## DCS bugs
|
||||
|
||||
These DCS bugs prevent us from improving AI behavior. Please upvote them! (But please
|
||||
_don't_ spam them with comments):
|
||||
|
||||
* [A2A and SEAD escorts don't escort](https://forums.eagle.ru/topic/251798-options-for-alternate-ai-escort-behavior/?tab=comments#comment-4668033)
|
||||
* [DEAD can't use mixed loadouts effectively](https://forums.eagle.ru/topic/271941-ai-rtbs-after-firing-decoys-despite-full-load-of-bombs/)
|
||||
|
||||
## Bugs and feature requests
|
||||
|
||||
If you need to report a bug or want to suggest a new feature, you can do this on our [bug tracker](https://github.com/dcs-liberation/dcs_liberation/issues). In either case, please use the search bar at the top of the page to see if it has already been reported. Note that you may need to remove the filter for open bugs if it's something we've recently fixed.
|
||||
|
||||
## Roadmap
|
||||
|
||||
Our plans for future releases can be found on our [Projects page](https://github.com/dcs-liberation/dcs_liberation/projects). Each planned release has a Project, and the page for that project has columns for to do, in progress, and done. Items in the Done column are in the [preview build](https://github.com/dcs-liberation/dcs_liberation/wiki/Preview-builds) for that release. Items in the To do column are planned to be added to that release.
|
||||
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
|
||||
|
||||
## Resources
|
||||
|
||||
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/dcs-liberation/dcs_liberation/wiki/)
|
||||
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
|
||||
|
||||
## Special Thanks
|
||||
|
||||
@@ -50,10 +31,6 @@ First, a big thanks to shdwp, for starting the original DCS Liberation project.
|
||||
|
||||
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
|
||||
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
|
||||
|
||||
Excellent lua scripts DCS Liberation uses as plugins:
|
||||
|
||||
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
|
||||
And for the JTAC feature, DCS Liberation embed Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
|
||||
|
||||
Please also show some support to these projects !
|
||||
|
||||
585
changelog.md
585
changelog.md
@@ -1,582 +1,3 @@
|
||||
# 5.2.0
|
||||
|
||||
Saves from 5.1.0 are compatible with 5.2.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS DCS 2.7.11.21408, including the new Apache AH-64D and the Syria map extension
|
||||
* **[Mission Generation]** Improved FARP Helipad handling and creation (now includes windsocks)
|
||||
* **[Modding]** Add UH-60L mod support
|
||||
* **[Modding]** Updated Community A-4E-C mod version support to 2.0.0 release. Version 1.4.2 is no longer compatible, unless the mod default loadouts are deleted/modified.
|
||||
* **[Modding]** Updated JAS-39-C mod support for v1.8.0-beta
|
||||
* **[Campaign]** Peace Spring, Vectron's Claw, Vegas Nerve, Scenic Route 2 campaign update
|
||||
* **[Campaign]** Added Tripoint Hostility campaign by Fuzzle
|
||||
* **[Campaign]** Add 3 new campaigns from Sith1144
|
||||
## Fixes
|
||||
|
||||
* **[Mission Generation]** Fixed incorrect SA-5 and NASAMS threat range when TR destroyed. It will not count as threat anymore when the TR is dead.
|
||||
* **[Mission Generation]** Fixed "Max Threat Range" error
|
||||
* **[Mission Generation]** Fix unculled zones not updating when needed
|
||||
* **[Mission Planner]** Now allows squadron transfers to control points where the number of free slots matches exactly the expected size of the transferring squadron next turn.
|
||||
* **[Data]** Removed Fw 190 A-8 and D-9 from Germany 1940 and 1942 faction list for historical accuracy.
|
||||
* **[Data]** Updated Loadouts for Tornado GR4, F-15E and F-16C
|
||||
* **[Data]** Corrected some unit data
|
||||
* **[UI]** Fixed various UI issues (for example Scaling and HighDPI)
|
||||
* **[UI]** Typhoon GR4 and IDS images
|
||||
|
||||
|
||||
# 5.1.0
|
||||
|
||||
Saves from 5.0.0 are compatible with 5.1.0
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.7.9.17830 and newer, including the HTS and ECM pod.
|
||||
* **[Campaign]** Add option to manually add and remove squadrons and different aircraft type in the new game wizard / air wing configuration dialog.
|
||||
* **[Mission Generation]** Add Option to enforce the Easy Communication setting for the mission
|
||||
* **[Mission Generation]** Add Option to select between only night missions, day missions or any time (default).
|
||||
* **[Modding]** Add F-104 mod support
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed some minor issues in campaigns which generated error messages in the log.
|
||||
* **[Campaign]** Changed the way how map object / scenery kills where tracked. This fixes issues with kill recognition after map updates from ED which change the object ids and therefore prevent correct kill recognition.
|
||||
* **[Mission Generation]** Fixed incorrect radio specification for the AN/ARC-222.
|
||||
* **[Mission Generation]** Fixed mission scripting error when using a dedicated server.
|
||||
* **[Mission Generation]** Fixed an issue where empty convoys lead to an index error when a point capture made a pending transfer of units not completable anymore.
|
||||
* **[Mission Generation]** Corrected Viggen FR22 & FR24 preset channels for the DCS 2.7.9 update
|
||||
* **[Mission Generation]** Fixed the SA-5 Generator to use the P-19 FlatFace SR as a Fallback radar if the faction does not have access to the TinShield SR.
|
||||
* **[UI]** Enable / Disable the settings, save and stats actions if no game is loaded to prevent an error as these functions can only be used on a valid game.
|
||||
* **[UI]** Added missing icons for Tornado GR4, and Tornado IDS.
|
||||
|
||||
# 5.0.0
|
||||
|
||||
Saves from 4.x are not compatible with 5.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Campaign]** Weather! Theaters now experience weather that is more realistic for the region and its current season. For example, Persian Gulf will have very hot, sunny summers and Marianas will experience lots of rain during fall. These changes affect pressure, temperature, clouds and precipitation. Additionally, temperature will drop during the night, by an amount that is somewhat realistic for the region.
|
||||
* **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated.
|
||||
* **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts.
|
||||
* **[Campaign]** FOBs control point can have FARP/helipad slot and host helicopters. To enable this feature on a FOB, add "Invisible FARP" statics objects near the FOB location in the campaign definition file.
|
||||
* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status.
|
||||
* **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers.
|
||||
* **[Campaign]** Skipped turns are no longer counted as defeats on front lines.
|
||||
* **[Campaign AI]** Overhauled campaign AI target prioritization.
|
||||
* **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI.
|
||||
* **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points.
|
||||
* **[Campaign AI]** Auto-planning mission range limits are now specified per-aircraft. On average this means that longer range missions will now be plannable. The limit only accounts for the direct distance to the target, not the path taken.
|
||||
* **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft.
|
||||
* **[Campaign AI]** Aircraft will now only be automatically purchased or assigned at appropriate bases. Naval aircraft will default to only operating from carriers, Harriers will default to LHAs and shore bases, helicopters will operate from anywhere. This can be customized per-squadron.
|
||||
* **[Engine]** Support for DCS 2.7.7.14727 and newer, including support for F-16 CBU-105s, SA-5s, and the Forrestal.
|
||||
* **[Kneeboard]** Minimum required fuel estimates have been added to the kneeboard for aircraft with supporting data (currently only the Hornet and Viper).
|
||||
* **[Kneeboard]** QNH (pressure MSL) and temperature have been added to the kneeboard.
|
||||
* **[Mission Generation]** EWRs are now also headed towards the center of the conflict
|
||||
* **[Mission Generation]** FACs can now use FC3 compatible laser codes. Note that this setting is global, not per FAC.
|
||||
* **[Modding]** Can now install custom campaigns to <DCS saved games>/Liberation/Campaigns instead of the Liberation install directory.
|
||||
* **[Modding]** Campaigns can now define a default start date.
|
||||
* **[Modding]** Campaigns now specify the squadrons that are present in the campaign, their roles, and their starting bases. Players can customize this at game start but the campaign will choose the defaults.
|
||||
* **[New Game Wizard]** Can now customize the player's air wing before campaign start to disable, relocate, or rename squadrons.
|
||||
* **[Plugins]** Updated SkynetIADS to 2.4.0 (adds SA-5 support).
|
||||
* **[UI]** Sell Button for aircraft will be disabled if there are no units available to be sold or all are already assigned to a mission
|
||||
* **[UI]** Enemy aircraft inventory now viewable in the air wing menu.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Naval control points will no longer claim ground objectives during campaign generation and prevent them from spawning.
|
||||
* **[Campaign]** Units aboard sunk cargo ships will now have their losses tracked properly.
|
||||
* **[Mission Generation]** Mission results and other files will now be opened with enforced utf-8 encoding to prevent an issue where destroyed ground units were untracked because of special characters in their names.
|
||||
* **[Mission Generation]** Fixed generation of landing waypoints so that the AI obeys them.
|
||||
* **[Mission Generation]** AI carrier aircraft with a start time of T+0 will now start at T+1s to avoid traffic jams.
|
||||
* **[Mission Generation]** Fixed cases of unused aircraft not being spawned at airfields as soon as any airport filled up.
|
||||
* **[Mission Generation]** Fixed cases with multiple client flights of the same airframe all received the same preset channels.
|
||||
* **[Mission Generation]** F-14A is now generated with stored alignment.
|
||||
* **[Mission Generation]** Su-33s set to cold or warm start on the Kuznetsov will always be generated as runway starts to avoid the AI getting stuck.
|
||||
* **[Mission Generation]** Fixed AI not receiving anti-ship tasks against carriers and LHAs.
|
||||
* **[Mods]** Fixed broken A-4 support causing no weapons to be available.
|
||||
* **[UI]** Selling of Units is now visible again in the UI dialog and shows the correct amount of sold units
|
||||
* **[UI]** Fixed bug where an incompatible campaign could be generated if no action is taken on the campaign selection screen.
|
||||
|
||||
# 4.1.1
|
||||
|
||||
Saves from 4.1.0 are compatible with 4.1.1.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed broken support for Mariana Islands map.
|
||||
* **[Mission Generation]** Fix SAM sites pointing towards the center of the conflict.
|
||||
* **[Flight Planning]** No longer using Su-34 for CAP missions.
|
||||
|
||||
# 4.1.0
|
||||
|
||||
Saves from 4.0.0 are compatible with 4.1.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Campaign]** Air defense sites now generate a fixed number of launchers per type.
|
||||
* **[Campaign]** Added support for Mariana Islands map.
|
||||
* **[Campaign AI]** Adjustments to aircraft selection priorities for most mission types.
|
||||
* **[Engine]** Support for DCS 2.7.4.9632 and newer, including the Marianas map, F-16 JSOWs, NASAMS, and Tin Shield EWR.
|
||||
* **[Flight Planning]** CAP patrol altitudes are now set per-aircraft. By default the altitude will be set based on the aircraft's maximum speed.
|
||||
* **[Flight Planning]** CAP patrol speeds are now set per-aircraft to be more suitable/sensible. By default the speed will be set based on the aircraft's maximum speed.
|
||||
* **[Mission Generation]** Improvements for better support of the Skynet Plugin and long range SAMs are now acting as EWR
|
||||
* **[Mission Generation]** SAM sites are now headed towards the center of the conflict
|
||||
* **[Mods]** Support for latest version of Gripen mod. In-progress campaigns may need to re-plan Gripen flights to pick up updated loadouts.
|
||||
* **[Plugins]** Increased time JTAC Autolase messages stay visible on the UI.
|
||||
* **[Plugins]** Updated SkynetIADS to 2.2.0 (adds NASAMS support).
|
||||
* **[UI]** Added ability to take notes and have those notes appear as a kneeboard page.
|
||||
* **[UI]** Hovering over the weather information now dispalys the cloud base (meters and feet).
|
||||
* **[UI]** Google search link added to unit information when there is no information provided.
|
||||
* **[UI]** Control point name displayed with ground object group name on map.
|
||||
* **[UI]** Buy or Replace will now show the correct price for generated ground objects like sams.
|
||||
* **[UI]** Improved logging for frontline movement to be more descriptive about what happened and why.
|
||||
* **[UI]** Brought ruler map module into source, which should fix file integrity issues with the module.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign]** Fixed the Silkworm generator to include launchers and not all radars.
|
||||
* **[Data]** Fixed Introduction dates for targeting pods (ATFLIR and LITENING were both a few years too early).
|
||||
* **[Data]** Removed SA-10 from Syria 2011 faction.
|
||||
* **[Economy]** EWRs can now be bought and sold for the correct price and can no longer be used to generate money
|
||||
* **[Flight Planning]** Helicopters are now correctly identified, and will fly ingress/CAS/BAI/egress and similar at low altitude.
|
||||
* **[Flight Planning]** Fixed potential issue with angles > 360° or < 0° being generated when summing two angles.
|
||||
* **[Mission Generation]** The lua data for other plugins is now generated correctly
|
||||
* **[Mission Generation]** Fixed problem with opfor planning missions against sold ground objects like SAMs
|
||||
* **[Mission Generation]** The legacy always-available tanker option no longer prevents mission creation.
|
||||
* **[Mission Generation]** Prevent the creation of a transfer order with 0 units for a rare situtation when a point was captured.
|
||||
* **[Mission Generation]** Planned transfers which will be impossible after a base capture will no longer prevent the mission result submit.
|
||||
* **[Mission Generation]** Fix occasional KeyError preventing mission generation when all units of the same type in a convoy were killed.
|
||||
* **[Mission Generation]** Fix for AAA Flak generator using Opel Blitz preventing the mission from being generated because duplicate unit names were used.
|
||||
* **[Mission Generation]** Fixed a potential bug with laser code generation where it would generate invalid codes.
|
||||
* **[UI]** Statistics window tick marks are now always integers.
|
||||
* **[UI]** Statistics window now shows the correct info for the turn
|
||||
* **[UI]** Toggling custom loadout for an aircraft with no preset loadouts no longer breaks the flight.
|
||||
|
||||
# 4.0.0
|
||||
|
||||
Saves from 3.x are not compatible with 4.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** Support for DCS 2.7.2.7910.1 and newer, including Cyprus, F-16 JDAMs, and the Hind.
|
||||
* **[Campaign]** Squadrons now (optionally, off by default) have a maximum size and killed pilots replenish at a limited rate.
|
||||
* **[Campaign]** Added an option to disable levelling up of AI pilots.
|
||||
* **[Campaign]** Added Russian Intervention 2015 campaign on Syria, for a small and somewhat realistic Russian COIN scenario.
|
||||
* **[Campaign]** Added Operation Atilla campaign on Syria, for a reasonably large invasion of Cyprus scenario.
|
||||
* **[Campaign AI]** AI will plan Tanker flights.
|
||||
* **[Campaign AI]** Removed max distance for AEW&C auto planning.
|
||||
* **[Economy]** Adjusted prices for aircraft to balance out some price inconsistencies.
|
||||
* **[Factions]** Added more tankers to factions.
|
||||
* **[Flight Planner]** Added ability to plan Tankers.
|
||||
* **[Modding]** Campaign format version is now 7.0 to account for DCS map changes that made scenery strike targets incompatible with existing campaigns.
|
||||
* **[Mods]** Added support for the Gripen mod.
|
||||
* **[Mods]** Removes MB-339PAN support, as the mod is now deprecated and no longer works with DCS 2.7+.
|
||||
* **[Mission Generation]** Added support for "Neutral Dot" label options.
|
||||
* **[New Game Wizard]** Mods are now selected via checkboxes in the new game wizard, not as separate factions.
|
||||
* **[UI]** Ctrl click and shift click now buy or sell 5 or 10 units respectively.
|
||||
* **[UI]** Multiple waypoints can now be deleted simultaneously if multiple waypoints are selected.
|
||||
* **[UI]** Carriers and LHAs now match the colour of airfields, and their destination icons are translucent.
|
||||
* **[UI]** Updated intel box text for first turn.
|
||||
* **[UI]** Base Capture Cheat is now usable at all bases and can also be used to transfer player-owned bases to OPFOR.
|
||||
* **[UI]** Pass Turn button is relabled as "Begin Campaign" on Turn 0.
|
||||
* **[UI]** Added a ruler to the map.
|
||||
* **[UI]** Liberation now saves games to `<DCS user directory>/Liberation/Saves` by default to declutter the main directory.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix procurement for factions that lack some unit types.
|
||||
* **[Campaign AI]** Fix auto purchase of aircraft for factions that have no transport aircraft.
|
||||
* **[Campaign AI]** Fix refunding of pending aircraft purchases when a side has no factory available.
|
||||
* **[Mission Generation]** Fixed problem with mission load when control point name contained an apostrophe.
|
||||
* **[Mission Generation]** Fixed EWR group names so they contribute to Skynet again.
|
||||
* **[Mission Generation]** Fixed duplicate name error when generating convoys and cargo ships when creating manual transfers after loading a game.
|
||||
* **[Mission Generation]** Fixed empty convoys not being disbanded when all units are killed/removed.
|
||||
* **[Mission Generation]** Fixed player losing frontline progress when skipping from turn 0 to turn 1.
|
||||
* **[Mission Generation]** Fixed issue where frontline would only search to the right for valid locations.
|
||||
* **[UI]** Made non-interactive map elements less obstructive.
|
||||
* **[UI]** Added support for Neutral Dot difficulty label
|
||||
* **[UI]** Clear skies at night no longer described as "Sunny" by the weather widget.
|
||||
* **[UI]** Removed ability to buy (useless) ground units at carriers and LHAs.
|
||||
* **[UI]** Fixed enable/disable of buy/sell buttons.
|
||||
* **[UI]** EWRs now appear in the custom waypoint list.
|
||||
|
||||
# 3.0.0
|
||||
|
||||
Saves from 2.5 are not compatible with 3.0.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Campaign]** Ground units can now be transferred by road, airlift, and cargo ship. See https://github.com/dcs-liberation/dcs_liberation/wiki/Unit-Transfers for more information.
|
||||
* **[Campaign]** Ground units can no longer be sold. To move units to a new location, transfer them.
|
||||
* **[Campaign]** Ground units must now be recruited at a base with a factory and transferred to their destination. When buying units in the UI, the purchase will automatically be fulfilled at the closest factory, and a transfer will be created on the next turn.
|
||||
* **[Campaign]** Non-control point FOBs will no longer spawn.
|
||||
* **[Campaign]** Added squadrons and pilots. See https://github.com/dcs-liberation/dcs_liberation/wiki/Squadrons-and-pilots for more information.
|
||||
* **[Campaign]** Capturing a base now depopulates all of its attached objectives with units: air defenses, EWRs, ships, armor groups, etc. Buildings are captured.
|
||||
* **[Campaign]** Ammunition Depots determine how many ground units can be deployed on the frontline by a control point.
|
||||
* **[Campaign AI]** AI now considers Ju-88s for CAS, strike, and DEAD missions.
|
||||
* **[Campaign AI]** AI planned AEW&C missions will now be scheduled ASAP.
|
||||
* **[Campaign AI]** AI now considers the range to the SAM's threat zone rather than the range to the SAM itself when determining target priorities.
|
||||
* **[Campaign AI]** Auto purchase of ground units will now maintain unit composition instead of buying randomly. The unit composition is predefined.
|
||||
* **[Campaign AI]** Auto purchase will aim to purchase enough ground units to support the frontline, plus 30% reserve units.
|
||||
* **[Campaign AI]** Auto purchase will now adjust its air/ground balance to favor whichever is under-funded.
|
||||
* **[Flight Planner]** Desired mission length is now configurable (defaults to 60 minutes). A BARCAP will be planned every 30 minutes. Other packages will simply have their takeoffs spread out or compressed such that the last flight will take off around the mission end time.
|
||||
* **[Flight Planner]** Flight plans now include bullseye waypoints.
|
||||
* **[Flight Planner]** Differentiated SEAD and SEAD escort. SEAD is tasked with suppressing the package target, SEAD escort is tasked with protecting the package from all SAMs along its route.
|
||||
* **[Flight Planner]** Planned airspeed increased to 0.85 mach for supersonic airframes and 85% of max speed for subsonic.
|
||||
* **[Flight Planner]** Taxi time estimation for airfields increased from 5 minutes to 8 minutes.
|
||||
* **[Flight Planner]** Reduce expected error margin for flight plans from 10% to 5%.
|
||||
* **[Flight Planner]** SEAD flights are scheduled one minute ahead of the package's TOT so that they can suppress the site ahead of the strike.
|
||||
* **[Flight Planner]** Automatic ATO generation for the player's coalition can now be disabled in the settings.
|
||||
* **[Payloads]** AI flights for most air to ground mission types (CAS excluded) will have their guns emptied to prevent strafing fully armed and operational battle stations. Gun-reliant airframes like A-10s and warbirds will keep their bullets.
|
||||
* **[Kneeboard]** ATC table overflow alleviated by wrapping long airfield names and splitting ATC frequency and channel into separate rows.
|
||||
* **[UI]** Overhauled the map implementation. Now uses satellite imagery instead of low res map images. Display options have moved from the toolbar to panels in the map.
|
||||
* **[UI]** Campaigns generated for an older or newer version of the game will now be marked as incompatible. They can still be played, but bugs may be present.
|
||||
* **[UI]** DCS loadouts are now selectable in the loadout setup menu.
|
||||
* **[UI]** Added global aircraft inventory view under Air Wing dialog.
|
||||
* **[UI]** Base menu now shows information about ground unit deployment limits.
|
||||
* **[Modding]** Campaigns now choose locations for factories to spawn.
|
||||
* **[Modding]** Campaigns now choose locations for ammunition depots to spawn.
|
||||
* **[Modding]** Campaigns now use map structures as strike targets.
|
||||
* **[Modding]** Campaigns may now set *any* objective type to be a required spawn rather than random chance. Support for random objective generation was removed.
|
||||
* **[Modding]** Campaigns may now place AAA objectives.
|
||||
* **[Modding]** Can now install custom factions to <DCS saved games>/Liberation/Factions instead of the Liberation install directory.
|
||||
* **[Performance Settings]** Added a settings to lower the number of smoke effects generated on frontlines. Lowered default settings for frontline smoke generators, so less smoke should be generated by default.
|
||||
* **[Configuration]** Liberation preferences (DCS install and save game location) are now saved to `%LOCALAPPDATA%/DCSLiberation` to prevent needing to reconfigure each new install.
|
||||
* **[Skynet]** Updated to 2.1.0.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaign AI]** Fix purchase of aircraft by priority (the faction's list was being used as the priority list rather than the game's).
|
||||
* **[Campaign AI]** Fixed bug causing AI to over-purchase cheap aircraft.
|
||||
* **[Campaign AI]** Auto planner will no longer attempt to plan missions for which the faction has no compatible aircraft.
|
||||
* **[Campaign AI]** Stop purchasing aircraft after the first unaffordable package to attempt to complete more packages rather than filling airfields with cheap escorts that will never be used.
|
||||
* **[Campaign]** Fixed bug where offshore strike locations were being used to spawn ship objectives.
|
||||
* **[Campaign]** EWR sites are now purchasable.
|
||||
* **[Flight Planner]** AI strike flight plans now include the correct target actions for building groups.
|
||||
* **[Flight Planner]** AI BAI/DEAD/SEAD flights now have tasks to attack all groups at the target location, not just the primary group (for multi-group SAM sites).
|
||||
* **[Flight Planner]** Fixed some contexts where damaged runways would be used. Destroying a carrier will no longer break the game.
|
||||
|
||||
# 2.5.1
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[UI]** Engagement ranges are now displayed by default.
|
||||
* **[UI]** Engagement range display generalized to work for all patrolling flight plans (BARCAP, TARCAP, and CAS).
|
||||
* **[Flight Planner]** Front lines no longer project threat zones to avoid pushing BARCAPs back so much. TARCAPs will be forcibly planned but strike packages will not route around front lines even if it is reasonable to do so.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Campaigns]** EWRs associated with a base will now only be generated near the base.
|
||||
* **[Flight Planner]** Fixed error when generating AEW&C flight plans in campaigns with no front lines.
|
||||
|
||||
# 2.5.0
|
||||
|
||||
Saves from 2.4 are not compatible with 2.5.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Engine]** DCS 2.7 Support
|
||||
* **[UI]** Improved FOB menu, added a custom banner, and do not display aircraft recruitment menu
|
||||
* **[Flight Planner]** Added AEW&C missions. (by siKruger)
|
||||
* **[Kneeboard]** Added dark kneeboard option (by GvonH)
|
||||
* **[Campaigns]** Multiple EWR sites may now be generated, and EWR sites may be generated outside bases (by SnappyComebacks)
|
||||
* **[Mission Generation]** Cloudy and rainy (but not thunderstorm) weather will use the cloud presets from DCS 2.7.
|
||||
* **[Plugins]** Added LotATC export plugin (by drsoran)
|
||||
* **[Plugins]** Added Splash Damage Plugin (by Wheelijoe)
|
||||
* **[Loadouts]** Replaced Litening with ATFLIR for all default F/A-18C loadouts.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Flight Planner]** Front lines now project threat zones, so TARCAP/escorts will not be pruned for flights near the front. Packages may also route around the front line when practical.
|
||||
* **[Flight Planner]** Fixed error when planning BAI at SAMs with dead subgroups.
|
||||
* **[Flight Planner]** Mig-19 was not allowed for CAS roles fixed
|
||||
* **[Flight Planner]** Increased size of navigation planning area to avoid plannign failures with distant waypoints.
|
||||
* **[Flight Planner]** Fixed UI refresh when unchecking the "default loadout" box in the loadout editor.
|
||||
* **[Objective names]** Fixed typos in objective name : ARMADILLLO -> ARMADILLO (by SnappyComebacks)
|
||||
* **[Payloads]** F-86 Sabre was missing a custom payload
|
||||
* **[Payloads]** Added GAR-8 period restrictions (by Mustang-25)
|
||||
* **[Campaign]** Date now progresses.
|
||||
* **[Campaign]** Added game over message when a coalition runs out of functioning airbases.
|
||||
* **[Mission Generation]** Fixed "invalid face handle" error in kneeboard generation that occurred on some machines.
|
||||
|
||||
## Regressions
|
||||
|
||||
* **[Mod Support]** Stopped support for 2.5.5 Rafale Mode, and removed factions that were using it
|
||||
* **[Mod Support]** Su-57 mod support might be out of date
|
||||
|
||||
# 2.4.3
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[New Game Wizard]** Added the possibility to setup custom start date
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Mods]** Updated C-130J mod data to version 6.4
|
||||
* **[Mods]** Updated F-22A mod to latest version
|
||||
|
||||
# 2.4.2
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Factions]** Introduction dates and fallback weapons added for US, Russian, UK, and French weapons. Huge thanks to @TheCandianVendingMachine for the massive amount of data entry!
|
||||
* **[Campaigns]** Added 1995 start dates.
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Economy]** Pending ground unit purchases will also be transferred when a connected base is captured.
|
||||
* **[UI]** Fixed rounding of budget in recruitment menu.
|
||||
|
||||
# 2.4.1
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Units]** Fixed syntax error with the SH-60B payload file.
|
||||
* **[Culling]** Missile sites generate reasonably sized non-cull zones rather than 100km ones.
|
||||
* **[UI]** Budget display is also now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed some areas where the old, non-pretty name was displayed to users.
|
||||
|
||||
# 2.4.0
|
||||
|
||||
Saves from 2.3 are not compatible with 2.4.
|
||||
|
||||
## Highlights
|
||||
|
||||
* Improved flight plan generation to avoid loitering in or traveling through threatened areas when practical.
|
||||
* Improved AI aircraft purchasing behavior.
|
||||
* Era-restricted weapons (work in progress).
|
||||
* Tons of UI polish.
|
||||
* Rebalanced economy to keep opfor competitive over the course of the game.
|
||||
|
||||
## Features/Improvements
|
||||
|
||||
* **[Flight Planner]** Air-to-air and SEAD escorts will no longer be automatically planned for packages that are not in range of threats.
|
||||
* **[Flight Planner]** Non-custom flight plans will now navigate around threat areas en route to the target area when practical.
|
||||
* **[Flight Planner]** Flight plans along front lines now ensure that the race track start is closer to the departure airfield than the race track end.
|
||||
* **[Campaign AI]** Auto-purchase now prefers airfields that are not within range of the enemy.
|
||||
* **[Campaign AI]** Auto-purchase now prefers the best aircraft for the task, but will attempt to maintain some variety.
|
||||
* **[Campaign AI]** Opfor now sells off odd aircraft since they're unlikely to be used.
|
||||
* **[Campaign AI]** Multiple rounds of CAP will be planned (roughly 90 minutes of coverage). Default starting budget has increased to account for the increased need for aircraft.
|
||||
* **[Mission Generator]** Multiple groups are created for complex SAM sites (SAMs with additional point defense or SHORADS), improving Skynet behavior.
|
||||
* **[Mission Generator]** Default start type can now be chosen in the settings. This replaces the non-functional "AI Parking Start" option. **Selecting any type other than cold will break OCA/Aircraft missions.**
|
||||
* **[Cheat Menu]** Added ability to toggle base capture and frontline advance/retreat cheats.
|
||||
* **[Skynet]** Updated to 2.0.1.
|
||||
* **[Skynet]** Point defenses are now configured to remain on to protect the site they accompany.
|
||||
* **[Hercules]** Updated the Hercules Cargo list file.
|
||||
* **[Balance]** Opfor now gains income using the same rules as the player, significantly increasing their income relative to the player for most campaigns.
|
||||
* **[Balance]** Units now retreat from captured bases when able. Units with no retreat path will be captured and sold.
|
||||
* **[Economy]** FOBs generate only $10M per turn (previously $20M like airbases).
|
||||
* **[Economy]** Carriers and off-map spawns generate no income (previously $20M like airbases).
|
||||
* **[Economy]** Sales of aircraft and ground vehicles can now be cancelled before the next turn begins.
|
||||
* **[UI]** Multi-SAM objectives now show threat and detection rings per group.
|
||||
* **[UI]** New icon for AA sites with no active threat.
|
||||
* **[UI]** Unit names are now prettier and more accurate, and can now be set per-country for added historical flavour.
|
||||
* **[UI]** Default loadout is now shown for flights with no custom loadout selected.
|
||||
* **[UI]** Aircraft for a new flight are now only selectable if they match the task type for that flight.
|
||||
* **[UI]** WIP - There is now a unit info button for each unit in the recruitment list, that should help newer players learn what each unit does.
|
||||
* **[UI]** Docs for time-on-target and creating new theaters/factions/loadouts are now linked in the UI at the appropriate places.
|
||||
* **[UI]** ASAP is now a checkbox rather than a button. Enabling this will disable the TOT selector but changes to the package structure will automatically re-ASAP the package.
|
||||
* **[UI]** Arrival airfield is now shown in the flight list if it differs from the departure airfield.
|
||||
* **[UI]** Start type can now be selected when creating a flight.
|
||||
* **[UI]** Arrival and divert airfields can be edited after the flight is created.
|
||||
* **[Factions]** Added option for date-based loadout restriction. Active radar homing missiles are handled, patches welcome for the other thousand weapons.
|
||||
* **[Factions]** Added Poland 2010 faction.
|
||||
* **[Factions]** Added Greece 2005 faction.
|
||||
* **[Factions]** Added Iran 1988 faction.
|
||||
* **[Units]** Support for E-2 Hawkeye, SH-60B Seahawk, S-3B Viking (thanks to awinterquest) and SpGH Dana - these are now being used by appropriate factions.
|
||||
* **[Culling]** Missile sites are no longer culled.
|
||||
* **[Campaigns]** Added campaign "Black Sea Lite" by Starfire
|
||||
* **[Campaigns]** Added campaign "Exercise Vegas Nerve" by Starfire
|
||||
* **[New game Wizard]** The theater page is now the first page of the campaign wizard, recommended factions will be selected automatically on the faction selection page
|
||||
* **[New game Wizard]** Added information text about the selected campaign performance.
|
||||
* **[Mod Support]** Added support for High Digit SAMs mod 1.4.0
|
||||
* **[Mod Support]** Added SAMs sites generator : KS19Generator, SA10BGenerator, SA12Generator, SA17Generator, SA20Generator, SA20BGenerator, SA23Generator
|
||||
|
||||
## Fixes
|
||||
|
||||
* **[Hercules]** Updated the default Hercules radio frequency.
|
||||
* **[Economy]** Pending unit orders at captured bases will be refunded.
|
||||
* **[UI]** Carrier group SAM threat rings now move with the carrier.
|
||||
* **[UI]** Base intel menu no longer compresses text, and is now scrollable.
|
||||
* **[UI]** Edit Flight window is now dynamically sized to adapt to the width of waypoint names, so they no longer get truncated.
|
||||
* **[UI]** Budget income display is now rounded to 2 decimal places.
|
||||
* **[UI]** Fixed incorrect income per turn displayed for strike target tooltip.
|
||||
* **[Factions]** USA with C-130 faction now links to the required mod.
|
||||
* **[Campaign]** Fixed issue where destroyed buildings would sometimes not count as destroyed and thus respawn.
|
||||
* **[Campaign]** Fixed issue where destroyed runways were not registered.
|
||||
* **[Units]** J-11A is no longer spawned with empty loadout.
|
||||
* **[Units]** F-14B is no longer spawned with empty loadout for fighter sweep tasks.
|
||||
* **[Units]** Pyotr Velikiy cruiser has been removed for now as it's nearly unkillable.
|
||||
* **[Units]** Submarines have been removed for now as they aren't wholly functional.
|
||||
* **[Units]** Fixed "FACTION ERROR : Unable to find OliverHazardPerryGroupGenerator in pydcs" error at startup.
|
||||
* **[Mission Generator]** Fixed a bug where units set to Aggressive stance sometimes did not move.
|
||||
* **[Mission Generator]** Flyover points for OCA/Aircraft missions are now generated correctly.
|
||||
* **[Flight Planner]** Fixed not being able to create custom waypoints for buildings.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against SAMs.
|
||||
* **[Flight Planner]** Strike missions will no longer be automatically planned against FOB structures.
|
||||
|
||||
# 2.3.4
|
||||
|
||||
## Fixes:
|
||||
[Mission Generator] Mission generator would crash when generating fire missions for destroyed SCUD sites - fixed
|
||||
|
||||
# 2.3.3
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaigns]** Reworked Golan Heights campaign on Syria, (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Added a lite version of the Golan Heights campaign
|
||||
* **[Campaigns]** Reworked Syrian Civil War campaign (Added FOB and preset locations for SAMS)
|
||||
* **[Campaigns]** Reworked Emirates campaign
|
||||
* **[Campaigns]** AA units added to frontlines and updated all factions to include some frontline AA units.
|
||||
* **[Mission Generator]** Infantry will only be generated for APC and IFV groups
|
||||
* **[Mission Generator]** Infantry squads size is not randomized anymore
|
||||
* **[Mission Generator]** Infantry squads can have a mortar.
|
||||
* **[Mission Generator]** SCUD missiles sites will now fire on enemy controls points in range when possible
|
||||
* **[Factions]** Updated Nato Desert Storm to include F-14A
|
||||
* **[Factions]** Updated Iraq 1991 factions to include Zsu-57 and Mig-29A
|
||||
* **[Factions]** Germany 1944, added Stug III and Stug IV
|
||||
* **[Factions]** Added factions Insurgents (Hard) with better and more weapons
|
||||
* **[Plugins]** [The EWRS plugin](https://github.com/Bob7heBuilder/EWRS) is now included.
|
||||
* **[UI]** Added enemy intelligence summary and details window.
|
||||
|
||||
## Fixes:
|
||||
* **[Factions]** AI would never buy artillery units for the frontline - fixed
|
||||
* **[Factions]** Removed the F-111 unit from the NATO desert storm faction. (Recruiting it would cause crashes in DCS, since it is not a valid unit)
|
||||
* **[Campaign]** Automatic redeployment of ground units would sometimes fail - fixed
|
||||
* **[Mission Generator]** Artillery groups would retreat in the wrong direction - fixed
|
||||
* **[Units]** Fixed SPG_Stryker_M1128_MGS not being in db
|
||||
* **[UI]** Fixed and added many missing ground units icons
|
||||
* **[UI]** Ship groups could be replaced by SAM sites in the UI, which would lead to broken mission being generated - fixed
|
||||
* **[New Game Wizard]** Removed the "mid game" campaign generator option which is currently broken
|
||||
* **[Mission Generator]** Empty navy groups will no longer be generated
|
||||
* **[Mission Generator]** Fixed BAI, SEAD, and DEAD flights ocassionally being assigned the wrong targets.
|
||||
* **[Flight Planner]** Fixed not being able to plan packages against opfor carriers
|
||||
* **[UI]** Repaired SAMs no longer show as dead.
|
||||
* **[UI]** Fixed not being able to manage a disbanded site after disbanding and closing the base menu.
|
||||
|
||||
# 2.3.2
|
||||
|
||||
## Features/Improvements
|
||||
* **[Units]** Support for newly added BTR-82A, T-72B3
|
||||
* **[Units]** Added ZSU-57 AAA sites
|
||||
* **[Culling]** BARCAP missions no longer create culling exclusion zones.
|
||||
* **[Flight Planner]** Improved TOT planning. Negative start times no longer occur with TARCAPs and hold times no longer affect planning for flight plans without hold points.
|
||||
* **[Factions]** Added Iraq 1991 faction (thanks again to Hawkmoon!)
|
||||
|
||||
## Fixes:
|
||||
* **[Mission Generator]** Fix mission generation error when there are too many radio frequency to setup for the Mig-21
|
||||
* **[Mission Generator]** Fix ground units not moving forward
|
||||
* **[Mission Generator]** Fixed assigned radio channels overlapping with beacons.
|
||||
* **[Flight Planner]** Fix creation of custom waypoints.
|
||||
* **[Campaigns]** Fixed many cases of SAMs spawning on the runways/taxiways in Syria Full.
|
||||
|
||||
# 2.3.1
|
||||
|
||||
## Features/Improvements
|
||||
* **[UX]** Added a warning message when the player is attempting to buy more planes at an already full airbase.
|
||||
* **[Campaigns]** Migrated Syria full map to new format. (Thanks to Hawkmoon)
|
||||
* **[Faction]** Added NATO desert Storm faction (Thanks to Hawkmoon)
|
||||
|
||||
## Fixes:
|
||||
* **[AI]** CAP flights will engage enemies again.
|
||||
* **[Campaigns]** Fixed a missing path on the Caucasus Full Map campaign
|
||||
|
||||
# 2.3.0
|
||||
|
||||
## Features/Improvements
|
||||
* **[Campaign Map]** Overhauled the campaign model
|
||||
* **[Campaign Map]** Possible to add FOB as control points
|
||||
* **[Campaign Map]** Added off-map spawn locations
|
||||
* **[Campaign AI]** Overhauled AI recruiting behaviour
|
||||
* **[Campaign AI]** Added AI procurement for Blue
|
||||
* **[Campaign]** New Campaign: "Black Sea"
|
||||
* **[Mission Planner]** Possible to move carrier and tarawa on the campaign map
|
||||
* **[Mission Generator]** Infantry squads on frontline can have manpads
|
||||
* **[Mission Generator]** Unused aircraft now spawned to allow for OCA strikes
|
||||
* **[Mission Generator]** Opfor now obeys parking limits
|
||||
* **[Mission Generator]** Support for Anubis C-130 Hercules mod
|
||||
* **[Flight Planner]** Added fighter sweep missions.
|
||||
* **[Flight Planner]** Added BAI missions.
|
||||
* **[Flight Planner]** Added anti-ship missions.
|
||||
* **[Flight Planner]** Differentiated BARCAP and TARCAP. TARCAP is now for hostile areas and will arrive before the package.
|
||||
* **[Flight Planner]** Added OCA missions
|
||||
* **[Flight Planner]** Added Alternate/divert airfields
|
||||
* **[Culling]** Added possibility to include/exclude carriers from culling zones
|
||||
* **[QOL]** On liberation startup, your latest save game is loaded automatically
|
||||
* **[Units]** Reduced starting fuel load for C101
|
||||
* **[UI]** Inform the user of the weather
|
||||
* **[UI]** Added toolbar buttons to change map display settings
|
||||
* **[Game]** Added new Economy options for adjusting income multipliers and starting budgets.
|
||||
|
||||
## Fixes :
|
||||
* **[Map]** Missiles sites now have a proper icon and will not re-use the SAM sites icon
|
||||
* **[Mission Generator]** Ground unit waypoints improperly set to "On Road" - fixed
|
||||
* **[Mission Generator]** Target waypoints not at ground level - fixed
|
||||
* **[Mission Generator]** Selected skill not applied to Helicopters - fixed
|
||||
* **[Mission Generator]** Ground units do not always spawn - fixed
|
||||
* **[Kneeboard]** Briefing waypoints off by one - fixed
|
||||
* **[Game]** Destroyed buildings still granting budget - fixed
|
||||
|
||||
# 2.2.1
|
||||
|
||||
## Features/Improvements
|
||||
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
|
||||
* **[Factions]** Added map Persian Gulf full by Plob
|
||||
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
|
||||
* **[UI]** Mission start screen now informs players about delayed flights.
|
||||
* **[Units]** Added support for F-14A-135-GR
|
||||
* **[Modding]** Possible to setup liveries overrides in factions definition files
|
||||
|
||||
## Fixes :
|
||||
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
|
||||
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
|
||||
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
|
||||
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
|
||||
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
|
||||
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
|
||||
|
||||
# 2.2.0
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Campaign Generator]** Added early warning radar generation
|
||||
* **[Campaign Generator]** Added scud launcher sites
|
||||
* **[Cheat Menu]** Added ability to capture base from mission planner
|
||||
* **[Cheat Menu]** Added ability to show red ATO
|
||||
* **[Factions]** Added WW2 factions that do not depend on WW2 asset pack
|
||||
* **[Factions]** Cold War / Middle eastern factions will use Flak sites
|
||||
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
|
||||
* **[Flight Planner]** Pick runways and ascent/descent based on headwind
|
||||
* **[Map]** Added polygon debug mode display
|
||||
* **[Map]** Highlight the selected flight path on the map
|
||||
* **[Map]** Improved SAM display settings
|
||||
* **[Map]** Improved flight plan display settings
|
||||
* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
|
||||
* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
|
||||
* **[Moddability]** Custom campaigns can be designed through json files
|
||||
* **[Moddability]** LUA plugins can now be injected into Liberation missions.
|
||||
* **[Moddability]** Optional Skynet IADS lua plugin now included
|
||||
* **[New Game]** Starting budget can be freely selected
|
||||
* **[New Game]** Exanded information for faction and campaign selection in the new game wizard
|
||||
* **[UI]** Add double and right click actions to many UI elements.
|
||||
* **[UI]** Add polygon drawing mode for map background
|
||||
* **[UI]** Added a warning if you press takeoff with no player enabled flights
|
||||
* **[UI]** Packages and flights now visible in the main window sidebar
|
||||
* **[Units/Factions]** Added bombers to some coalitions
|
||||
* **[Units/Factions]** Added support for SU-57 mod by Cubanace
|
||||
* **[Units]** Added Freya EWR sites to german WW2 factions
|
||||
* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)
|
||||
* **[Units]** Added support for new P-47 variants
|
||||
|
||||
## Fixes :
|
||||
* **[Campaign Generator]** Big airbases could end up without any airbase defense.
|
||||
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
|
||||
* **[Flight Planner]** Fix waypoint alitudes for helicopters
|
||||
* **[Flight Planner]** Fixed CAS aircraft wandering away from frontline
|
||||
* **[Maps]** Incirlik airbase was missing exclusions zones, so SAMS could end up being generated on the runway
|
||||
* **[Mission Generator]** Fixed player/client confusion when a flight had only one player slot.
|
||||
* **[Radios]** Fix A-10C radio
|
||||
* **[UI]** Many missing unit icons were added
|
||||
* **[UI]** Missing TER weapons in custom payload now selectable.
|
||||
|
||||
# 2.1.5
|
||||
|
||||
## Features/Improvements :
|
||||
@@ -589,7 +10,9 @@ Saves from 2.3 are not compatible with 2.4.
|
||||
# 2.1.4
|
||||
|
||||
## Fixes :
|
||||
* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames.
|
||||
* **[UI]** Fixed an issue that prevent generating the mission (take off button no working) on old savegames.
|
||||
|
||||
# 2.1.3
|
||||
|
||||
## Features/Improvements :
|
||||
* **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions
|
||||
@@ -853,4 +276,4 @@ Sorry :(
|
||||
* **[Mission Generator]** Planned flights will spawn even if their home base has been captured or is being contested by enemy ground units.
|
||||
* **[Campaign Generator]** Base defenses would not be generated on Normandy map and in some rare cases on others maps as well
|
||||
* **[Mission Planning]** CAS waypoints created from the "Predefined waypoint selector" would not be at the exact location of the frontline
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
|
||||
@@ -1,80 +0,0 @@
|
||||
# Measuring estimated fuel consumption
|
||||
|
||||
To estimate fuel consumption numbers for an aircraft, create a mission with a
|
||||
typical heavy load for the aircraft. For example, to measure for the F/A-18C, a
|
||||
loadout with two bags, two GBU-31s, two sidewinders, an AMRAAM, and an ATFLIR.
|
||||
Do **not** drop bags or weapons during the test flight.
|
||||
|
||||
Start the aircraft on the ground at a large airport (for example, Akrotiri) at a
|
||||
parking space at the opposite end of the takeoff runway so you can estimate long
|
||||
taxi fuel consumption.
|
||||
|
||||
When you enter the jet, note the amount of fuel below, then taxi to the far end
|
||||
of the runway. Hold short and note the remaining fuel below.
|
||||
|
||||
Follow a typical takeoff pattern for the aircraft. For the F/A-18C, this might
|
||||
be AB takeoff, reduce to MIL at 350KIAS, and maintian 350KIAS/0.85 mach until
|
||||
cruise altitude (angles 25).
|
||||
|
||||
Once you reach angels 25, pause the game. Note your remaining fuel below and
|
||||
measure the distance traveled from takeoff. Mark your location on the map.
|
||||
|
||||
Level out and increase to cruise speed if needed. Liberation assumes 0.85 mach
|
||||
for supersonic aircraft, for subsonic aircraft it depends so pick something
|
||||
reasonable and note your descision in a comment in the file when done. Maintain
|
||||
speed, heading, and altitude for a long distance (the longer the distance, the
|
||||
more accurate the result, but be careful to leave enough fuel for the final
|
||||
section). Once complete, note the distance traveled and the remaining fuel.
|
||||
|
||||
Finally, increase speed as you would for an attack. At least MIL power,
|
||||
potentially use AB sparingly, etc. The goal is to measure fuel consumption per
|
||||
mile traveled during an attack run.
|
||||
|
||||
```
|
||||
start:
|
||||
taxi end:
|
||||
to 25k distance:
|
||||
at 25k fuel:
|
||||
cruise (.85 mach) distance:
|
||||
cruise (.85 mach) end fuel:
|
||||
combat distance:
|
||||
combat end fuel:
|
||||
```
|
||||
|
||||
Finally, fill out the data in the aircraft data. Below is an example for the
|
||||
F/A-18C:
|
||||
|
||||
```
|
||||
start: 15290
|
||||
taxi end: 15120
|
||||
climb distance: 40NM
|
||||
at 25k fuel: 13350
|
||||
cruise (.85 mach) distance: 100NM
|
||||
cruise (.85 mach) end fuel: 11140
|
||||
combat distance: 100NM
|
||||
combat end fuel: 8390
|
||||
|
||||
taxi = start - taxi end = 15290 - 15120 = 170
|
||||
climb fuel = taxi end - at 25k fuel = 15120 - 13350 = 1770
|
||||
climb ppm = climb fuel / climb distance = 1770 / 40 = 44.25
|
||||
cruise fuel = at 25k fuel - cruise end fuel = 13350 - 11140 = 2210
|
||||
cruise ppm = cruise fuel / cruise distance = 2210 / 100 = 22.1
|
||||
combat fuel = cruise end fuel - combat end fuel = 11140 - 8390 = 2750
|
||||
combat ppm = combat fuel / combat distance = 2750 / 100 = 27.5
|
||||
```
|
||||
|
||||
```yaml
|
||||
fuel:
|
||||
# Parking A1 to RWY 32 at Akrotiri.
|
||||
taxi: 170
|
||||
# AB takeoff to 350/0.85, reduce to MIL and maintain 350 to 25k ft.
|
||||
climb_ppm: 44.25
|
||||
# 0.85 mach for 100NM.
|
||||
cruise_ppm: 22.1
|
||||
# ~0.9 mach for 100NM. Occasional AB use.
|
||||
combat_ppm: 27.5
|
||||
min_safe: 2000
|
||||
```
|
||||
|
||||
The last entry (`min_safe`) is the minimum amount of fuel that the aircraft
|
||||
should land with.
|
||||
@@ -1,3 +1,2 @@
|
||||
from .game import Game
|
||||
from . import db
|
||||
from .version import VERSION
|
||||
from . import db
|
||||
@@ -1,2 +0,0 @@
|
||||
from .campaign import Campaign
|
||||
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
|
||||
@@ -1,183 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import logging
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Dict, Any
|
||||
|
||||
from packaging.version import Version
|
||||
import yaml
|
||||
|
||||
from game.profiling import logged_duration
|
||||
from game.theater import (
|
||||
ConflictTheater,
|
||||
CaucasusTheater,
|
||||
NevadaTheater,
|
||||
PersianGulfTheater,
|
||||
NormandyTheater,
|
||||
TheChannelTheater,
|
||||
SyriaTheater,
|
||||
MarianaIslandsTheater,
|
||||
)
|
||||
from game.version import CAMPAIGN_FORMAT_VERSION, SUPPORTED_CAMPAIGN_VERSION
|
||||
from .campaignairwingconfig import CampaignAirWingConfig
|
||||
from .mizcampaignloader import MizCampaignLoader
|
||||
from .. import persistency
|
||||
|
||||
PERF_FRIENDLY = 0
|
||||
PERF_MEDIUM = 1
|
||||
PERF_HARD = 2
|
||||
PERF_NASA = 3
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Campaign:
|
||||
name: str
|
||||
icon_name: str
|
||||
authors: str
|
||||
description: str
|
||||
|
||||
#: The revision of the campaign format the campaign was built for. We do not attempt
|
||||
#: to migrate old campaigns, but this is used to show a warning in the UI when
|
||||
#: selecting a campaign that is not up to date.
|
||||
version: Tuple[int, int]
|
||||
|
||||
recommended_player_faction: str
|
||||
recommended_enemy_faction: str
|
||||
recommended_start_date: Optional[datetime.date]
|
||||
performance: int
|
||||
data: Dict[str, Any]
|
||||
path: Path
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, path: Path) -> Campaign:
|
||||
with path.open() as campaign_file:
|
||||
if path.suffix == ".yaml":
|
||||
data = yaml.safe_load(campaign_file)
|
||||
else:
|
||||
data = json.load(campaign_file)
|
||||
|
||||
sanitized_theater = data["theater"].replace(" ", "")
|
||||
version_field = data.get("version", "0")
|
||||
try:
|
||||
version = Version(version_field)
|
||||
except TypeError:
|
||||
logging.warning(
|
||||
f"Non-string campaign version in {path}. Parse may be incorrect."
|
||||
)
|
||||
version = Version(str(version_field))
|
||||
|
||||
start_date_raw = data.get("recommended_start_date")
|
||||
|
||||
# YAML automatically parses dates, but while we still support JSON campaigns we
|
||||
# need to be able to handle parsing dates from strings ourselves as well.
|
||||
start_date: Optional[datetime.date]
|
||||
if isinstance(start_date_raw, str):
|
||||
start_date = datetime.date.fromisoformat(start_date_raw)
|
||||
elif isinstance(start_date_raw, datetime.date):
|
||||
start_date = start_date_raw
|
||||
elif start_date_raw is None:
|
||||
start_date = None
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Invalid value for recommended_start_date in {path}: {start_date_raw}"
|
||||
)
|
||||
|
||||
return cls(
|
||||
data["name"],
|
||||
f"Terrain_{sanitized_theater}",
|
||||
data.get("authors", "???"),
|
||||
data.get("description", ""),
|
||||
(version.major, version.minor),
|
||||
data.get("recommended_player_faction", "USA 2005"),
|
||||
data.get("recommended_enemy_faction", "Russia 1990"),
|
||||
start_date,
|
||||
data.get("performance", 0),
|
||||
data,
|
||||
path,
|
||||
)
|
||||
|
||||
def load_theater(self) -> ConflictTheater:
|
||||
theaters = {
|
||||
"Caucasus": CaucasusTheater,
|
||||
"Nevada": NevadaTheater,
|
||||
"Persian Gulf": PersianGulfTheater,
|
||||
"Normandy": NormandyTheater,
|
||||
"The Channel": TheChannelTheater,
|
||||
"Syria": SyriaTheater,
|
||||
"MarianaIslands": MarianaIslandsTheater,
|
||||
}
|
||||
theater = theaters[self.data["theater"]]
|
||||
t = theater()
|
||||
|
||||
try:
|
||||
miz = self.data["miz"]
|
||||
except KeyError as ex:
|
||||
raise RuntimeError(
|
||||
"Old format (non-miz) campaigns are no longer supported."
|
||||
) from ex
|
||||
|
||||
with logged_duration("Importing miz data"):
|
||||
MizCampaignLoader(self.path.parent / miz, t).populate_theater()
|
||||
return t
|
||||
|
||||
def load_air_wing_config(self, theater: ConflictTheater) -> CampaignAirWingConfig:
|
||||
try:
|
||||
squadron_data = self.data["squadrons"]
|
||||
except KeyError:
|
||||
logging.warning(f"Campaign {self.name} does not define any squadrons")
|
||||
return CampaignAirWingConfig({})
|
||||
return CampaignAirWingConfig.from_campaign_data(squadron_data, theater)
|
||||
|
||||
@property
|
||||
def is_out_of_date(self) -> bool:
|
||||
"""Returns True if this campaign is not up to date with the latest format.
|
||||
|
||||
This is more permissive than is_from_future, which is sensitive to minor version
|
||||
bumps (the old game definitely doesn't support the minor features added in the
|
||||
new version, and the campaign may require them. However, the minor version only
|
||||
indicates *optional* new features, so we do not need to mark out of date
|
||||
campaigns as incompatible if they are within the same major version.
|
||||
"""
|
||||
return self.version[0] < CAMPAIGN_FORMAT_VERSION[0]
|
||||
|
||||
@property
|
||||
def is_from_future(self) -> bool:
|
||||
"""Returns True if this campaign is newer than the supported format."""
|
||||
return self.version > SUPPORTED_CAMPAIGN_VERSION
|
||||
|
||||
@property
|
||||
def is_compatible(self) -> bool:
|
||||
"""Returns True is this campaign was built for this version of the game."""
|
||||
if self.version == (0, 0):
|
||||
return False
|
||||
if self.is_out_of_date:
|
||||
return False
|
||||
if self.is_from_future:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def iter_campaigns_in_dir(path: Path) -> Iterator[Path]:
|
||||
yield from path.glob("*.yaml")
|
||||
yield from path.glob("*.json")
|
||||
|
||||
@classmethod
|
||||
def iter_campaign_defs(cls) -> Iterator[Path]:
|
||||
yield from cls.iter_campaigns_in_dir(
|
||||
Path(persistency.base_path()) / "Liberation/Campaigns"
|
||||
)
|
||||
yield from cls.iter_campaigns_in_dir(Path("resources/campaigns"))
|
||||
|
||||
@classmethod
|
||||
def load_each(cls) -> Iterator[Campaign]:
|
||||
for path in cls.iter_campaign_defs():
|
||||
try:
|
||||
logging.debug(f"Loading campaign from {path}...")
|
||||
campaign = Campaign.from_file(path)
|
||||
yield campaign
|
||||
except RuntimeError:
|
||||
logging.exception(f"Unable to load campaign from {path}")
|
||||
@@ -1,68 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
|
||||
from gen.flights.flight import FlightType
|
||||
from game.theater.controlpoint import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater import ConflictTheater
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SquadronConfig:
|
||||
primary: FlightType
|
||||
secondary: list[FlightType]
|
||||
aircraft: list[str]
|
||||
|
||||
@property
|
||||
def auto_assignable(self) -> set[FlightType]:
|
||||
return set(self.secondary) | {self.primary}
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> SquadronConfig:
|
||||
secondary_raw = data.get("secondary")
|
||||
if secondary_raw is None:
|
||||
secondary = []
|
||||
elif isinstance(secondary_raw, str):
|
||||
secondary = cls.expand_secondary_alias(secondary_raw)
|
||||
else:
|
||||
secondary = [FlightType(s) for s in secondary_raw]
|
||||
|
||||
return SquadronConfig(
|
||||
FlightType(data["primary"]), secondary, data.get("aircraft", [])
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def expand_secondary_alias(alias: str) -> list[FlightType]:
|
||||
if alias == "any":
|
||||
return list(FlightType)
|
||||
elif alias == "air-to-air":
|
||||
return [t for t in FlightType if t.is_air_to_air]
|
||||
elif alias == "air-to-ground":
|
||||
return [t for t in FlightType if t.is_air_to_ground]
|
||||
raise KeyError(f"Unknown secondary mission type: {alias}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CampaignAirWingConfig:
|
||||
by_location: dict[ControlPoint, list[SquadronConfig]]
|
||||
|
||||
@classmethod
|
||||
def from_campaign_data(
|
||||
cls, data: dict[Union[str, int], Any], theater: ConflictTheater
|
||||
) -> CampaignAirWingConfig:
|
||||
by_location: dict[ControlPoint, list[SquadronConfig]] = defaultdict(list)
|
||||
for base_id, squadron_configs in data.items():
|
||||
if isinstance(base_id, int):
|
||||
base = theater.find_control_point_by_id(base_id)
|
||||
else:
|
||||
base = theater.control_point_named(base_id)
|
||||
|
||||
for squadron_data in squadron_configs:
|
||||
by_location[base].append(SquadronConfig.from_data(squadron_data))
|
||||
|
||||
return CampaignAirWingConfig(by_location)
|
||||
@@ -1,144 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.squadrons import Squadron
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from gen.flights.flight import FlightType
|
||||
from .campaignairwingconfig import CampaignAirWingConfig, SquadronConfig
|
||||
from ..dcs.aircrafttype import AircraftType
|
||||
from ..theater import ControlPoint
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class DefaultSquadronAssigner:
|
||||
def __init__(
|
||||
self, config: CampaignAirWingConfig, game: Game, coalition: Coalition
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.game = game
|
||||
self.coalition = coalition
|
||||
self.air_wing = coalition.air_wing
|
||||
|
||||
def assign(self) -> None:
|
||||
for control_point in self.game.theater.control_points_for(
|
||||
self.coalition.player
|
||||
):
|
||||
for squadron_config in self.config.by_location[control_point]:
|
||||
squadron_def = self.find_squadron_for(squadron_config, control_point)
|
||||
if squadron_def is None:
|
||||
logging.info(
|
||||
f"{self.coalition.faction.name} has no aircraft compatible "
|
||||
f"with {squadron_config.primary} at {control_point}"
|
||||
)
|
||||
continue
|
||||
|
||||
squadron = Squadron.create_from(
|
||||
squadron_def, control_point, self.coalition, self.game
|
||||
)
|
||||
squadron.set_auto_assignable_mission_types(
|
||||
squadron_config.auto_assignable
|
||||
)
|
||||
self.air_wing.add_squadron(squadron)
|
||||
|
||||
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
|
||||
)
|
||||
if squadron_def is not None:
|
||||
return squadron_def
|
||||
|
||||
# If we didn't find any of the preferred types we should use any squadron
|
||||
# compatible with the primary task.
|
||||
squadron_def = self.find_squadron_for_task(config.primary, control_point)
|
||||
if squadron_def is not None:
|
||||
return squadron_def
|
||||
|
||||
# If we can't find any squadron matching the requirement, we should
|
||||
# create one.
|
||||
return self.air_wing.squadron_def_generator.generate_for_task(
|
||||
config.primary, control_point
|
||||
)
|
||||
|
||||
def find_preferred_squadron(
|
||||
self, preferred_aircraft: str, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
# Attempt to find a squadron with the name in the request.
|
||||
squadron_def = self.find_squadron_by_name(
|
||||
preferred_aircraft, task, control_point
|
||||
)
|
||||
if squadron_def is not None:
|
||||
return squadron_def
|
||||
|
||||
# If the name didn't match a squadron available to this coalition, try to find
|
||||
# an aircraft with the matching name that meets the requirements.
|
||||
try:
|
||||
aircraft = AircraftType.named(preferred_aircraft)
|
||||
except KeyError:
|
||||
# No aircraft with this name.
|
||||
return None
|
||||
|
||||
if aircraft not in self.coalition.faction.aircrafts:
|
||||
return None
|
||||
|
||||
squadron_def = self.find_squadron_for_airframe(aircraft, task, control_point)
|
||||
if squadron_def is not None:
|
||||
return squadron_def
|
||||
|
||||
# No premade squadron available for this aircraft that meets the requirements,
|
||||
# so generate one if possible.
|
||||
return self.air_wing.squadron_def_generator.generate_for_aircraft(aircraft)
|
||||
|
||||
@staticmethod
|
||||
def squadron_compatible_with(
|
||||
squadron: SquadronDef,
|
||||
task: FlightType,
|
||||
control_point: ControlPoint,
|
||||
ignore_base_preference: bool = False,
|
||||
) -> bool:
|
||||
if ignore_base_preference:
|
||||
return control_point.can_operate(squadron.aircraft)
|
||||
return squadron.operates_from(control_point) and task in squadron.mission_types
|
||||
|
||||
def find_squadron_for_airframe(
|
||||
self, aircraft: AircraftType, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
for squadron in self.air_wing.squadron_defs[aircraft]:
|
||||
if not squadron.claimed and self.squadron_compatible_with(
|
||||
squadron, task, control_point
|
||||
):
|
||||
return squadron
|
||||
return None
|
||||
|
||||
def find_squadron_by_name(
|
||||
self, name: str, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
for squadrons in self.air_wing.squadron_defs.values():
|
||||
for squadron in squadrons:
|
||||
if (
|
||||
not squadron.claimed
|
||||
and squadron.name == name
|
||||
and self.squadron_compatible_with(
|
||||
squadron, task, control_point, ignore_base_preference=True
|
||||
)
|
||||
):
|
||||
return squadron
|
||||
return None
|
||||
|
||||
def find_squadron_for_task(
|
||||
self, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
for squadrons in self.air_wing.squadron_defs.values():
|
||||
for squadron in squadrons:
|
||||
if not squadron.claimed and self.squadron_compatible_with(
|
||||
squadron, task, control_point
|
||||
):
|
||||
return squadron
|
||||
return None
|
||||
@@ -1,473 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Iterator, List, Dict, Tuple, TYPE_CHECKING
|
||||
|
||||
from dcs import Mission
|
||||
from dcs.countries import CombinedJointTaskForcesBlue, CombinedJointTaskForcesRed
|
||||
from dcs.country import Country
|
||||
from dcs.planes import F_15C
|
||||
from dcs.ships import Stennis, LHA_Tarawa, HandyWind, USS_Arleigh_Burke_IIa
|
||||
from dcs.statics import Fortification, Warehouse
|
||||
from dcs.terrain import Airport
|
||||
from dcs.unitgroup import PlaneGroup, ShipGroup, VehicleGroup, StaticGroup
|
||||
from dcs.vehicles import Armor, Unarmed, MissilesSS, AirDefence
|
||||
|
||||
from game.point_with_heading import PointWithHeading
|
||||
from game.positioned import Positioned
|
||||
from game.profiling import logged_duration
|
||||
from game.scenery_group import SceneryGroup
|
||||
from game.utils import Distance, meters, Heading
|
||||
from game.theater.controlpoint import (
|
||||
Airfield,
|
||||
Carrier,
|
||||
ControlPoint,
|
||||
Fob,
|
||||
Lha,
|
||||
OffMapSpawn,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.conflicttheater import ConflictTheater
|
||||
|
||||
|
||||
class MizCampaignLoader:
|
||||
BLUE_COUNTRY = CombinedJointTaskForcesBlue()
|
||||
RED_COUNTRY = CombinedJointTaskForcesRed()
|
||||
|
||||
OFF_MAP_UNIT_TYPE = F_15C.id
|
||||
|
||||
CV_UNIT_TYPE = Stennis.id
|
||||
LHA_UNIT_TYPE = LHA_Tarawa.id
|
||||
FRONT_LINE_UNIT_TYPE = Armor.M_113.id
|
||||
SHIPPING_LANE_UNIT_TYPE = HandyWind.id
|
||||
|
||||
FOB_UNIT_TYPE = Unarmed.SKP_11.id
|
||||
FARP_HELIPADS_TYPE = ["Invisible FARP", "SINGLE_HELIPAD"]
|
||||
|
||||
OFFSHORE_STRIKE_TARGET_UNIT_TYPE = Fortification.Oil_platform.id
|
||||
SHIP_UNIT_TYPE = USS_Arleigh_Burke_IIa.id
|
||||
MISSILE_SITE_UNIT_TYPE = MissilesSS.Scud_B.id
|
||||
COASTAL_DEFENSE_UNIT_TYPE = MissilesSS.Hy_launcher.id
|
||||
|
||||
# Multiple options for air defenses so campaign designers can more accurately see
|
||||
# the coverage of their IADS for the expected type.
|
||||
LONG_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.Patriot_ln.id,
|
||||
AirDefence.S_300PS_5P85C_ln.id,
|
||||
AirDefence.S_300PS_5P85D_ln.id,
|
||||
}
|
||||
|
||||
MEDIUM_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.Hawk_ln.id,
|
||||
AirDefence.S_75M_Volhov.id,
|
||||
AirDefence._5p73_s_125_ln.id,
|
||||
}
|
||||
|
||||
SHORT_RANGE_SAM_UNIT_TYPES = {
|
||||
AirDefence.M1097_Avenger.id,
|
||||
AirDefence.Rapier_fsa_launcher.id,
|
||||
AirDefence._2S6_Tunguska.id,
|
||||
AirDefence.Strela_1_9P31.id,
|
||||
}
|
||||
|
||||
AAA_UNIT_TYPES = {
|
||||
AirDefence.Flak18.id,
|
||||
AirDefence.Vulcan.id,
|
||||
AirDefence.ZSU_23_4_Shilka.id,
|
||||
}
|
||||
|
||||
EWR_UNIT_TYPE = AirDefence._1L13_EWR.id
|
||||
|
||||
ARMOR_GROUP_UNIT_TYPE = Armor.M_1_Abrams.id
|
||||
|
||||
FACTORY_UNIT_TYPE = Fortification.Workshop_A.id
|
||||
|
||||
AMMUNITION_DEPOT_UNIT_TYPE = Warehouse._Ammunition_depot.id
|
||||
|
||||
STRIKE_TARGET_UNIT_TYPE = Fortification.Tech_combine.id
|
||||
|
||||
def __init__(self, miz: Path, theater: ConflictTheater) -> None:
|
||||
self.theater = theater
|
||||
self.mission = Mission()
|
||||
with logged_duration("Loading miz"):
|
||||
self.mission.load_file(str(miz))
|
||||
self.control_point_id = itertools.count(1000)
|
||||
|
||||
# If there are no red carriers there usually aren't red units. Make sure
|
||||
# both countries are initialized so we don't have to deal with None.
|
||||
if self.mission.country(self.BLUE_COUNTRY.name) is None:
|
||||
self.mission.coalition["blue"].add_country(self.BLUE_COUNTRY)
|
||||
if self.mission.country(self.RED_COUNTRY.name) is None:
|
||||
self.mission.coalition["red"].add_country(self.RED_COUNTRY)
|
||||
|
||||
@staticmethod
|
||||
def control_point_from_airport(airport: Airport) -> ControlPoint:
|
||||
cp = Airfield(airport, starts_blue=airport.is_blue())
|
||||
|
||||
# Use the unlimited aircraft option to determine if an airfield should
|
||||
# be owned by the player when the campaign is "inverted".
|
||||
cp.captured_invert = airport.unlimited_aircrafts
|
||||
|
||||
return cp
|
||||
|
||||
def country(self, blue: bool) -> Country:
|
||||
country = self.mission.country(
|
||||
self.BLUE_COUNTRY.name if blue else self.RED_COUNTRY.name
|
||||
)
|
||||
# Should be guaranteed because we initialized them.
|
||||
assert country
|
||||
return country
|
||||
|
||||
@property
|
||||
def blue(self) -> Country:
|
||||
return self.country(blue=True)
|
||||
|
||||
@property
|
||||
def red(self) -> Country:
|
||||
return self.country(blue=False)
|
||||
|
||||
def off_map_spawns(self, blue: bool) -> Iterator[PlaneGroup]:
|
||||
for group in self.country(blue).plane_group:
|
||||
if group.units[0].type == self.OFF_MAP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def carriers(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.CV_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def lhas(self, blue: bool) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue).ship_group:
|
||||
if group.units[0].type == self.LHA_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def fobs(self, blue: bool) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue).vehicle_group:
|
||||
if group.units[0].type == self.FOB_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ships(self) -> Iterator[ShipGroup]:
|
||||
for group in self.red.ship_group:
|
||||
if group.units[0].type == self.SHIP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def offshore_strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in self.red.static_group:
|
||||
if group.units[0].type == self.OFFSHORE_STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def missile_sites(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type == self.MISSILE_SITE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def coastal_defenses(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type == self.COASTAL_DEFENSE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def long_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.LONG_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def medium_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.MEDIUM_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def short_range_sams(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.SHORT_RANGE_SAM_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def aaa(self) -> Iterator[VehicleGroup]:
|
||||
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
||||
if group.units[0].type in self.AAA_UNIT_TYPES:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ewrs(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.red.vehicle_group:
|
||||
if group.units[0].type in self.EWR_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def armor_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in itertools.chain(self.blue.vehicle_group, self.red.vehicle_group):
|
||||
if group.units[0].type in self.ARMOR_GROUP_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def helipads(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.FARP_HELIPADS_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def factories(self) -> Iterator[StaticGroup]:
|
||||
for group in self.blue.static_group:
|
||||
if group.units[0].type in self.FACTORY_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def ammunition_depots(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.AMMUNITION_DEPOT_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def strike_targets(self) -> Iterator[StaticGroup]:
|
||||
for group in itertools.chain(self.blue.static_group, self.red.static_group):
|
||||
if group.units[0].type in self.STRIKE_TARGET_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def scenery(self) -> List[SceneryGroup]:
|
||||
return SceneryGroup.from_trigger_zones(self.mission.triggers._zones)
|
||||
|
||||
@cached_property
|
||||
def control_points(self) -> Dict[int, ControlPoint]:
|
||||
control_points = {}
|
||||
for airport in self.mission.terrain.airport_list():
|
||||
if airport.is_blue() or airport.is_red():
|
||||
control_point = self.control_point_from_airport(airport)
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
for blue in (False, True):
|
||||
for group in self.off_map_spawns(blue):
|
||||
control_point = OffMapSpawn(
|
||||
next(self.control_point_id),
|
||||
str(group.name),
|
||||
group.position,
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured_invert = group.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for ship in self.carriers(blue):
|
||||
control_point = Carrier(
|
||||
ship.name,
|
||||
ship.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for ship in self.lhas(blue):
|
||||
control_point = Lha(
|
||||
ship.name,
|
||||
ship.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured_invert = ship.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
for fob in self.fobs(blue):
|
||||
control_point = Fob(
|
||||
str(fob.name),
|
||||
fob.position,
|
||||
next(self.control_point_id),
|
||||
starts_blue=blue,
|
||||
)
|
||||
control_point.captured_invert = fob.late_activation
|
||||
control_points[control_point.id] = control_point
|
||||
|
||||
return control_points
|
||||
|
||||
@property
|
||||
def front_line_path_groups(self) -> Iterator[VehicleGroup]:
|
||||
for group in self.country(blue=True).vehicle_group:
|
||||
if group.units[0].type == self.FRONT_LINE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
@property
|
||||
def shipping_lane_groups(self) -> Iterator[ShipGroup]:
|
||||
for group in self.country(blue=True).ship_group:
|
||||
if group.units[0].type == self.SHIPPING_LANE_UNIT_TYPE:
|
||||
yield group
|
||||
|
||||
def add_supply_routes(self) -> None:
|
||||
for group in self.front_line_path_groups:
|
||||
# The unit will have its first waypoint at the source CP and the final
|
||||
# waypoint at the destination CP. Each waypoint defines the path of the
|
||||
# cargo ship.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the first waypoint of {group.name}"
|
||||
)
|
||||
destination = self.theater.closest_control_point(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the final waypoint of {group.name}"
|
||||
)
|
||||
|
||||
self.control_points[origin.id].create_convoy_route(destination, waypoints)
|
||||
self.control_points[destination.id].create_convoy_route(
|
||||
origin, list(reversed(waypoints))
|
||||
)
|
||||
|
||||
def add_shipping_lanes(self) -> None:
|
||||
for group in self.shipping_lane_groups:
|
||||
# The unit will have its first waypoint at the source CP and the final
|
||||
# waypoint at the destination CP. Each waypoint defines the path of the
|
||||
# cargo ship.
|
||||
waypoints = [p.position for p in group.points]
|
||||
origin = self.theater.closest_control_point(waypoints[0])
|
||||
if origin is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the first waypoint of {group.name}"
|
||||
)
|
||||
destination = self.theater.closest_control_point(waypoints[-1])
|
||||
if destination is None:
|
||||
raise RuntimeError(
|
||||
f"No control point near the final waypoint of {group.name}"
|
||||
)
|
||||
|
||||
self.control_points[origin.id].create_shipping_lane(destination, waypoints)
|
||||
self.control_points[destination.id].create_shipping_lane(
|
||||
origin, list(reversed(waypoints))
|
||||
)
|
||||
|
||||
def objective_info(
|
||||
self, near: Positioned, allow_naval: bool = False
|
||||
) -> Tuple[ControlPoint, Distance]:
|
||||
closest = self.theater.closest_control_point(near.position, allow_naval)
|
||||
distance = meters(closest.position.distance_to_point(near.position))
|
||||
return closest, distance
|
||||
|
||||
def add_preset_locations(self) -> None:
|
||||
for static in self.offshore_strike_targets:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.offshore_strike_locations.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for ship in self.ships:
|
||||
closest, distance = self.objective_info(ship, allow_naval=True)
|
||||
closest.preset_locations.ships.append(
|
||||
PointWithHeading.from_point(
|
||||
ship.position, Heading.from_degrees(ship.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.missile_sites:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.missile_sites.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.coastal_defenses:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.coastal_defenses.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.long_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.long_range_sams.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.medium_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.medium_range_sams.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.short_range_sams:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.short_range_sams.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.aaa:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.aaa.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.ewrs:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.ewrs.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for group in self.armor_groups:
|
||||
closest, distance = self.objective_info(group)
|
||||
closest.preset_locations.armor_groups.append(
|
||||
PointWithHeading.from_point(
|
||||
group.position, Heading.from_degrees(group.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for static in self.helipads:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.helipads.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for static in self.factories:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.factories.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for static in self.ammunition_depots:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.ammunition_depots.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for static in self.strike_targets:
|
||||
closest, distance = self.objective_info(static)
|
||||
closest.preset_locations.strike_locations.append(
|
||||
PointWithHeading.from_point(
|
||||
static.position, Heading.from_degrees(static.units[0].heading)
|
||||
)
|
||||
)
|
||||
|
||||
for scenery_group in self.scenery:
|
||||
closest, distance = self.objective_info(scenery_group)
|
||||
closest.preset_locations.scenery.append(scenery_group)
|
||||
|
||||
def populate_theater(self) -> None:
|
||||
for control_point in self.control_points.values():
|
||||
self.theater.add_controlpoint(control_point)
|
||||
self.add_preset_locations()
|
||||
self.add_supply_routes()
|
||||
self.add_shipping_lanes()
|
||||
@@ -1,325 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import random
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.operatingbases import OperatingBases
|
||||
from game.squadrons.squadrondef import SquadronDef
|
||||
from game.theater import ControlPoint
|
||||
from gen.flights.ai_flight_planner_db import aircraft_for_task, tasks_for_aircraft
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.factions.faction import Faction
|
||||
|
||||
|
||||
class SquadronDefGenerator:
|
||||
def __init__(self, faction: Faction) -> None:
|
||||
self.faction = faction
|
||||
self.count = itertools.count(1)
|
||||
self.used_nicknames: set[str] = set()
|
||||
|
||||
def generate_for_task(
|
||||
self, task: FlightType, control_point: ControlPoint
|
||||
) -> Optional[SquadronDef]:
|
||||
aircraft_choice: Optional[AircraftType] = None
|
||||
for aircraft in aircraft_for_task(task):
|
||||
if aircraft not in self.faction.aircrafts:
|
||||
continue
|
||||
if not control_point.can_operate(aircraft):
|
||||
continue
|
||||
aircraft_choice = aircraft
|
||||
# 50/50 chance to keep looking for an aircraft that isn't as far up the
|
||||
# priority list to maintain some unit variety.
|
||||
if random.choice([True, False]):
|
||||
break
|
||||
|
||||
if aircraft_choice is None:
|
||||
return None
|
||||
return self.generate_for_aircraft(aircraft_choice)
|
||||
|
||||
def generate_for_aircraft(self, aircraft: AircraftType) -> SquadronDef:
|
||||
return SquadronDef(
|
||||
name=f"Squadron {next(self.count):03}",
|
||||
nickname=self.random_nickname(),
|
||||
country=self.faction.country,
|
||||
role="Flying Squadron",
|
||||
aircraft=aircraft,
|
||||
livery=None,
|
||||
mission_types=tuple(tasks_for_aircraft(aircraft)),
|
||||
operating_bases=OperatingBases.default_for_aircraft(aircraft),
|
||||
pilot_pool=[],
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _make_random_nickname() -> str:
|
||||
from gen.naming import ANIMALS
|
||||
|
||||
animal = random.choice(ANIMALS)
|
||||
adjective = random.choice(
|
||||
(
|
||||
None,
|
||||
"Aggressive",
|
||||
"Alpha",
|
||||
"Ancient",
|
||||
"Angelic",
|
||||
"Angry",
|
||||
"Apoplectic",
|
||||
"Aquamarine",
|
||||
"Astral",
|
||||
"Avenging",
|
||||
"Azure",
|
||||
"Badass",
|
||||
"Barbaric",
|
||||
"Battle",
|
||||
"Battling",
|
||||
"Bellicose",
|
||||
"Belligerent",
|
||||
"Big",
|
||||
"Bionic",
|
||||
"Black",
|
||||
"Bladed",
|
||||
"Blazoned",
|
||||
"Blood",
|
||||
"Bloody",
|
||||
"Blue",
|
||||
"Bold",
|
||||
"Boxing",
|
||||
"Brash",
|
||||
"Brass",
|
||||
"Brave",
|
||||
"Brazen",
|
||||
"Bronze",
|
||||
"Brown",
|
||||
"Brutal",
|
||||
"Burning",
|
||||
"Buzzing",
|
||||
"Celestial",
|
||||
"Clever",
|
||||
"Cloud",
|
||||
"Cobalt",
|
||||
"Copper",
|
||||
"Coral",
|
||||
"Crazy",
|
||||
"Crimson",
|
||||
"Crouching",
|
||||
"Cursed",
|
||||
"Cyan",
|
||||
"Danger",
|
||||
"Dangerous",
|
||||
"Dapper",
|
||||
"Daring",
|
||||
"Dark",
|
||||
"Dawn",
|
||||
"Day",
|
||||
"Deadly",
|
||||
"Death",
|
||||
"Defiant",
|
||||
"Demon",
|
||||
"Desert",
|
||||
"Devil",
|
||||
"Devil's",
|
||||
"Diabolical",
|
||||
"Diamond",
|
||||
"Dire",
|
||||
"Dirty",
|
||||
"Doom",
|
||||
"Doomed",
|
||||
"Double",
|
||||
"Drunken",
|
||||
"Dusk",
|
||||
"Dusty",
|
||||
"Eager",
|
||||
"Ebony",
|
||||
"Electric",
|
||||
"Emerald",
|
||||
"Eternal",
|
||||
"Evil",
|
||||
"Faithful",
|
||||
"Famous",
|
||||
"Fanged",
|
||||
"Fearless",
|
||||
"Feisty",
|
||||
"Ferocious",
|
||||
"Fierce",
|
||||
"Fiery",
|
||||
"Fighting",
|
||||
"Fire",
|
||||
"First",
|
||||
"Flame",
|
||||
"Flaming",
|
||||
"Flying",
|
||||
"Forest",
|
||||
"Frenzied",
|
||||
"Frosty",
|
||||
"Frozen",
|
||||
"Furious",
|
||||
"Gallant",
|
||||
"Ghost",
|
||||
"Giant",
|
||||
"Gigantic",
|
||||
"Glaring",
|
||||
"Global",
|
||||
"Gold",
|
||||
"Golden",
|
||||
"Green",
|
||||
"Grey",
|
||||
"Grim",
|
||||
"Grizzly",
|
||||
"Growling",
|
||||
"Grumpy",
|
||||
"Hammer",
|
||||
"Hard",
|
||||
"Hardy",
|
||||
"Heavy",
|
||||
"Hell",
|
||||
"Hell's",
|
||||
"Hidden",
|
||||
"Homicidal",
|
||||
"Hostile",
|
||||
"Howling",
|
||||
"Hyper",
|
||||
"Ice",
|
||||
"Icy",
|
||||
"Immortal",
|
||||
"Indignant",
|
||||
"Infamous",
|
||||
"Invincible",
|
||||
"Iron",
|
||||
"Jolly",
|
||||
"Laser",
|
||||
"Lava",
|
||||
"Lavender",
|
||||
"Lethal",
|
||||
"Light",
|
||||
"Lightning",
|
||||
"Livid",
|
||||
"Lucky",
|
||||
"Mad",
|
||||
"Magenta",
|
||||
"Magma",
|
||||
"Maroon",
|
||||
"Menacing",
|
||||
"Merciless",
|
||||
"Metal",
|
||||
"Midnight",
|
||||
"Mighty",
|
||||
"Mithril",
|
||||
"Mocking",
|
||||
"Moon",
|
||||
"Mountain",
|
||||
"Muddy",
|
||||
"Nasty",
|
||||
"Naughty",
|
||||
"Night",
|
||||
"Nova",
|
||||
"Nutty",
|
||||
"Obsidian",
|
||||
"Ocean",
|
||||
"Oddball",
|
||||
"Old",
|
||||
"Omega",
|
||||
"Onyx",
|
||||
"Orange",
|
||||
"Perky",
|
||||
"Pink",
|
||||
"Power",
|
||||
"Prickly",
|
||||
"Proud",
|
||||
"Puckered",
|
||||
"Pugnacious",
|
||||
"Puking",
|
||||
"Purple",
|
||||
"Ragged",
|
||||
"Raging",
|
||||
"Rainbow",
|
||||
"Rampant",
|
||||
"Razor",
|
||||
"Ready",
|
||||
"Reaper",
|
||||
"Reckless",
|
||||
"Red",
|
||||
"Roaring",
|
||||
"Rocky",
|
||||
"Rolling",
|
||||
"Royal",
|
||||
"Rusty",
|
||||
"Sable",
|
||||
"Salty",
|
||||
"Sand",
|
||||
"Sarcastic",
|
||||
"Saucy",
|
||||
"Scarlet",
|
||||
"Scarred",
|
||||
"Scary",
|
||||
"Screaming",
|
||||
"Scythed",
|
||||
"Shadow",
|
||||
"Shiny",
|
||||
"Shocking",
|
||||
"Silver",
|
||||
"Sky",
|
||||
"Smoke",
|
||||
"Smokin'",
|
||||
"Snapping",
|
||||
"Snappy",
|
||||
"Snarling",
|
||||
"Snow",
|
||||
"Soaring",
|
||||
"Space",
|
||||
"Spiky",
|
||||
"Spiny",
|
||||
"Star",
|
||||
"Steady",
|
||||
"Steel",
|
||||
"Stone",
|
||||
"Storm",
|
||||
"Striking",
|
||||
"Strong",
|
||||
"Stubborn",
|
||||
"Sun",
|
||||
"Super",
|
||||
"Terrible",
|
||||
"Thorny",
|
||||
"Thunder",
|
||||
"Top",
|
||||
"Tough",
|
||||
"Toxic",
|
||||
"Tricky",
|
||||
"Turquoise",
|
||||
"Typhoon",
|
||||
"Ultimate",
|
||||
"Ultra",
|
||||
"Ultramarine",
|
||||
"Vengeful",
|
||||
"Venom",
|
||||
"Vermillion",
|
||||
"Vicious",
|
||||
"Victorious",
|
||||
"Vigilant",
|
||||
"Violent",
|
||||
"Violet",
|
||||
"War",
|
||||
"Water",
|
||||
"Whistling",
|
||||
"White",
|
||||
"Wicked",
|
||||
"Wild",
|
||||
"Wizard",
|
||||
"Wrathful",
|
||||
"Yellow",
|
||||
"Young",
|
||||
)
|
||||
)
|
||||
if adjective is None:
|
||||
return animal.title()
|
||||
return f"{adjective} {animal}".title()
|
||||
|
||||
def random_nickname(self) -> str:
|
||||
while True:
|
||||
nickname = self._make_random_nickname()
|
||||
if nickname not in self.used_nicknames:
|
||||
self.used_nicknames.add(nickname)
|
||||
return nickname
|
||||
@@ -1,235 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Optional
|
||||
|
||||
from dcs import Point
|
||||
from faker import Faker
|
||||
|
||||
from game.campaignloader import CampaignAirWingConfig
|
||||
from game.campaignloader.defaultsquadronassigner import DefaultSquadronAssigner
|
||||
from game.commander import TheaterCommander
|
||||
from game.commander.missionscheduler import MissionScheduler
|
||||
from game.income import Income
|
||||
from game.navmesh import NavMesh
|
||||
from game.orderedset import OrderedSet
|
||||
from game.profiling import logged_duration, MultiEventTracer
|
||||
from game.squadrons import AirWing
|
||||
from game.threatzones import ThreatZones
|
||||
from game.transfers import PendingTransfers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.factions.faction import Faction
|
||||
from game.procurement import AircraftProcurementRequest, ProcurementAi
|
||||
from game.theater.bullseye import Bullseye
|
||||
from game.theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
|
||||
from gen.ato import AirTaskingOrder
|
||||
|
||||
|
||||
class Coalition:
|
||||
def __init__(
|
||||
self, game: Game, faction: Faction, budget: float, player: bool
|
||||
) -> None:
|
||||
self.game = game
|
||||
self.player = player
|
||||
self.faction = faction
|
||||
self.budget = budget
|
||||
self.ato = AirTaskingOrder()
|
||||
self.transit_network = TransitNetwork()
|
||||
self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet()
|
||||
self.bullseye = Bullseye(Point(0, 0))
|
||||
self.faker = Faker(self.faction.locales)
|
||||
self.air_wing = AirWing(player, game, self.faction)
|
||||
self.transfers = PendingTransfers(game, player)
|
||||
|
||||
# Late initialized because the two coalitions in the game are mutually
|
||||
# dependent, so must be both constructed before this property can be set.
|
||||
self._opponent: Optional[Coalition] = None
|
||||
|
||||
# Volatile properties that are not persisted to the save file since they can be
|
||||
# recomputed on load. Keeping this data out of the save file makes save compat
|
||||
# breaks less frequent. Each of these properties has a non-underscore-prefixed
|
||||
# @property that should be used for non-Optional access.
|
||||
#
|
||||
# All of these are late-initialized (whether via on_load or called later), but
|
||||
# will be non-None after the game has finished loading.
|
||||
self._threat_zone: Optional[ThreatZones] = None
|
||||
self._navmesh: Optional[NavMesh] = None
|
||||
self.on_load()
|
||||
|
||||
@property
|
||||
def doctrine(self) -> Doctrine:
|
||||
return self.faction.doctrine
|
||||
|
||||
@property
|
||||
def coalition_id(self) -> int:
|
||||
if self.player:
|
||||
return 2
|
||||
return 1
|
||||
|
||||
@property
|
||||
def country_name(self) -> str:
|
||||
return self.faction.country
|
||||
|
||||
@property
|
||||
def opponent(self) -> Coalition:
|
||||
assert self._opponent is not None
|
||||
return self._opponent
|
||||
|
||||
@property
|
||||
def threat_zone(self) -> ThreatZones:
|
||||
assert self._threat_zone is not None
|
||||
return self._threat_zone
|
||||
|
||||
@property
|
||||
def nav_mesh(self) -> NavMesh:
|
||||
assert self._navmesh is not None
|
||||
return self._navmesh
|
||||
|
||||
def __getstate__(self) -> dict[str, Any]:
|
||||
state = self.__dict__.copy()
|
||||
# Avoid persisting any volatile types that can be deterministically
|
||||
# recomputed on load for the sake of save compatibility.
|
||||
del state["_threat_zone"]
|
||||
del state["_navmesh"]
|
||||
del state["faker"]
|
||||
return state
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
self.__dict__.update(state)
|
||||
# Regenerate any state that was not persisted.
|
||||
self.on_load()
|
||||
|
||||
def on_load(self) -> None:
|
||||
self.faker = Faker(self.faction.locales)
|
||||
|
||||
def set_opponent(self, opponent: Coalition) -> None:
|
||||
if self._opponent is not None:
|
||||
raise RuntimeError("Double-initialization of Coalition.opponent")
|
||||
self._opponent = opponent
|
||||
|
||||
def configure_default_air_wing(
|
||||
self, air_wing_config: CampaignAirWingConfig
|
||||
) -> None:
|
||||
DefaultSquadronAssigner(air_wing_config, self.game, self).assign()
|
||||
|
||||
def adjust_budget(self, amount: float) -> None:
|
||||
self.budget += amount
|
||||
|
||||
def compute_threat_zones(self) -> None:
|
||||
self._threat_zone = ThreatZones.for_faction(self.game, self.player)
|
||||
|
||||
def compute_nav_meshes(self) -> None:
|
||||
self._navmesh = NavMesh.from_threat_zones(
|
||||
self.opponent.threat_zone, self.game.theater
|
||||
)
|
||||
|
||||
def update_transit_network(self) -> None:
|
||||
self.transit_network = TransitNetworkBuilder(
|
||||
self.game.theater, self.player
|
||||
).build()
|
||||
|
||||
def set_bullseye(self, bullseye: Bullseye) -> None:
|
||||
self.bullseye = bullseye
|
||||
|
||||
def end_turn(self) -> None:
|
||||
"""Processes coalition-specific turn finalization.
|
||||
|
||||
For more information on turn finalization in general, see the documentation for
|
||||
`Game.finish_turn`.
|
||||
"""
|
||||
self.air_wing.end_turn()
|
||||
self.budget += Income(self.game, self.player).total
|
||||
|
||||
# Need to recompute before transfers and deliveries to account for captures.
|
||||
# This happens in in initialize_turn as well, because cheating doesn't advance a
|
||||
# turn but can capture bases so we need to recompute there as well.
|
||||
self.update_transit_network()
|
||||
|
||||
# Must happen *before* unit deliveries are handled, or else new units will spawn
|
||||
# one hop ahead. ControlPoint.process_turn handles unit deliveries. The
|
||||
# coalition-specific turn-end happens before the theater-wide turn-end, so this
|
||||
# is handled correctly.
|
||||
self.transfers.perform_transfers()
|
||||
|
||||
def preinit_turn_0(self) -> None:
|
||||
"""Runs final Coalition initialization.
|
||||
|
||||
Final initialization occurs before Game.initialize_turn runs for turn 0.
|
||||
"""
|
||||
self.air_wing.populate_for_turn_0()
|
||||
|
||||
def initialize_turn(self) -> None:
|
||||
"""Processes coalition-specific turn initialization.
|
||||
|
||||
For more information on turn initialization in general, see the documentation
|
||||
for `Game.initialize_turn`.
|
||||
"""
|
||||
# Needs to happen *before* planning transfers so we don't cancel them.
|
||||
self.ato.clear()
|
||||
self.air_wing.reset()
|
||||
self.refund_outstanding_orders()
|
||||
self.procurement_requests.clear()
|
||||
|
||||
with logged_duration("Transit network identification"):
|
||||
self.update_transit_network()
|
||||
with logged_duration("Procurement of airlift assets"):
|
||||
self.transfers.order_airlift_assets()
|
||||
with logged_duration("Transport planning"):
|
||||
self.transfers.plan_transports()
|
||||
|
||||
self.plan_missions()
|
||||
self.plan_procurement()
|
||||
|
||||
def refund_outstanding_orders(self) -> None:
|
||||
# TODO: Split orders between air and ground units.
|
||||
# This isn't quite right. If the player has ground purchases automated we should
|
||||
# be refunding the ground units, and if they have air automated but not ground
|
||||
# we should be refunding air units.
|
||||
if self.player and not self.game.settings.automate_aircraft_reinforcements:
|
||||
return
|
||||
|
||||
for cp in self.game.theater.control_points_for(self.player):
|
||||
cp.ground_unit_orders.refund_all(self)
|
||||
for squadron in self.air_wing.iter_squadrons():
|
||||
squadron.refund_orders()
|
||||
|
||||
def plan_missions(self) -> None:
|
||||
color = "Blue" if self.player else "Red"
|
||||
with MultiEventTracer() as tracer:
|
||||
with tracer.trace(f"{color} mission planning"):
|
||||
with tracer.trace(f"{color} mission identification"):
|
||||
TheaterCommander(self.game, self.player).plan_missions(tracer)
|
||||
with tracer.trace(f"{color} mission scheduling"):
|
||||
MissionScheduler(
|
||||
self, self.game.settings.desired_player_mission_duration
|
||||
).schedule_missions()
|
||||
|
||||
def plan_procurement(self) -> None:
|
||||
# The first turn needs to buy a *lot* of aircraft to fill CAPs, so it gets much
|
||||
# more of the budget that turn. Otherwise budget (after repairs) is split evenly
|
||||
# between air and ground. For the default starting budget of 2000 this gives 600
|
||||
# to ground forces and 1400 to aircraft. After that the budget will be spent
|
||||
# proportionally based on how much is already invested.
|
||||
|
||||
if self.player:
|
||||
manage_runways = self.game.settings.automate_runway_repair
|
||||
manage_front_line = self.game.settings.automate_front_line_reinforcements
|
||||
manage_aircraft = self.game.settings.automate_aircraft_reinforcements
|
||||
else:
|
||||
manage_runways = True
|
||||
manage_front_line = True
|
||||
manage_aircraft = True
|
||||
|
||||
self.budget = ProcurementAi(
|
||||
self.game,
|
||||
self.player,
|
||||
self.faction,
|
||||
manage_runways,
|
||||
manage_front_line,
|
||||
manage_aircraft,
|
||||
).spend_budget(self.budget)
|
||||
|
||||
def add_procurement_request(self, request: AircraftProcurementRequest) -> None:
|
||||
self.procurement_requests.add(request)
|
||||
@@ -1 +0,0 @@
|
||||
from .theatercommander import TheaterCommander
|
||||
@@ -1,52 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.theater import ControlPoint
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from game.utils import meters
|
||||
|
||||
|
||||
@dataclass
|
||||
class Garrisons:
|
||||
blocking_capture: list[VehicleGroupGroundObject]
|
||||
defending_front_line: list[VehicleGroupGroundObject]
|
||||
|
||||
@property
|
||||
def in_priority_order(self) -> Iterator[VehicleGroupGroundObject]:
|
||||
yield from self.blocking_capture
|
||||
yield from self.defending_front_line
|
||||
|
||||
def eliminate(self, garrison: VehicleGroupGroundObject) -> None:
|
||||
if garrison in self.blocking_capture:
|
||||
self.blocking_capture.remove(garrison)
|
||||
if garrison in self.defending_front_line:
|
||||
self.defending_front_line.remove(garrison)
|
||||
|
||||
def __contains__(self, item: VehicleGroupGroundObject) -> bool:
|
||||
return item in self.in_priority_order
|
||||
|
||||
@classmethod
|
||||
def for_control_point(cls, control_point: ControlPoint) -> Garrisons:
|
||||
"""Categorize garrison groups based on target priority.
|
||||
|
||||
Any garrisons blocking base capture are the highest priority.
|
||||
"""
|
||||
blocking = []
|
||||
defending = []
|
||||
garrisons = [
|
||||
tgo
|
||||
for tgo in control_point.ground_objects
|
||||
if isinstance(tgo, VehicleGroupGroundObject) and not tgo.is_dead
|
||||
]
|
||||
for garrison in garrisons:
|
||||
if (
|
||||
meters(garrison.distance_to(control_point))
|
||||
< ControlPoint.CAPTURE_DISTANCE
|
||||
):
|
||||
blocking.append(garrison)
|
||||
else:
|
||||
defending.append(garrison)
|
||||
|
||||
return Garrisons(blocking, defending)
|
||||
@@ -1,58 +0,0 @@
|
||||
from dataclasses import field, dataclass
|
||||
from enum import Enum, auto
|
||||
from typing import Optional
|
||||
|
||||
from game.theater import MissionTarget
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
class EscortType(Enum):
|
||||
AirToAir = auto()
|
||||
Sead = auto()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedFlight:
|
||||
"""A flight outline proposed by the mission planner.
|
||||
|
||||
Proposed flights haven't been assigned specific aircraft yet. They have only
|
||||
a task, a required number of aircraft, and a maximum distance allowed
|
||||
between the objective and the departure airfield.
|
||||
"""
|
||||
|
||||
#: The flight's role.
|
||||
task: FlightType
|
||||
|
||||
#: The number of aircraft required.
|
||||
num_aircraft: int
|
||||
|
||||
#: The type of threat this flight defends against if it is an escort. Escort
|
||||
#: flights will be pruned if the rest of the package is not threatened by
|
||||
#: the threat they defend against. If this flight is not an escort, this
|
||||
#: field is None.
|
||||
escort_type: Optional[EscortType] = field(default=None)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.task} {self.num_aircraft} ship"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProposedMission:
|
||||
"""A mission outline proposed by the mission planner.
|
||||
|
||||
Proposed missions haven't been assigned aircraft yet. They have only an
|
||||
objective location and a list of proposed flights that are required for the
|
||||
mission.
|
||||
"""
|
||||
|
||||
#: The mission objective.
|
||||
location: MissionTarget
|
||||
|
||||
#: The proposed flights that are required for the mission.
|
||||
flights: list[ProposedFlight]
|
||||
|
||||
asap: bool = field(default=False)
|
||||
|
||||
def __str__(self) -> str:
|
||||
flights = ", ".join([str(f) for f in self.flights])
|
||||
return f"{self.location.name}: {flights}"
|
||||
@@ -1,76 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
from typing import Iterator, Dict, TYPE_CHECKING
|
||||
|
||||
from game.theater import MissionTarget
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.flights.traveltime import TotEstimator
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class MissionScheduler:
|
||||
def __init__(self, coalition: Coalition, desired_mission_length: timedelta) -> None:
|
||||
self.coalition = coalition
|
||||
self.desired_mission_length = desired_mission_length
|
||||
|
||||
def schedule_missions(self) -> None:
|
||||
"""Identifies and plans mission for the turn."""
|
||||
|
||||
def start_time_generator(
|
||||
count: int, earliest: int, latest: int, margin: int
|
||||
) -> Iterator[timedelta]:
|
||||
interval = (latest - earliest) // count
|
||||
for time in range(earliest, latest, interval):
|
||||
error = random.randint(-margin, margin)
|
||||
yield timedelta(seconds=max(0, time + error))
|
||||
|
||||
dca_types = {
|
||||
FlightType.BARCAP,
|
||||
FlightType.TARCAP,
|
||||
}
|
||||
|
||||
previous_cap_end_time: Dict[MissionTarget, timedelta] = defaultdict(timedelta)
|
||||
non_dca_packages = [
|
||||
p for p in self.coalition.ato.packages if p.primary_task not in dca_types
|
||||
]
|
||||
|
||||
start_time = start_time_generator(
|
||||
count=len(non_dca_packages),
|
||||
earliest=5 * 60,
|
||||
latest=int(self.desired_mission_length.total_seconds()),
|
||||
margin=5 * 60,
|
||||
)
|
||||
for package in self.coalition.ato.packages:
|
||||
tot = TotEstimator(package).earliest_tot()
|
||||
if package.primary_task in dca_types:
|
||||
previous_end_time = previous_cap_end_time[package.target]
|
||||
if tot > previous_end_time:
|
||||
# Can't get there exactly on time, so get there ASAP. This
|
||||
# will typically only happen for the first CAP at each
|
||||
# target.
|
||||
package.time_over_target = tot
|
||||
else:
|
||||
package.time_over_target = previous_end_time
|
||||
|
||||
departure_time = package.mission_departure_time
|
||||
# Should be impossible for CAPs
|
||||
if departure_time is None:
|
||||
logging.error(f"Could not determine mission end time for {package}")
|
||||
continue
|
||||
previous_cap_end_time[package.target] = departure_time
|
||||
elif package.auto_asap:
|
||||
package.set_tot_asap()
|
||||
else:
|
||||
# But other packages should be spread out a bit. Note that take
|
||||
# times are delayed, but all aircraft will become active at
|
||||
# mission start. This makes it more worthwhile to attack enemy
|
||||
# airfields to hit grounded aircraft, since they're more likely
|
||||
# to be present. Runway and air started aircraft will be
|
||||
# delayed until their takeoff time by AirConflictGenerator.
|
||||
package.time_over_target = next(start_time) + tot
|
||||
@@ -1,246 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import operator
|
||||
from collections import Iterator, Iterable
|
||||
from typing import TypeVar, TYPE_CHECKING
|
||||
|
||||
from game.theater import (
|
||||
ControlPoint,
|
||||
OffMapSpawn,
|
||||
MissionTarget,
|
||||
Fob,
|
||||
FrontLine,
|
||||
Airfield,
|
||||
)
|
||||
from game.theater.theatergroundobject import (
|
||||
BuildingGroundObject,
|
||||
IadsGroundObject,
|
||||
NavalGroundObject,
|
||||
)
|
||||
from game.utils import meters, nautical_miles
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache, ClosestAirfields
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.transfers import CargoShip, Convoy
|
||||
|
||||
MissionTargetType = TypeVar("MissionTargetType", bound=MissionTarget)
|
||||
|
||||
|
||||
class ObjectiveFinder:
|
||||
"""Identifies potential objectives for the mission planner."""
|
||||
|
||||
# TODO: Merge into doctrine.
|
||||
AIRFIELD_THREAT_RANGE = nautical_miles(150)
|
||||
SAM_THREAT_RANGE = nautical_miles(100)
|
||||
|
||||
def __init__(self, game: Game, is_player: bool) -> None:
|
||||
self.game = game
|
||||
self.is_player = is_player
|
||||
|
||||
def enemy_air_defenses(self) -> Iterator[IadsGroundObject]:
|
||||
"""Iterates over all enemy SAM sites."""
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if isinstance(ground_object, IadsGroundObject):
|
||||
yield ground_object
|
||||
|
||||
def enemy_ships(self) -> Iterator[NavalGroundObject]:
|
||||
for cp in self.enemy_control_points():
|
||||
for ground_object in cp.ground_objects:
|
||||
if not isinstance(ground_object, NavalGroundObject):
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
yield ground_object
|
||||
|
||||
def threatening_ships(self) -> Iterator[NavalGroundObject]:
|
||||
"""Iterates over enemy ships near friendly control points.
|
||||
|
||||
Groups are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
return self._targets_by_range(self.enemy_ships())
|
||||
|
||||
def _targets_by_range(
|
||||
self, targets: Iterable[MissionTargetType]
|
||||
) -> Iterator[MissionTargetType]:
|
||||
target_ranges: list[tuple[MissionTargetType, float]] = []
|
||||
for target in targets:
|
||||
ranges: list[float] = []
|
||||
for cp in self.friendly_control_points():
|
||||
ranges.append(target.distance_to(cp))
|
||||
target_ranges.append((target, min(ranges)))
|
||||
|
||||
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
def strike_targets(self) -> Iterator[BuildingGroundObject]:
|
||||
"""Iterates over enemy strike targets.
|
||||
|
||||
Targets are sorted by their closest proximity to any friendly control
|
||||
point (airfield or fleet).
|
||||
"""
|
||||
targets: list[tuple[BuildingGroundObject, float]] = []
|
||||
# Building objectives are made of several individual TGOs (one per
|
||||
# building).
|
||||
found_targets: set[str] = set()
|
||||
for enemy_cp in self.enemy_control_points():
|
||||
for ground_object in enemy_cp.ground_objects:
|
||||
# TODO: Reuse ground_object.mission_types.
|
||||
# The mission types for ground objects are currently not
|
||||
# accurate because we include things like strike and BAI for all
|
||||
# targets since they have different planning behavior (waypoint
|
||||
# generation is better for players with strike when the targets
|
||||
# are stationary, AI behavior against weaker air defenses is
|
||||
# better with BAI), so that's not a useful filter. Once we have
|
||||
# better control over planning profiles and target dependent
|
||||
# loadouts we can clean this up.
|
||||
if not isinstance(ground_object, BuildingGroundObject):
|
||||
# Other group types (like ships, SAMs, garrisons, etc) have better
|
||||
# suited mission types like anti-ship, DEAD, and BAI.
|
||||
continue
|
||||
|
||||
if isinstance(enemy_cp, Fob) and ground_object.is_control_point:
|
||||
# This is the FOB structure itself. Can't be repaired or
|
||||
# targeted by the player, so shouldn't be targetable by the
|
||||
# AI.
|
||||
continue
|
||||
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
if ground_object.name in found_targets:
|
||||
continue
|
||||
ranges: list[float] = []
|
||||
for friendly_cp in self.friendly_control_points():
|
||||
ranges.append(ground_object.distance_to(friendly_cp))
|
||||
targets.append((ground_object, min(ranges)))
|
||||
found_targets.add(ground_object.name)
|
||||
targets = sorted(targets, key=operator.itemgetter(1))
|
||||
for target, _range in targets:
|
||||
yield target
|
||||
|
||||
def front_lines(self) -> Iterator[FrontLine]:
|
||||
"""Iterates over all active front lines in the theater."""
|
||||
yield from self.game.theater.conflicts()
|
||||
|
||||
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
|
||||
|
||||
Vulnerability is defined as any enemy CP within threat range of of the
|
||||
CP.
|
||||
"""
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
# Off-map spawn locations don't need protection.
|
||||
continue
|
||||
airfields_in_proximity = self.closest_airfields_to(cp)
|
||||
airfields_in_threat_range = (
|
||||
airfields_in_proximity.operational_airfields_within(
|
||||
self.AIRFIELD_THREAT_RANGE
|
||||
)
|
||||
)
|
||||
for airfield in airfields_in_threat_range:
|
||||
if not airfield.is_friendly(self.is_player):
|
||||
yield cp
|
||||
break
|
||||
|
||||
def oca_targets(self, min_aircraft: int) -> Iterator[ControlPoint]:
|
||||
airfields = []
|
||||
for control_point in self.enemy_control_points():
|
||||
if not isinstance(control_point, Airfield):
|
||||
continue
|
||||
if control_point.allocated_aircraft().total_present >= min_aircraft:
|
||||
airfields.append(control_point)
|
||||
return self._targets_by_range(airfields)
|
||||
|
||||
def convoys(self) -> Iterator[Convoy]:
|
||||
for front_line in self.front_lines():
|
||||
yield from self.game.coalition_for(
|
||||
self.is_player
|
||||
).transfers.convoys.travelling_to(
|
||||
front_line.control_point_hostile_to(self.is_player)
|
||||
)
|
||||
|
||||
def cargo_ships(self) -> Iterator[CargoShip]:
|
||||
for front_line in self.front_lines():
|
||||
yield from self.game.coalition_for(
|
||||
self.is_player
|
||||
).transfers.cargo_ships.travelling_to(
|
||||
front_line.control_point_hostile_to(self.is_player)
|
||||
)
|
||||
|
||||
def friendly_control_points(self) -> Iterator[ControlPoint]:
|
||||
"""Iterates over all friendly control points."""
|
||||
return (
|
||||
c for c in self.game.theater.controlpoints if c.is_friendly(self.is_player)
|
||||
)
|
||||
|
||||
def farthest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is farthest from any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
farthest = None
|
||||
max_distance = meters(0)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
continue
|
||||
distance = threat_zones.distance_to_threat(cp.position)
|
||||
if distance > max_distance:
|
||||
farthest = cp
|
||||
max_distance = distance
|
||||
|
||||
if farthest is None:
|
||||
raise RuntimeError("Found no friendly control points. You probably lost.")
|
||||
return farthest
|
||||
|
||||
def closest_friendly_control_point(self) -> ControlPoint:
|
||||
"""Finds the friendly control point that is closest to any threats."""
|
||||
threat_zones = self.game.threat_zone_for(not self.is_player)
|
||||
|
||||
closest = None
|
||||
min_distance = meters(math.inf)
|
||||
for cp in self.friendly_control_points():
|
||||
if isinstance(cp, OffMapSpawn):
|
||||
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]:
|
||||
"""Iterates over all enemy control points."""
|
||||
return (
|
||||
c
|
||||
for c in self.game.theater.controlpoints
|
||||
if not c.is_friendly(self.is_player)
|
||||
)
|
||||
|
||||
def prioritized_unisolated_points(self) -> list[ControlPoint]:
|
||||
prioritized = []
|
||||
capturable_later = []
|
||||
for cp in self.game.theater.control_points_for(not self.is_player):
|
||||
if cp.is_isolated:
|
||||
continue
|
||||
if cp.has_active_frontline:
|
||||
prioritized.append(cp)
|
||||
else:
|
||||
capturable_later.append(cp)
|
||||
prioritized.extend(self._targets_by_range(capturable_later))
|
||||
return prioritized
|
||||
|
||||
@staticmethod
|
||||
def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:
|
||||
"""Returns the closest airfields to the given location."""
|
||||
return ObjectiveDistanceCache.get_closest_airfields(location)
|
||||
@@ -1,94 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from game.utils import nautical_miles
|
||||
from gen.ato import Package
|
||||
from game.theater import MissionTarget, OffMapSpawn, ControlPoint
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.squadrons.airwing import AirWing
|
||||
from gen.flights.closestairfields import ClosestAirfields
|
||||
from .missionproposals import ProposedFlight
|
||||
|
||||
|
||||
class PackageBuilder:
|
||||
"""Builds a Package for the flights it receives."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
location: MissionTarget,
|
||||
closest_airfields: ClosestAirfields,
|
||||
air_wing: AirWing,
|
||||
is_player: bool,
|
||||
package_country: str,
|
||||
start_type: str,
|
||||
asap: bool,
|
||||
) -> None:
|
||||
self.closest_airfields = closest_airfields
|
||||
self.is_player = is_player
|
||||
self.package_country = package_country
|
||||
self.package = Package(location, auto_asap=asap)
|
||||
self.air_wing = air_wing
|
||||
self.start_type = start_type
|
||||
|
||||
def plan_flight(self, plan: ProposedFlight) -> bool:
|
||||
"""Allocates aircraft for the given flight and adds them to the package.
|
||||
|
||||
If no suitable aircraft are available, False is returned. If the failed
|
||||
flight was critical and the rest of the mission will be scrubbed, the
|
||||
caller should return any previously planned flights to the inventory
|
||||
using release_planned_aircraft.
|
||||
"""
|
||||
squadron = self.air_wing.best_squadron_for(
|
||||
self.package.target, plan.task, plan.num_aircraft, this_turn=True
|
||||
)
|
||||
if squadron is None:
|
||||
return False
|
||||
start_type = squadron.location.required_aircraft_start_type
|
||||
if start_type is None:
|
||||
start_type = self.start_type
|
||||
|
||||
flight = Flight(
|
||||
self.package,
|
||||
self.package_country,
|
||||
squadron,
|
||||
plan.num_aircraft,
|
||||
plan.task,
|
||||
start_type,
|
||||
divert=self.find_divert_field(squadron.aircraft, squadron.location),
|
||||
)
|
||||
self.package.add_flight(flight)
|
||||
return True
|
||||
|
||||
def find_divert_field(
|
||||
self, aircraft: AircraftType, arrival: ControlPoint
|
||||
) -> Optional[ControlPoint]:
|
||||
divert_limit = nautical_miles(150)
|
||||
for airfield in self.closest_airfields.operational_airfields_within(
|
||||
divert_limit
|
||||
):
|
||||
if airfield.captured != self.is_player:
|
||||
continue
|
||||
if airfield == arrival:
|
||||
continue
|
||||
if not airfield.can_operate(aircraft):
|
||||
continue
|
||||
if isinstance(airfield, OffMapSpawn):
|
||||
continue
|
||||
return airfield
|
||||
return None
|
||||
|
||||
def build(self) -> Package:
|
||||
"""Returns the built package."""
|
||||
return self.package
|
||||
|
||||
def release_planned_aircraft(self) -> None:
|
||||
"""Returns any planned flights to the inventory."""
|
||||
flights = list(self.package.flights)
|
||||
for flight in flights:
|
||||
flight.return_pilots_and_aircraft()
|
||||
self.package.remove_flight(flight)
|
||||
@@ -1,221 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from typing import Set, Iterable, Dict, TYPE_CHECKING, Optional
|
||||
|
||||
from game.commander.missionproposals import ProposedMission, ProposedFlight, EscortType
|
||||
from game.commander.packagebuilder import PackageBuilder
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.procurement import AircraftProcurementRequest
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.settings import Settings
|
||||
from game.squadrons import AirWing
|
||||
from game.theater import ConflictTheater
|
||||
from game.threatzones import ThreatZones
|
||||
from gen.ato import AirTaskingOrder, Package
|
||||
from gen.flights.closestairfields import ObjectiveDistanceCache
|
||||
from gen.flights.flight import FlightType
|
||||
from gen.flights.flightplan import FlightPlanBuilder
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class PackageFulfiller:
|
||||
"""Responsible for package aircraft allocation and flight plan layout."""
|
||||
|
||||
def __init__(
|
||||
self, coalition: Coalition, theater: ConflictTheater, settings: Settings
|
||||
) -> None:
|
||||
self.coalition = coalition
|
||||
self.theater = theater
|
||||
self.player_missions_asap = settings.auto_ato_player_missions_asap
|
||||
self.default_start_type = settings.default_start_type
|
||||
|
||||
@property
|
||||
def is_player(self) -> bool:
|
||||
return self.coalition.player
|
||||
|
||||
@property
|
||||
def ato(self) -> AirTaskingOrder:
|
||||
return self.coalition.ato
|
||||
|
||||
@property
|
||||
def air_wing(self) -> AirWing:
|
||||
return self.coalition.air_wing
|
||||
|
||||
@property
|
||||
def doctrine(self) -> Doctrine:
|
||||
return self.coalition.doctrine
|
||||
|
||||
@property
|
||||
def threat_zones(self) -> ThreatZones:
|
||||
return self.coalition.opponent.threat_zone
|
||||
|
||||
def add_procurement_request(self, request: AircraftProcurementRequest) -> None:
|
||||
self.coalition.add_procurement_request(request)
|
||||
|
||||
def air_wing_can_plan(self, mission_type: FlightType) -> bool:
|
||||
"""Returns True if it is possible for the air wing to plan this mission type.
|
||||
|
||||
Not all mission types can be fulfilled by all air wings. Many factions do not
|
||||
have AEW&C aircraft, so they will never be able to plan those missions. It's
|
||||
also possible for the player to exclude mission types from their squadron
|
||||
designs.
|
||||
"""
|
||||
return self.air_wing.can_auto_plan(mission_type)
|
||||
|
||||
def plan_flight(
|
||||
self,
|
||||
mission: ProposedMission,
|
||||
flight: ProposedFlight,
|
||||
builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
purchase_multiplier: int,
|
||||
) -> None:
|
||||
if not builder.plan_flight(flight):
|
||||
missing_types.add(flight.task)
|
||||
purchase_order = AircraftProcurementRequest(
|
||||
near=mission.location,
|
||||
task_capability=flight.task,
|
||||
number=flight.num_aircraft * purchase_multiplier,
|
||||
)
|
||||
# Reserves are planned for critical missions, so prioritize those orders
|
||||
# over aircraft needed for non-critical missions.
|
||||
self.add_procurement_request(purchase_order)
|
||||
|
||||
def scrub_mission_missing_aircraft(
|
||||
self,
|
||||
mission: ProposedMission,
|
||||
builder: PackageBuilder,
|
||||
missing_types: Set[FlightType],
|
||||
not_attempted: Iterable[ProposedFlight],
|
||||
purchase_multiplier: int,
|
||||
) -> None:
|
||||
# Try to plan the rest of the mission just so we can count the missing
|
||||
# types to buy.
|
||||
for flight in not_attempted:
|
||||
self.plan_flight(
|
||||
mission, flight, builder, missing_types, purchase_multiplier
|
||||
)
|
||||
|
||||
missing_types_str = ", ".join(sorted([t.name for t in missing_types]))
|
||||
builder.release_planned_aircraft()
|
||||
color = "Blue" if self.is_player else "Red"
|
||||
logging.debug(
|
||||
f"{color}: not enough aircraft in range for {mission.location.name} "
|
||||
f"capable of: {missing_types_str}"
|
||||
)
|
||||
|
||||
def check_needed_escorts(self, builder: PackageBuilder) -> Dict[EscortType, bool]:
|
||||
threats = defaultdict(bool)
|
||||
for flight in builder.package.flights:
|
||||
if self.threat_zones.waypoints_threatened_by_aircraft(
|
||||
flight.flight_plan.escorted_waypoints()
|
||||
):
|
||||
threats[EscortType.AirToAir] = True
|
||||
if self.threat_zones.waypoints_threatened_by_radar_sam(
|
||||
list(flight.flight_plan.escorted_waypoints())
|
||||
):
|
||||
threats[EscortType.Sead] = True
|
||||
return threats
|
||||
|
||||
def plan_mission(
|
||||
self,
|
||||
mission: ProposedMission,
|
||||
purchase_multiplier: int,
|
||||
tracer: MultiEventTracer,
|
||||
) -> Optional[Package]:
|
||||
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
|
||||
builder = PackageBuilder(
|
||||
mission.location,
|
||||
ObjectiveDistanceCache.get_closest_airfields(mission.location),
|
||||
self.air_wing,
|
||||
self.is_player,
|
||||
self.coalition.country_name,
|
||||
self.default_start_type,
|
||||
mission.asap,
|
||||
)
|
||||
|
||||
# Attempt to plan all the main elements of the mission first. Escorts
|
||||
# will be planned separately so we can prune escorts for packages that
|
||||
# are not expected to encounter that type of threat.
|
||||
missing_types: Set[FlightType] = set()
|
||||
escorts = []
|
||||
for proposed_flight in mission.flights:
|
||||
if not self.air_wing_can_plan(proposed_flight.task):
|
||||
# This air wing can never plan this mission type because they do not
|
||||
# have compatible aircraft or squadrons. Skip fulfillment so that we
|
||||
# don't place the purchase request.
|
||||
continue
|
||||
if proposed_flight.escort_type is not None:
|
||||
# Escorts are planned after the primary elements of the package.
|
||||
# If the package does not need escorts they may be pruned.
|
||||
escorts.append(proposed_flight)
|
||||
continue
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(
|
||||
mission,
|
||||
proposed_flight,
|
||||
builder,
|
||||
missing_types,
|
||||
purchase_multiplier,
|
||||
)
|
||||
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(
|
||||
mission, builder, missing_types, escorts, purchase_multiplier
|
||||
)
|
||||
return None
|
||||
|
||||
if not builder.package.flights:
|
||||
# The non-escort part of this mission is unplannable by this faction. Scrub
|
||||
# the mission and do not attempt planning escorts because there's no reason
|
||||
# to buy them because this mission will never be planned.
|
||||
return None
|
||||
|
||||
# Create flight plans for the main flights of the package so we can
|
||||
# determine threats. This is done *after* creating all of the flights
|
||||
# rather than as each flight is added because the flight plan for
|
||||
# flights that will rendezvous with their package will be affected by
|
||||
# the other flights in the package. Escorts will not be able to
|
||||
# contribute to this.
|
||||
flight_plan_builder = FlightPlanBuilder(
|
||||
builder.package, self.coalition, self.theater
|
||||
)
|
||||
for flight in builder.package.flights:
|
||||
with tracer.trace("Flight plan population"):
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
needed_escorts = self.check_needed_escorts(builder)
|
||||
for escort in escorts:
|
||||
# This list was generated from the not None set, so this should be
|
||||
# impossible.
|
||||
assert escort.escort_type is not None
|
||||
if needed_escorts[escort.escort_type]:
|
||||
with tracer.trace("Flight planning"):
|
||||
self.plan_flight(
|
||||
mission, escort, builder, missing_types, purchase_multiplier
|
||||
)
|
||||
|
||||
# Check again for unavailable aircraft. If the escort was required and
|
||||
# none were found, scrub the mission.
|
||||
if missing_types:
|
||||
self.scrub_mission_missing_aircraft(
|
||||
mission, builder, missing_types, escorts, purchase_multiplier
|
||||
)
|
||||
return None
|
||||
|
||||
package = builder.build()
|
||||
# Add flight plans for escorts.
|
||||
for flight in package.flights:
|
||||
if not flight.flight_plan.waypoints:
|
||||
with tracer.trace("Flight plan population"):
|
||||
flight_plan_builder.populate_flight_plan(flight)
|
||||
|
||||
if package.has_players and self.player_missions_asap:
|
||||
package.auto_asap = True
|
||||
package.set_tot_asap()
|
||||
|
||||
return package
|
||||
@@ -1,11 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.aewc import PlanAewc
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class PlanAewcSupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for target in state.aewc_targets:
|
||||
yield [PlanAewc(target)]
|
||||
@@ -1,15 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.primitive.oca import PlanOcaStrike
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AttackAirInfrastructure(CompoundTask[TheaterState]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for garrison in state.oca_targets:
|
||||
yield [PlanOcaStrike(garrison, self.aircraft_cold_start)]
|
||||
@@ -1,15 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.strike import PlanStrike
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class AttackBuildings(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for building in state.strike_targets:
|
||||
# Ammo depots are targeted based on the needs of the front line by
|
||||
# ReduceEnemyFrontLineCapacity. No reason to target them before that front
|
||||
# line is active.
|
||||
if not building.is_ammo_depot:
|
||||
yield [PlanStrike(building)]
|
||||
@@ -1,12 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.bai import PlanBai
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class AttackGarrisons(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for garrisons in state.enemy_garrisons.values():
|
||||
for garrison in garrisons.in_priority_order:
|
||||
yield [PlanBai(garrison)]
|
||||
@@ -1,51 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.destroyenemygroundunits import (
|
||||
DestroyEnemyGroundUnits,
|
||||
)
|
||||
from game.commander.tasks.compound.reduceenemyfrontlinecapacity import (
|
||||
ReduceEnemyFrontLineCapacity,
|
||||
)
|
||||
from game.commander.tasks.primitive.breakthroughattack import BreakthroughAttack
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater import FrontLine, ControlPoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaptureBase(CompoundTask[TheaterState]):
|
||||
front_line: FrontLine
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [BreakthroughAttack(self.front_line, state.context.coalition.player)]
|
||||
yield [DestroyEnemyGroundUnits(self.front_line)]
|
||||
if self.worth_destroying_ammo_depots(state):
|
||||
yield [ReduceEnemyFrontLineCapacity(self.enemy_cp(state))]
|
||||
|
||||
def enemy_cp(self, state: TheaterState) -> ControlPoint:
|
||||
return self.front_line.control_point_hostile_to(state.context.coalition.player)
|
||||
|
||||
def units_deployable(self, state: TheaterState, player: bool) -> int:
|
||||
cp = self.front_line.control_point_friendly_to(player)
|
||||
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||
return cp.deployable_front_line_units_with(len(ammo_depots))
|
||||
|
||||
def unit_cap(self, state: TheaterState, player: bool) -> int:
|
||||
cp = self.front_line.control_point_friendly_to(player)
|
||||
ammo_depots = list(state.ammo_dumps_at(cp))
|
||||
return cp.front_line_capacity_with(len(ammo_depots))
|
||||
|
||||
def enemy_has_ammo_dumps(self, state: TheaterState) -> bool:
|
||||
return bool(state.ammo_dumps_at(self.enemy_cp(state)))
|
||||
|
||||
def worth_destroying_ammo_depots(self, state: TheaterState) -> bool:
|
||||
if not self.enemy_has_ammo_dumps(state):
|
||||
return False
|
||||
|
||||
friendly_cap = self.unit_cap(state, state.context.coalition.player)
|
||||
enemy_deployable = self.units_deployable(state, state.context.coalition.player)
|
||||
|
||||
# If the enemy can currently deploy 50% more units than we possibly could, it's
|
||||
# worth killing an ammo depot.
|
||||
return enemy_deployable / friendly_cap > 1.5
|
||||
@@ -1,13 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.capturebase import CaptureBase
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CaptureBases(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for front in state.active_front_lines:
|
||||
yield [CaptureBase(front)]
|
||||
@@ -1,19 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.primitive.cas import PlanCas
|
||||
from game.commander.tasks.primitive.defensivestance import DefensiveStance
|
||||
from game.commander.tasks.primitive.retreatstance import RetreatStance
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DefendBase(CompoundTask[TheaterState]):
|
||||
front_line: FrontLine
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [DefensiveStance(self.front_line, state.context.coalition.player)]
|
||||
yield [RetreatStance(self.front_line, state.context.coalition.player)]
|
||||
yield [PlanCas(self.front_line)]
|
||||
@@ -1,13 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.defendbase import DefendBase
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DefendBases(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for front in state.active_front_lines:
|
||||
yield [DefendBase(front)]
|
||||
@@ -1,24 +0,0 @@
|
||||
from collections import Iterator
|
||||
from typing import Union
|
||||
|
||||
from game.commander.tasks.primitive.antiship import PlanAntiShip
|
||||
from game.commander.tasks.primitive.dead import PlanDead
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||
|
||||
|
||||
class DegradeIads(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for air_defense in state.threatening_air_defenses:
|
||||
yield [self.plan_against(air_defense)]
|
||||
for detector in state.detecting_air_defenses:
|
||||
yield [self.plan_against(detector)]
|
||||
|
||||
@staticmethod
|
||||
def plan_against(
|
||||
target: Union[IadsGroundObject, NavalGroundObject]
|
||||
) -> Union[PlanDead, PlanAntiShip]:
|
||||
if isinstance(target, IadsGroundObject):
|
||||
return PlanDead(target)
|
||||
return PlanAntiShip(target)
|
||||
@@ -1,19 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.primitive.aggressiveattack import AggressiveAttack
|
||||
from game.commander.tasks.primitive.cas import PlanCas
|
||||
from game.commander.tasks.primitive.eliminationattack import EliminationAttack
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater import FrontLine
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DestroyEnemyGroundUnits(CompoundTask[TheaterState]):
|
||||
front_line: FrontLine
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [EliminationAttack(self.front_line, state.context.coalition.player)]
|
||||
yield [AggressiveAttack(self.front_line, state.context.coalition.player)]
|
||||
yield [PlanCas(self.front_line)]
|
||||
@@ -1,11 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.cas import PlanCas
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class FrontLineDefense(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for front_line in state.vulnerable_front_lines:
|
||||
yield [PlanCas(front_line)]
|
||||
@@ -1,27 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.antishipping import PlanAntiShipping
|
||||
from game.commander.tasks.primitive.convoyinterdiction import PlanConvoyInterdiction
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class InterdictReinforcements(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
# These will only rarely get planned. When a convoy is travelling multiple legs,
|
||||
# they're targetable after the first leg. The reason for this is that
|
||||
# procurement happens *after* mission planning so that the missions that could
|
||||
# not be filled will guide the procurement process. Procurement is the stage
|
||||
# that convoys are created (because they're created to move ground units that
|
||||
# were just purchased), so we haven't created any yet. Any incomplete transfers
|
||||
# from the previous turn (multi-leg journeys) will still be present though so
|
||||
# they can be targeted.
|
||||
#
|
||||
# Even after this is fixed, the player's convoys that were created through the
|
||||
# UI will never be targeted on the first turn of their journey because the AI
|
||||
# stops planning after the start of the turn. We could potentially fix this by
|
||||
# moving opfor mission planning until the takeoff button is pushed.
|
||||
for convoy in state.enemy_convoys:
|
||||
yield [PlanConvoyInterdiction(convoy)]
|
||||
for ship in state.enemy_shipping:
|
||||
yield [PlanAntiShipping(ship)]
|
||||
@@ -1,34 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.attackairinfrastructure import (
|
||||
AttackAirInfrastructure,
|
||||
)
|
||||
from game.commander.tasks.compound.attackbuildings import AttackBuildings
|
||||
from game.commander.tasks.compound.attackgarrisons import AttackGarrisons
|
||||
from game.commander.tasks.compound.capturebases import CaptureBases
|
||||
from game.commander.tasks.compound.defendbases import DefendBases
|
||||
from game.commander.tasks.compound.degradeiads import DegradeIads
|
||||
from game.commander.tasks.compound.interdictreinforcements import (
|
||||
InterdictReinforcements,
|
||||
)
|
||||
from game.commander.tasks.compound.protectairspace import ProtectAirSpace
|
||||
from game.commander.tasks.compound.theatersupport import TheaterSupport
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlanNextAction(CompoundTask[TheaterState]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [TheaterSupport()]
|
||||
yield [ProtectAirSpace()]
|
||||
yield [CaptureBases()]
|
||||
yield [DefendBases()]
|
||||
yield [InterdictReinforcements()]
|
||||
yield [AttackGarrisons()]
|
||||
yield [AttackAirInfrastructure(self.aircraft_cold_start)]
|
||||
yield [AttackBuildings()]
|
||||
yield [DegradeIads()]
|
||||
@@ -1,12 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.barcap import PlanBarcap
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class ProtectAirSpace(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for cp, needed in state.barcaps_needed.items():
|
||||
if needed > 0:
|
||||
yield [PlanBarcap(cp, needed)]
|
||||
@@ -1,16 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.primitive.strike import PlanStrike
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
from game.theater import ControlPoint
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReduceEnemyFrontLineCapacity(CompoundTask[TheaterState]):
|
||||
control_point: ControlPoint
|
||||
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for ammo_dump in state.ammo_dumps_at(self.control_point):
|
||||
yield [PlanStrike(ammo_dump)]
|
||||
@@ -1,11 +0,0 @@
|
||||
from collections import Iterator
|
||||
|
||||
from game.commander.tasks.primitive.refueling import PlanRefueling
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
class PlanRefuelingSupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
for target in state.refueling_targets:
|
||||
yield [PlanRefueling(target)]
|
||||
@@ -1,14 +0,0 @@
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.compound.aewcsupport import PlanAewcSupport
|
||||
from game.commander.tasks.compound.refuelingsupport import PlanRefuelingSupport
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import CompoundTask, Method
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TheaterSupport(CompoundTask[TheaterState]):
|
||||
def each_valid_method(self, state: TheaterState) -> Iterator[Method[TheaterState]]:
|
||||
yield [PlanAewcSupport()]
|
||||
yield [PlanRefuelingSupport()]
|
||||
@@ -1,77 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import FrontLine
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class FrontLineStanceTask(TheaterCommanderTask, ABC):
|
||||
def __init__(self, front_line: FrontLine, player: bool) -> None:
|
||||
self.front_line = front_line
|
||||
self.friendly_cp = self.front_line.control_point_friendly_to(player)
|
||||
self.enemy_cp = self.front_line.control_point_hostile_to(player)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def stance(self) -> CombatStance:
|
||||
...
|
||||
|
||||
@staticmethod
|
||||
def management_allowed(state: TheaterState) -> bool:
|
||||
return (
|
||||
not state.context.coalition.player
|
||||
or state.context.settings.automate_front_line_stance
|
||||
)
|
||||
|
||||
def better_stance_already_set(self, state: TheaterState) -> bool:
|
||||
current_stance = state.front_line_stances[self.front_line]
|
||||
if current_stance is None:
|
||||
return False
|
||||
preference = (
|
||||
CombatStance.RETREAT,
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.AMBUSH,
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.ELIMINATION,
|
||||
CombatStance.BREAKTHROUGH,
|
||||
)
|
||||
current_rating = preference.index(current_stance)
|
||||
new_rating = preference.index(self.stance)
|
||||
return current_rating >= new_rating
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
...
|
||||
|
||||
@property
|
||||
def ground_force_balance(self) -> float:
|
||||
# TODO: Planned CAS missions should reduce the expected opposing force size.
|
||||
friendly_forces = self.friendly_cp.deployable_front_line_units
|
||||
enemy_forces = self.enemy_cp.deployable_front_line_units
|
||||
if enemy_forces == 0:
|
||||
return math.inf
|
||||
return friendly_forces / enemy_forces
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not self.management_allowed(state):
|
||||
return False
|
||||
if self.better_stance_already_set(state):
|
||||
return False
|
||||
if self.friendly_cp.deployable_front_line_units == 0:
|
||||
return False
|
||||
return self.have_sufficient_front_line_advantage
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.front_line_stances[self.front_line] = self.stance
|
||||
|
||||
def execute(self, coalition: Coalition) -> None:
|
||||
self.friendly_cp.stances[self.enemy_cp.id] = self.stance
|
||||
@@ -1,174 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import operator
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from enum import unique, IntEnum, auto
|
||||
from typing import TYPE_CHECKING, Optional, Generic, TypeVar, Iterator, Union
|
||||
|
||||
from game.commander.missionproposals import ProposedFlight, EscortType, ProposedMission
|
||||
from game.commander.packagefulfiller import PackageFulfiller
|
||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.settings import AutoAtoBehavior
|
||||
from game.theater import MissionTarget
|
||||
from game.theater.theatergroundobject import IadsGroundObject, NavalGroundObject
|
||||
from game.utils import Distance, meters
|
||||
from gen.ato import Package
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
MissionTargetT = TypeVar("MissionTargetT", bound=MissionTarget)
|
||||
|
||||
|
||||
@unique
|
||||
class RangeType(IntEnum):
|
||||
Detection = auto()
|
||||
Threat = auto()
|
||||
|
||||
|
||||
# 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
|
||||
class PackagePlanningTask(TheaterCommanderTask, Generic[MissionTargetT]):
|
||||
target: MissionTargetT
|
||||
flights: list[ProposedFlight] = field(init=False)
|
||||
package: Optional[Package] = field(init=False, default=None)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.flights = []
|
||||
self.package = Package(self.target)
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
state.context.coalition.player
|
||||
and state.context.settings.auto_ato_behavior is AutoAtoBehavior.Disabled
|
||||
):
|
||||
return False
|
||||
return self.fulfill_mission(state)
|
||||
|
||||
def execute(self, coalition: Coalition) -> None:
|
||||
if self.package is None:
|
||||
raise RuntimeError("Attempted to execute failed package planning task")
|
||||
coalition.ato.add_package(self.package)
|
||||
|
||||
@abstractmethod
|
||||
def propose_flights(self) -> None:
|
||||
...
|
||||
|
||||
def propose_flight(
|
||||
self,
|
||||
task: FlightType,
|
||||
num_aircraft: int,
|
||||
escort_type: Optional[EscortType] = None,
|
||||
) -> None:
|
||||
self.flights.append(ProposedFlight(task, num_aircraft, escort_type))
|
||||
|
||||
@property
|
||||
def asap(self) -> bool:
|
||||
return False
|
||||
|
||||
@property
|
||||
def purchase_multiplier(self) -> int:
|
||||
"""The multiplier for aircraft quantity when missions could not be fulfilled.
|
||||
|
||||
For missions that do not schedule in rounds like BARCAPs do, this should be one
|
||||
to ensure that the we only purchase enough aircraft to plan the mission once.
|
||||
|
||||
For missions that repeat within the same turn, however, we may need to buy for
|
||||
the same mission more than once. If three rounds of BARCAP still need to be
|
||||
fulfilled, this would return 3, and we'd triplicate the purchase order.
|
||||
|
||||
There is a small misbehavior here that's not symptomatic for our current mission
|
||||
planning: multi-round, multi-flight packages will only purchase multiple sets of
|
||||
aircraft for whatever is unavailable for the *first* failed package. For
|
||||
example, if we extend this to CAS and have no CAS aircraft but enough TARCAP
|
||||
aircraft for one round, we'll order CAS for every round but will not order any
|
||||
TARCAP aircraft, since we can't know that TARCAP aircraft are needed until we
|
||||
attempt to plan the second mission *without returning the first round aircraft*.
|
||||
"""
|
||||
return 1
|
||||
|
||||
def fulfill_mission(self, state: TheaterState) -> bool:
|
||||
self.propose_flights()
|
||||
fulfiller = PackageFulfiller(
|
||||
state.context.coalition,
|
||||
state.context.theater,
|
||||
state.context.settings,
|
||||
)
|
||||
self.package = fulfiller.plan_mission(
|
||||
ProposedMission(self.target, self.flights),
|
||||
self.purchase_multiplier,
|
||||
state.context.tracer,
|
||||
)
|
||||
return self.package is not None
|
||||
|
||||
def propose_common_escorts(self) -> None:
|
||||
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
|
||||
def iter_iads_ranges(
|
||||
self, state: TheaterState, range_type: RangeType
|
||||
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
||||
target_ranges: list[
|
||||
tuple[Union[IadsGroundObject, NavalGroundObject], Distance]
|
||||
] = []
|
||||
all_iads: Iterator[
|
||||
Union[IadsGroundObject, NavalGroundObject]
|
||||
] = itertools.chain(state.enemy_air_defenses, state.enemy_ships)
|
||||
for target in all_iads:
|
||||
distance = meters(target.distance_to(self.target))
|
||||
if range_type is RangeType.Detection:
|
||||
target_range = target.max_detection_range()
|
||||
elif range_type is RangeType.Threat:
|
||||
target_range = target.max_threat_range()
|
||||
else:
|
||||
raise ValueError(f"Unknown RangeType: {range_type}")
|
||||
if not target_range:
|
||||
continue
|
||||
|
||||
# IADS out of range of our target area will have a positive
|
||||
# distance_to_threat and should be pruned. The rest have a decreasing
|
||||
# distance_to_threat as overlap increases. The most negative distance has
|
||||
# the greatest coverage of the target and should be treated as the highest
|
||||
# priority threat.
|
||||
distance_to_threat = distance - target_range
|
||||
if distance_to_threat > meters(0):
|
||||
continue
|
||||
target_ranges.append((target, distance_to_threat))
|
||||
|
||||
# TODO: Prioritize IADS by vulnerability?
|
||||
target_ranges = sorted(target_ranges, key=operator.itemgetter(1))
|
||||
for target, _range in target_ranges:
|
||||
yield target
|
||||
|
||||
def iter_detecting_iads(
|
||||
self, state: TheaterState
|
||||
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
||||
return self.iter_iads_ranges(state, RangeType.Detection)
|
||||
|
||||
def iter_iads_threats(
|
||||
self, state: TheaterState
|
||||
) -> Iterator[Union[IadsGroundObject, NavalGroundObject]]:
|
||||
return self.iter_iads_ranges(state, RangeType.Threat)
|
||||
|
||||
def target_area_preconditions_met(
|
||||
self, state: TheaterState, ignore_iads: bool = False
|
||||
) -> bool:
|
||||
"""Checks if the target area has been cleared of threats."""
|
||||
threatened = False
|
||||
|
||||
# Non-blocking, but analyzed so we can pick detectors worth eliminating.
|
||||
for detector in self.iter_detecting_iads(state):
|
||||
if detector not in state.detecting_air_defenses:
|
||||
state.detecting_air_defenses.append(detector)
|
||||
|
||||
if not ignore_iads:
|
||||
for iads_threat in self.iter_iads_threats(state):
|
||||
threatened = True
|
||||
if iads_threat not in state.threatening_air_defenses:
|
||||
state.threatening_air_defenses.append(iads_threat)
|
||||
return not threatened
|
||||
@@ -1,27 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import MissionTarget
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAewc(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
return self.target in state.aewc_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.aewc_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.AEWC, 1)
|
||||
|
||||
@property
|
||||
def asap(self) -> bool:
|
||||
# Supports all the early CAP flights, so should be in the air ASAP.
|
||||
return True
|
||||
@@ -1,14 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class AggressiveAttack(FrontLineStanceTask):
|
||||
@property
|
||||
def stance(self) -> CombatStance:
|
||||
return CombatStance.AGGRESSIVE
|
||||
|
||||
@property
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
return self.ground_force_balance >= 0.8
|
||||
@@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import NavalGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAntiShip(PackagePlanningTask[NavalGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.threatening_air_defenses:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_ship(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, 2)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
@@ -1,25 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.transfers import CargoShip
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanAntiShipping(PackagePlanningTask[CargoShip]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.enemy_shipping:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_shipping.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.ANTISHIP, 2)
|
||||
self.propose_common_escorts()
|
||||
@@ -1,25 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import VehicleGroupGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanBai(PackagePlanningTask[VehicleGroupGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not state.has_garrison(self.target):
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_garrison(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BAI, 2)
|
||||
self.propose_common_escorts()
|
||||
@@ -1,28 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanBarcap(PackagePlanningTask[ControlPoint]):
|
||||
max_orders: int
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not state.barcaps_needed[self.target]:
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.barcaps_needed[self.target] -= 1
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BARCAP, 2)
|
||||
|
||||
@property
|
||||
def purchase_multiplier(self) -> int:
|
||||
return self.max_orders
|
||||
@@ -1,28 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class BreakthroughAttack(FrontLineStanceTask):
|
||||
@property
|
||||
def stance(self) -> CombatStance:
|
||||
return CombatStance.BREAKTHROUGH
|
||||
|
||||
@property
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
return self.ground_force_balance >= 2.0
|
||||
|
||||
def opposing_garrisons_eliminated(self, state: TheaterState) -> bool:
|
||||
garrisons = state.enemy_garrisons[self.enemy_cp]
|
||||
return not bool(garrisons.blocking_capture)
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
return self.opposing_garrisons_eliminated(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
super().apply_effects(state)
|
||||
state.active_front_lines.remove(self.front_line)
|
||||
@@ -1,33 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import FrontLine
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanCas(PackagePlanningTask[FrontLine]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.vulnerable_front_lines:
|
||||
return False
|
||||
|
||||
# Do not bother planning CAS when there are no enemy ground units at the front.
|
||||
# An exception is made for turn zero since that's not being truly planned, but
|
||||
# just to determine what missions should be planned on turn 1 (when there *will*
|
||||
# be ground units) and what aircraft should be ordered.
|
||||
enemy_cp = self.target.control_point_friendly_to(
|
||||
player=not state.context.coalition.player
|
||||
)
|
||||
if enemy_cp.deployable_front_line_units == 0 and state.context.turn > 0:
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.vulnerable_front_lines.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.CAS, 2)
|
||||
self.propose_flight(FlightType.TARCAP, 2)
|
||||
@@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.data.doctrine import Doctrine
|
||||
from game.transfers import Convoy
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanConvoyInterdiction(PackagePlanningTask[Convoy]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.enemy_convoys:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.enemy_convoys.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.BAI, 2)
|
||||
self.propose_common_escorts()
|
||||
@@ -1,46 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.missionproposals import EscortType
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import IadsGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanDead(PackagePlanningTask[IadsGroundObject]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if (
|
||||
self.target not in state.threatening_air_defenses
|
||||
and self.target not in state.detecting_air_defenses
|
||||
):
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state, ignore_iads=True):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.eliminate_air_defense(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.DEAD, 2)
|
||||
|
||||
# Only include SEAD against SAMs that still have emitters. No need to
|
||||
# suppress an EWR, and SEAD isn't useful against a SAM that no longer has a
|
||||
# working track radar.
|
||||
#
|
||||
# For SAMs without track radars and EWRs, we still want a SEAD escort if
|
||||
# needed.
|
||||
#
|
||||
# Note that there is a quirk here: we should potentially be included a SEAD
|
||||
# escort *and* SEAD when the target is a radar SAM but the flight path is
|
||||
# also threatened by SAMs. We don't want to include a SEAD escort if the
|
||||
# package is *only* threatened by the target though. Could be improved, but
|
||||
# needs a decent refactor to the escort planning to do so.
|
||||
if self.target.has_live_radar_sam:
|
||||
self.propose_flight(FlightType.SEAD, 2)
|
||||
else:
|
||||
self.propose_flight(FlightType.SEAD_ESCORT, 2, EscortType.Sead)
|
||||
self.propose_flight(FlightType.ESCORT, 2, EscortType.AirToAir)
|
||||
@@ -1,14 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class DefensiveStance(FrontLineStanceTask):
|
||||
@property
|
||||
def stance(self) -> CombatStance:
|
||||
return CombatStance.DEFENSIVE
|
||||
|
||||
@property
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
return self.ground_force_balance >= 0.5
|
||||
@@ -1,14 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class EliminationAttack(FrontLineStanceTask):
|
||||
@property
|
||||
def stance(self) -> CombatStance:
|
||||
return CombatStance.ELIMINATION
|
||||
|
||||
@property
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
return self.ground_force_balance >= 1.5
|
||||
@@ -1,29 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import ControlPoint
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanOcaStrike(PackagePlanningTask[ControlPoint]):
|
||||
aircraft_cold_start: bool
|
||||
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.oca_targets:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.oca_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.OCA_RUNWAY, 2)
|
||||
if self.aircraft_cold_start:
|
||||
self.propose_flight(FlightType.OCA_AIRCRAFT, 2)
|
||||
self.propose_common_escorts()
|
||||
@@ -1,22 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater import MissionTarget
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanRefueling(PackagePlanningTask[MissionTarget]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if not super().preconditions_met(state):
|
||||
return False
|
||||
return self.target in state.refueling_targets
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.refueling_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.REFUELING, 1)
|
||||
@@ -1,14 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from game.commander.tasks.frontlinestancetask import FrontLineStanceTask
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
|
||||
class RetreatStance(FrontLineStanceTask):
|
||||
@property
|
||||
def stance(self) -> CombatStance:
|
||||
return CombatStance.RETREAT
|
||||
|
||||
@property
|
||||
def have_sufficient_front_line_advantage(self) -> bool:
|
||||
return True
|
||||
@@ -1,26 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from game.commander.tasks.packageplanningtask import PackagePlanningTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.theater.theatergroundobject import TheaterGroundObject
|
||||
from gen.flights.flight import FlightType
|
||||
|
||||
|
||||
@dataclass
|
||||
class PlanStrike(PackagePlanningTask[TheaterGroundObject[Any]]):
|
||||
def preconditions_met(self, state: TheaterState) -> bool:
|
||||
if self.target not in state.strike_targets:
|
||||
return False
|
||||
if not self.target_area_preconditions_met(state):
|
||||
return False
|
||||
return super().preconditions_met(state)
|
||||
|
||||
def apply_effects(self, state: TheaterState) -> None:
|
||||
state.strike_targets.remove(self.target)
|
||||
|
||||
def propose_flights(self) -> None:
|
||||
self.propose_flight(FlightType.STRIKE, 2)
|
||||
self.propose_common_escorts()
|
||||
@@ -1,16 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import PrimitiveTask
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.coalition import Coalition
|
||||
|
||||
|
||||
class TheaterCommanderTask(PrimitiveTask[TheaterState]):
|
||||
@abstractmethod
|
||||
def execute(self, coalition: Coalition) -> None:
|
||||
...
|
||||
@@ -1,88 +0,0 @@
|
||||
"""The Theater Commander is the highest level campaign AI.
|
||||
|
||||
Target selection is performed with a hierarchical-task-network (HTN, linked below).
|
||||
These work by giving the planner an initial "task" which decomposes into other tasks
|
||||
until a concrete set of actions is formed. For example, the "capture base" task may
|
||||
decompose in the following manner:
|
||||
|
||||
* Defend
|
||||
* Reinforce front line
|
||||
* Set front line stance to defend
|
||||
* Destroy enemy front line units
|
||||
* Set front line stance to elimination
|
||||
* Plan CAS at front line
|
||||
* Prepare
|
||||
* Destroy enemy IADS
|
||||
* Plan DEAD against SAM Armadillo
|
||||
* ...
|
||||
* Destroy enemy front line units
|
||||
* Set front line stance to elimination
|
||||
* Plan CAS at front line
|
||||
* Inhibit
|
||||
* Destroy enemy unit production infrastructure
|
||||
* Destroy factory at Palmyra
|
||||
* ...
|
||||
* Destroy enemy front line units
|
||||
* Set front line stance to elimination
|
||||
* Plan CAS at front line
|
||||
* Attack
|
||||
* Set front line stance to breakthrough
|
||||
* Destroy enemy front line units
|
||||
* Set front line stance to elimination
|
||||
* Plan CAS at front line
|
||||
|
||||
This is not a reflection of the actual task composition but illustrates the capability
|
||||
of the system. Each task has preconditions which are checked before the task is
|
||||
decomposed. If preconditions are not met the task is ignored and the next is considered.
|
||||
For example the task to destroy the factory at Palmyra might be excluded until the air
|
||||
defenses protecting it are eliminated; or defensive air operations might be excluded if
|
||||
the enemy does not have sufficient air forces, or if the protected target has sufficient
|
||||
SAM coverage.
|
||||
|
||||
Each action updates the world state, which causes each action to account for the result
|
||||
of the tasks executed before it. Above, the preconditions for attacking the factory at
|
||||
Palmyra may not have been met due to the IADS coverage, leading the planning to decide
|
||||
on an attack against the IADS in the area instead. When planning the next task in the
|
||||
same turn, the world state will have been updated to account for the (hopefully)
|
||||
destroyed SAM sites, allowing the planner to choose the mission to attack the factory.
|
||||
|
||||
Preconditions can be aware of previous actions as well. A precondition for "Plan CAS at
|
||||
front line" can be "No CAS missions planned at front line" to avoid over-planning CAS
|
||||
even though it is a primitive task used by many other tasks.
|
||||
|
||||
https://en.wikipedia.org/wiki/Hierarchical_task_network
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from game.commander.tasks.compound.nextaction import PlanNextAction
|
||||
from game.commander.tasks.theatercommandertask import TheaterCommanderTask
|
||||
from game.commander.theaterstate import TheaterState
|
||||
from game.htn import Planner
|
||||
from game.profiling import MultiEventTracer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
|
||||
class TheaterCommander(Planner[TheaterState, TheaterCommanderTask]):
|
||||
def __init__(self, game: Game, player: bool) -> None:
|
||||
super().__init__(
|
||||
PlanNextAction(
|
||||
aircraft_cold_start=game.settings.default_start_type == "Cold"
|
||||
)
|
||||
)
|
||||
self.game = game
|
||||
self.player = player
|
||||
|
||||
def plan_missions(self, tracer: MultiEventTracer) -> None:
|
||||
state = TheaterState.from_game(self.game, self.player, tracer)
|
||||
while True:
|
||||
result = self.plan(state)
|
||||
if result is None:
|
||||
# Planned all viable tasks this turn.
|
||||
return
|
||||
for task in result.tasks:
|
||||
task.execute(self.game.coalition_for(self.player))
|
||||
state = result.end_state
|
||||
@@ -1,176 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import itertools
|
||||
import math
|
||||
from collections import Iterator
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Any, Union, Optional
|
||||
|
||||
from game.commander.garrisons import Garrisons
|
||||
from game.commander.objectivefinder import ObjectiveFinder
|
||||
from game.htn import WorldState
|
||||
from game.profiling import MultiEventTracer
|
||||
from game.settings import Settings
|
||||
from game.squadrons import AirWing
|
||||
from game.theater import ControlPoint, FrontLine, MissionTarget, ConflictTheater
|
||||
from game.theater.theatergroundobject import (
|
||||
TheaterGroundObject,
|
||||
NavalGroundObject,
|
||||
IadsGroundObject,
|
||||
VehicleGroupGroundObject,
|
||||
BuildingGroundObject,
|
||||
)
|
||||
from game.threatzones import ThreatZones
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
from game.coalition import Coalition
|
||||
from game.transfers import Convoy, CargoShip
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PersistentContext:
|
||||
coalition: Coalition
|
||||
theater: ConflictTheater
|
||||
turn: int
|
||||
settings: Settings
|
||||
tracer: MultiEventTracer
|
||||
|
||||
|
||||
@dataclass
|
||||
class TheaterState(WorldState["TheaterState"]):
|
||||
context: PersistentContext
|
||||
barcaps_needed: dict[ControlPoint, int]
|
||||
active_front_lines: list[FrontLine]
|
||||
front_line_stances: dict[FrontLine, Optional[CombatStance]]
|
||||
vulnerable_front_lines: list[FrontLine]
|
||||
aewc_targets: list[MissionTarget]
|
||||
refueling_targets: list[MissionTarget]
|
||||
enemy_air_defenses: list[IadsGroundObject]
|
||||
threatening_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
|
||||
detecting_air_defenses: list[Union[IadsGroundObject, NavalGroundObject]]
|
||||
enemy_convoys: list[Convoy]
|
||||
enemy_shipping: list[CargoShip]
|
||||
enemy_ships: list[NavalGroundObject]
|
||||
enemy_garrisons: dict[ControlPoint, Garrisons]
|
||||
oca_targets: list[ControlPoint]
|
||||
strike_targets: list[TheaterGroundObject[Any]]
|
||||
enemy_barcaps: list[ControlPoint]
|
||||
threat_zones: ThreatZones
|
||||
|
||||
def _rebuild_threat_zones(self) -> None:
|
||||
"""Recreates the theater's threat zones based on the current planned state."""
|
||||
self.threat_zones = ThreatZones.for_threats(
|
||||
self.context.coalition.opponent.doctrine,
|
||||
barcap_locations=self.enemy_barcaps,
|
||||
air_defenses=itertools.chain(self.enemy_air_defenses, self.enemy_ships),
|
||||
)
|
||||
|
||||
def eliminate_air_defense(self, target: IadsGroundObject) -> None:
|
||||
if target in self.threatening_air_defenses:
|
||||
self.threatening_air_defenses.remove(target)
|
||||
if target in self.detecting_air_defenses:
|
||||
self.detecting_air_defenses.remove(target)
|
||||
self.enemy_air_defenses.remove(target)
|
||||
self._rebuild_threat_zones()
|
||||
|
||||
def eliminate_ship(self, target: NavalGroundObject) -> None:
|
||||
if target in self.threatening_air_defenses:
|
||||
self.threatening_air_defenses.remove(target)
|
||||
if target in self.detecting_air_defenses:
|
||||
self.detecting_air_defenses.remove(target)
|
||||
self.enemy_ships.remove(target)
|
||||
self._rebuild_threat_zones()
|
||||
|
||||
def has_garrison(self, target: VehicleGroupGroundObject) -> bool:
|
||||
return target in self.enemy_garrisons[target.control_point]
|
||||
|
||||
def eliminate_garrison(self, target: VehicleGroupGroundObject) -> None:
|
||||
self.enemy_garrisons[target.control_point].eliminate(target)
|
||||
|
||||
def ammo_dumps_at(
|
||||
self, control_point: ControlPoint
|
||||
) -> Iterator[BuildingGroundObject]:
|
||||
for target in self.strike_targets:
|
||||
if target.control_point != control_point:
|
||||
continue
|
||||
if target.is_ammo_depot:
|
||||
assert isinstance(target, BuildingGroundObject)
|
||||
yield target
|
||||
|
||||
def clone(self) -> TheaterState:
|
||||
# Do not use copy.deepcopy. Copying every TGO, control point, etc is absurdly
|
||||
# expensive.
|
||||
return TheaterState(
|
||||
context=self.context,
|
||||
barcaps_needed=dict(self.barcaps_needed),
|
||||
active_front_lines=list(self.active_front_lines),
|
||||
front_line_stances=dict(self.front_line_stances),
|
||||
vulnerable_front_lines=list(self.vulnerable_front_lines),
|
||||
aewc_targets=list(self.aewc_targets),
|
||||
refueling_targets=list(self.refueling_targets),
|
||||
enemy_air_defenses=list(self.enemy_air_defenses),
|
||||
enemy_convoys=list(self.enemy_convoys),
|
||||
enemy_shipping=list(self.enemy_shipping),
|
||||
enemy_ships=list(self.enemy_ships),
|
||||
enemy_garrisons={
|
||||
cp: dataclasses.replace(g) for cp, g in self.enemy_garrisons.items()
|
||||
},
|
||||
oca_targets=list(self.oca_targets),
|
||||
strike_targets=list(self.strike_targets),
|
||||
enemy_barcaps=list(self.enemy_barcaps),
|
||||
threat_zones=self.threat_zones,
|
||||
# Persistent properties are not copied. These are a way for failed subtasks
|
||||
# to communicate requirements to other tasks. For example, the task to
|
||||
# attack enemy garrisons might fail because the target area has IADS
|
||||
# protection. In that case, the preconditions of PlanBai would fail, but
|
||||
# would add the IADS that prevented it from being planned to the list of
|
||||
# IADS threats so that DegradeIads will consider it a threat later.
|
||||
threatening_air_defenses=self.threatening_air_defenses,
|
||||
detecting_air_defenses=self.detecting_air_defenses,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_game(
|
||||
cls, game: Game, player: bool, tracer: MultiEventTracer
|
||||
) -> TheaterState:
|
||||
coalition = game.coalition_for(player)
|
||||
finder = ObjectiveFinder(game, player)
|
||||
ordered_capturable_points = finder.prioritized_unisolated_points()
|
||||
|
||||
context = PersistentContext(
|
||||
coalition, game.theater, game.turn, game.settings, tracer
|
||||
)
|
||||
|
||||
# Plan enough rounds of CAP that the target has coverage over the expected
|
||||
# mission duration.
|
||||
mission_duration = game.settings.desired_player_mission_duration.total_seconds()
|
||||
barcap_duration = coalition.doctrine.cap_duration.total_seconds()
|
||||
barcap_rounds = math.ceil(mission_duration / barcap_duration)
|
||||
|
||||
return TheaterState(
|
||||
context=context,
|
||||
barcaps_needed={
|
||||
cp: barcap_rounds for cp in finder.vulnerable_control_points()
|
||||
},
|
||||
active_front_lines=list(finder.front_lines()),
|
||||
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()],
|
||||
enemy_air_defenses=list(finder.enemy_air_defenses()),
|
||||
threatening_air_defenses=[],
|
||||
detecting_air_defenses=[],
|
||||
enemy_convoys=list(finder.convoys()),
|
||||
enemy_shipping=list(finder.cargo_ships()),
|
||||
enemy_ships=list(finder.enemy_ships()),
|
||||
enemy_garrisons={
|
||||
cp: Garrisons.for_control_point(cp) for cp in ordered_capturable_points
|
||||
},
|
||||
oca_targets=list(finder.oca_targets(min_aircraft=20)),
|
||||
strike_targets=list(finder.strike_targets()),
|
||||
enemy_barcaps=list(game.theater.control_points_for(not player)),
|
||||
threat_zones=game.threat_zone_for(not player),
|
||||
)
|
||||
21
game/data/aaa_db.py
Normal file
21
game/data/aaa_db.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
AAA_UNITS = [
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.AAA_ZU_23_Closed,
|
||||
AirDefence.AAA_ZU_23_Emplacement,
|
||||
AirDefence.AAA_ZU_23_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent_Closed,
|
||||
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
|
||||
AirDefence.AAA_ZU_23_Insurgent,
|
||||
AirDefence.AAA_8_8cm_Flak_18,
|
||||
AirDefence.AAA_Flak_38,
|
||||
AirDefence.AAA_8_8cm_Flak_36,
|
||||
AirDefence.AAA_8_8cm_Flak_37,
|
||||
AirDefence.AAA_Flak_Vierling_38,
|
||||
AirDefence.AAA_Kdo_G_40,
|
||||
AirDefence.AAA_8_8cm_Flak_41,
|
||||
AirDefence.AAA_Bofors_40mm
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
from dcs.unit import Unit
|
||||
from dcs.vehicles import AirDefence
|
||||
|
||||
|
||||
class AlicCodes:
|
||||
CODES = {
|
||||
AirDefence._1L13_EWR.id: 101,
|
||||
AirDefence._55G6_EWR.id: 102,
|
||||
AirDefence.S_300PS_40B6MD_sr.id: 103,
|
||||
AirDefence.S_300PS_64H6E_sr.id: 104,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1.id: 107,
|
||||
AirDefence.Kub_1S91_str.id: 108,
|
||||
AirDefence.Dog_Ear_radar.id: 109,
|
||||
AirDefence.S_300PS_40B6M_tr.id: 110,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1.id: 115,
|
||||
AirDefence.Osa_9A33_ln.id: 117,
|
||||
AirDefence.Strela_10M3.id: 118,
|
||||
AirDefence.Tor_9A331.id: 119,
|
||||
AirDefence._2S6_Tunguska.id: 120,
|
||||
AirDefence.ZSU_23_4_Shilka.id: 121,
|
||||
AirDefence.P_19_s_125_sr.id: 122,
|
||||
AirDefence.Snr_s_125_tr.id: 123,
|
||||
AirDefence.Rapier_fsa_blindfire_radar.id: 124,
|
||||
AirDefence.Rapier_fsa_launcher.id: 125,
|
||||
AirDefence.SNR_75V.id: 126,
|
||||
AirDefence.HQ_7_LN_SP.id: 127,
|
||||
AirDefence.HQ_7_STR_SP.id: 128,
|
||||
AirDefence.RLS_19J6.id: 130,
|
||||
AirDefence.Roland_ADS.id: 201,
|
||||
AirDefence.Patriot_str.id: 202,
|
||||
AirDefence.Hawk_sr.id: 203,
|
||||
AirDefence.Hawk_tr.id: 204,
|
||||
AirDefence.Roland_Radar.id: 205,
|
||||
AirDefence.Hawk_cwar.id: 206,
|
||||
AirDefence.Gepard.id: 207,
|
||||
AirDefence.Vulcan.id: 208,
|
||||
AirDefence.NASAMS_Radar_MPQ64F1.id: 209,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def code_for(cls, unit: Unit) -> int:
|
||||
return cls.CODES[unit.type]
|
||||
@@ -1,63 +1,16 @@
|
||||
import inspect
|
||||
import dcs
|
||||
|
||||
DEFAULT_AVAILABLE_BUILDINGS = [
|
||||
"fuel",
|
||||
"comms",
|
||||
"oil",
|
||||
"ware",
|
||||
"farp",
|
||||
"power",
|
||||
"derrick",
|
||||
]
|
||||
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']
|
||||
|
||||
WW2_FREE = ["fuel", "ware"]
|
||||
WW2_GERMANY_BUILDINGS = [
|
||||
"fuel",
|
||||
"ww2bunker",
|
||||
"ww2bunker",
|
||||
"ww2bunker",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
]
|
||||
WW2_ALLIES_BUILDINGS = [
|
||||
"fuel",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
"allycamp",
|
||||
]
|
||||
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
|
||||
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
|
||||
|
||||
FORTIFICATION_BUILDINGS = [
|
||||
"Siegfried Line",
|
||||
"Concertina wire",
|
||||
"Concertina Wire",
|
||||
"Czech hedgehogs 1",
|
||||
"Czech hedgehogs 2",
|
||||
"Dragonteeth 1",
|
||||
"Dragonteeth 2",
|
||||
"Dragonteeth 3",
|
||||
"Dragonteeth 4",
|
||||
"Dragonteeth 5",
|
||||
"Haystack 1",
|
||||
"Haystack 2",
|
||||
"Haystack 3",
|
||||
"Haystack 4",
|
||||
"Hemmkurvenvenhindernis",
|
||||
"Log posts 1",
|
||||
"Log posts 2",
|
||||
"Log posts 3",
|
||||
"Log ramps 1",
|
||||
"Log ramps 2",
|
||||
"Log ramps 3",
|
||||
"Belgian Gate",
|
||||
"Container white",
|
||||
]
|
||||
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
|
||||
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',
|
||||
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis',
|
||||
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3',
|
||||
'Belgian Gate', 'Container white']
|
||||
|
||||
FORTIFICATION_UNITS = [
|
||||
c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
|
||||
]
|
||||
FORTIFICATION_UNITS_ID = [
|
||||
c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)
|
||||
]
|
||||
FORTIFICATION_UNITS = [c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
|
||||
FORTIFICATION_UNITS_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
|
||||
33
game/data/cap_capabilities_db.py
Normal file
33
game/data/cap_capabilities_db.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from dcs.planes import *
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
"""
|
||||
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
|
||||
They'll RTB when they don't have gun ammo left
|
||||
"""
|
||||
GUNFIGHTERS = [
|
||||
|
||||
# Cold War
|
||||
MiG_15bis,
|
||||
MiG_19P,
|
||||
MiG_21Bis,
|
||||
F_86F_Sabre,
|
||||
A_4E_C,
|
||||
F_5E_3,
|
||||
|
||||
# Trainers
|
||||
C_101CC,
|
||||
L_39ZA,
|
||||
|
||||
# WW2
|
||||
P_51D_30_NA,
|
||||
P_51D,
|
||||
P_47D_30,
|
||||
SpitfireLFMkIXCW,
|
||||
SpitfireLFMkIX,
|
||||
Bf_109K_4,
|
||||
FW_190D9,
|
||||
FW_190A8,
|
||||
I_16,
|
||||
|
||||
]
|
||||
@@ -1,189 +1,95 @@
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from game.utils import nm_to_meter, feet_to_meter
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.utils import Distance, feet, nautical_miles
|
||||
MODERN_DOCTRINE = {
|
||||
|
||||
"GENERATORS": {
|
||||
"CAS": True,
|
||||
"CAP": True,
|
||||
"SEAD": True,
|
||||
"STRIKE": True,
|
||||
"ANTISHIP": True,
|
||||
},
|
||||
|
||||
@dataclass
|
||||
class GroundUnitProcurementRatios:
|
||||
ratios: dict[GroundUnitClass, float]
|
||||
"STRIKE_MAX_RANGE": 1500000,
|
||||
"SEAD_MAX_RANGE": 1500000,
|
||||
|
||||
def for_unit_class(self, unit_class: GroundUnitClass) -> float:
|
||||
try:
|
||||
return self.ratios[unit_class] / sum(self.ratios.values())
|
||||
except KeyError:
|
||||
return 0.0
|
||||
"CAP_EVERY_X_MINUTES": 20,
|
||||
"CAS_EVERY_X_MINUTES": 30,
|
||||
"SEAD_EVERY_X_MINUTES": 40,
|
||||
"STRIKE_EVERY_X_MINUTES": 40,
|
||||
|
||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(45),
|
||||
"INGRESS_ALT": feet_to_meter(20000),
|
||||
"EGRESS_ALT": feet_to_meter(20000),
|
||||
"PATROL_ALT_RANGE": (feet_to_meter(15000), feet_to_meter(33000)),
|
||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Doctrine:
|
||||
cas: bool
|
||||
cap: bool
|
||||
sead: bool
|
||||
strike: bool
|
||||
antiship: bool
|
||||
"CAP_PATTERN_LENGTH": (nm_to_meter(15), nm_to_meter(40)),
|
||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(6), nm_to_meter(15)),
|
||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(10), nm_to_meter(40)),
|
||||
|
||||
rendezvous_altitude: Distance
|
||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
||||
}
|
||||
|
||||
#: The minimum distance between the departure airfield and the hold point.
|
||||
hold_distance: Distance
|
||||
COLDWAR_DOCTRINE = {
|
||||
|
||||
#: The minimum distance between the hold point and the join point.
|
||||
push_distance: Distance
|
||||
"GENERATORS": {
|
||||
"CAS": True,
|
||||
"CAP": True,
|
||||
"SEAD": True,
|
||||
"STRIKE": True,
|
||||
"ANTISHIP": True,
|
||||
},
|
||||
|
||||
#: The distance between the join point and the ingress point. Only used for the
|
||||
#: fallback flight plan layout (when the departure airfield is near a threat zone).
|
||||
join_distance: Distance
|
||||
"STRIKE_MAX_RANGE": 1500000,
|
||||
"SEAD_MAX_RANGE": 1500000,
|
||||
|
||||
#: The maximum distance between the ingress point (beginning of the attack) and
|
||||
#: target.
|
||||
max_ingress_distance: Distance
|
||||
"CAP_EVERY_X_MINUTES": 20,
|
||||
"CAS_EVERY_X_MINUTES": 30,
|
||||
"SEAD_EVERY_X_MINUTES": 40,
|
||||
"STRIKE_EVERY_X_MINUTES": 40,
|
||||
|
||||
#: The minimum distance between the ingress point (beginning of the attack) and
|
||||
#: target.
|
||||
min_ingress_distance: Distance
|
||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(30),
|
||||
"INGRESS_ALT": feet_to_meter(18000),
|
||||
"EGRESS_ALT": feet_to_meter(18000),
|
||||
"PATROL_ALT_RANGE": (feet_to_meter(10000), feet_to_meter(24000)),
|
||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
||||
|
||||
ingress_altitude: Distance
|
||||
"CAP_PATTERN_LENGTH": (nm_to_meter(12), nm_to_meter(24)),
|
||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(2), nm_to_meter(8)),
|
||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(8), nm_to_meter(25)),
|
||||
|
||||
min_patrol_altitude: Distance
|
||||
max_patrol_altitude: Distance
|
||||
pattern_altitude: Distance
|
||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
||||
}
|
||||
|
||||
#: The duration that CAP flights will remain on-station.
|
||||
cap_duration: timedelta
|
||||
WWII_DOCTRINE = {
|
||||
|
||||
#: The minimum length of the CAP race track.
|
||||
cap_min_track_length: Distance
|
||||
"GENERATORS": {
|
||||
"CAS": True,
|
||||
"CAP": True,
|
||||
"SEAD": False,
|
||||
"STRIKE": True,
|
||||
"ANTISHIP": True,
|
||||
},
|
||||
|
||||
#: The maximum length of the CAP race track.
|
||||
cap_max_track_length: Distance
|
||||
"STRIKE_MAX_RANGE": 1500000,
|
||||
"SEAD_MAX_RANGE": 1500000,
|
||||
|
||||
#: The minimum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_min_distance_from_cp: Distance
|
||||
"CAP_EVERY_X_MINUTES": 20,
|
||||
"CAS_EVERY_X_MINUTES": 30,
|
||||
"SEAD_EVERY_X_MINUTES": 40,
|
||||
"STRIKE_EVERY_X_MINUTES": 40,
|
||||
|
||||
#: The maximum distance between the defended position and the *end* of the
|
||||
#: CAP race track.
|
||||
cap_max_distance_from_cp: Distance
|
||||
"INGRESS_EGRESS_DISTANCE": nm_to_meter(7),
|
||||
"INGRESS_ALT": feet_to_meter(8000),
|
||||
"EGRESS_ALT": feet_to_meter(8000),
|
||||
"PATROL_ALT_RANGE": (feet_to_meter(4000), feet_to_meter(15000)),
|
||||
"PATTERN_ALTITUDE": feet_to_meter(5000),
|
||||
|
||||
#: The engagement range of CAP flights. Any enemy aircraft within this range
|
||||
#: of the CAP's current position will be engaged by the CAP.
|
||||
cap_engagement_range: Distance
|
||||
"CAP_PATTERN_LENGTH": (nm_to_meter(8), nm_to_meter(18)),
|
||||
"FRONTLINE_CAP_DISTANCE_FROM_FRONTLINE": (nm_to_meter(1), nm_to_meter(6)),
|
||||
"CAP_DISTANCE_FROM_CP": (nm_to_meter(0), nm_to_meter(5)),
|
||||
|
||||
cas_duration: timedelta
|
||||
"MAX_NUMBER_OF_INTERCEPTION_GROUP": 3,
|
||||
|
||||
sweep_distance: Distance
|
||||
|
||||
ground_unit_procurement_ratios: GroundUnitProcurementRatios
|
||||
|
||||
|
||||
MODERN_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
rendezvous_altitude=feet(25000),
|
||||
hold_distance=nautical_miles(25),
|
||||
push_distance=nautical_miles(20),
|
||||
join_distance=nautical_miles(20),
|
||||
max_ingress_distance=nautical_miles(45),
|
||||
min_ingress_distance=nautical_miles(10),
|
||||
ingress_altitude=feet(20000),
|
||||
min_patrol_altitude=feet(15000),
|
||||
max_patrol_altitude=feet(33000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nautical_miles(15),
|
||||
cap_max_track_length=nautical_miles(40),
|
||||
cap_min_distance_from_cp=nautical_miles(10),
|
||||
cap_max_distance_from_cp=nautical_miles(40),
|
||||
cap_engagement_range=nautical_miles(50),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(60),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 3,
|
||||
GroundUnitClass.Atgm: 2,
|
||||
GroundUnitClass.Apc: 2,
|
||||
GroundUnitClass.Ifv: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
COLDWAR_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=True,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
rendezvous_altitude=feet(22000),
|
||||
hold_distance=nautical_miles(15),
|
||||
push_distance=nautical_miles(10),
|
||||
join_distance=nautical_miles(10),
|
||||
max_ingress_distance=nautical_miles(30),
|
||||
min_ingress_distance=nautical_miles(10),
|
||||
ingress_altitude=feet(18000),
|
||||
min_patrol_altitude=feet(10000),
|
||||
max_patrol_altitude=feet(24000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nautical_miles(12),
|
||||
cap_max_track_length=nautical_miles(24),
|
||||
cap_min_distance_from_cp=nautical_miles(8),
|
||||
cap_max_distance_from_cp=nautical_miles(25),
|
||||
cap_engagement_range=nautical_miles(35),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(40),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 4,
|
||||
GroundUnitClass.Atgm: 2,
|
||||
GroundUnitClass.Apc: 3,
|
||||
GroundUnitClass.Ifv: 2,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 2,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
WWII_DOCTRINE = Doctrine(
|
||||
cap=True,
|
||||
cas=True,
|
||||
sead=False,
|
||||
strike=True,
|
||||
antiship=True,
|
||||
hold_distance=nautical_miles(10),
|
||||
push_distance=nautical_miles(5),
|
||||
join_distance=nautical_miles(5),
|
||||
rendezvous_altitude=feet(10000),
|
||||
max_ingress_distance=nautical_miles(7),
|
||||
min_ingress_distance=nautical_miles(5),
|
||||
ingress_altitude=feet(8000),
|
||||
min_patrol_altitude=feet(4000),
|
||||
max_patrol_altitude=feet(15000),
|
||||
pattern_altitude=feet(5000),
|
||||
cap_duration=timedelta(minutes=30),
|
||||
cap_min_track_length=nautical_miles(8),
|
||||
cap_max_track_length=nautical_miles(18),
|
||||
cap_min_distance_from_cp=nautical_miles(0),
|
||||
cap_max_distance_from_cp=nautical_miles(5),
|
||||
cap_engagement_range=nautical_miles(20),
|
||||
cas_duration=timedelta(minutes=30),
|
||||
sweep_distance=nautical_miles(10),
|
||||
ground_unit_procurement_ratios=GroundUnitProcurementRatios(
|
||||
{
|
||||
GroundUnitClass.Tank: 3,
|
||||
GroundUnitClass.Atgm: 3,
|
||||
GroundUnitClass.Apc: 3,
|
||||
GroundUnitClass.Artillery: 1,
|
||||
GroundUnitClass.Shorads: 3,
|
||||
GroundUnitClass.Recon: 1,
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import unique, Enum
|
||||
|
||||
|
||||
@unique
|
||||
class GroundUnitClass(Enum):
|
||||
Tank = "Tank"
|
||||
Atgm = "ATGM"
|
||||
Ifv = "IFV"
|
||||
Apc = "APC"
|
||||
Artillery = "Artillery"
|
||||
Logistics = "Logistics"
|
||||
Recon = "Recon"
|
||||
Infantry = "Infantry"
|
||||
Shorads = "SHORADS"
|
||||
Manpads = "MANPADS"
|
||||
@@ -1,117 +1,53 @@
|
||||
from dcs.ships import (
|
||||
Forrestal,
|
||||
PIOTR,
|
||||
MOSCOW,
|
||||
VINSON,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
Stennis,
|
||||
KUZNECOW,
|
||||
CV_1143_5,
|
||||
NEUSTRASH,
|
||||
ALBATROS,
|
||||
REZKY,
|
||||
MOLNIYA,
|
||||
LHA_Tarawa,
|
||||
PERRY,
|
||||
TICONDEROG,
|
||||
Type_052B,
|
||||
Type_052C,
|
||||
Type_054A,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
)
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.ships import *
|
||||
|
||||
TELARS = {
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.Roland_ADS,
|
||||
}
|
||||
UNITS_WITH_RADAR = [
|
||||
|
||||
TRACK_RADARS = {
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.RPC_5N62V,
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.NASAMS_Radar_MPQ64F1,
|
||||
}
|
||||
|
||||
LAUNCHER_TRACKER_PAIRS = {
|
||||
AirDefence.Kub_2P25_ln: AirDefence.Kub_1S91_str,
|
||||
AirDefence._5p73_s_125_ln: AirDefence.Snr_s_125_tr,
|
||||
AirDefence.S_300PS_5P85C_ln: AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.S_300PS_5P85D_ln: AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.Hawk_ln: AirDefence.Hawk_tr,
|
||||
AirDefence.Patriot_ln: AirDefence.Patriot_str,
|
||||
AirDefence.S_75M_Volhov: AirDefence.SNR_75V,
|
||||
AirDefence.Rapier_fsa_launcher: AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_LN_SP: AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.S_200_Launcher: AirDefence.RPC_5N62V,
|
||||
AirDefence.NASAMS_LN_B: AirDefence.NASAMS_Radar_MPQ64F1,
|
||||
AirDefence.NASAMS_LN_C: AirDefence.NASAMS_Radar_MPQ64F1,
|
||||
}
|
||||
|
||||
UNITS_WITH_RADAR = {
|
||||
# Radars
|
||||
AirDefence._2S6_Tunguska,
|
||||
AirDefence.SA_11_Buk_LN_9A310M1,
|
||||
AirDefence.Osa_9A33_ln,
|
||||
AirDefence.Tor_9A331,
|
||||
AirDefence.Gepard,
|
||||
AirDefence.Vulcan,
|
||||
AirDefence.Roland_ADS,
|
||||
AirDefence.ZSU_23_4_Shilka,
|
||||
AirDefence._1L13_EWR,
|
||||
AirDefence.Kub_1S91_str,
|
||||
AirDefence.S_300PS_40B6M_tr,
|
||||
AirDefence.S_300PS_40B6MD_sr,
|
||||
AirDefence._55G6_EWR,
|
||||
AirDefence.S_300PS_64H6E_sr,
|
||||
AirDefence.SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.Dog_Ear_radar,
|
||||
AirDefence.Hawk_tr,
|
||||
AirDefence.Hawk_sr,
|
||||
AirDefence.Patriot_str,
|
||||
AirDefence.Hawk_cwar,
|
||||
AirDefence.P_19_s_125_sr,
|
||||
AirDefence.Roland_Radar,
|
||||
AirDefence.Snr_s_125_tr,
|
||||
AirDefence.SNR_75V,
|
||||
AirDefence.RLS_19J6,
|
||||
AirDefence.RPC_5N62V,
|
||||
AirDefence.Rapier_fsa_blindfire_radar,
|
||||
AirDefence.HQ_7_LN_SP,
|
||||
AirDefence.HQ_7_STR_SP,
|
||||
AirDefence.FuMG_401,
|
||||
AirDefence.FuSe_65,
|
||||
AirDefence.SAM_SA_15_Tor_9A331,
|
||||
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
|
||||
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
|
||||
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
|
||||
AirDefence.SPAAA_Gepard,
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.EWR_1L13,
|
||||
AirDefence.SAM_SA_6_Kub_STR_9S91,
|
||||
AirDefence.SAM_SA_10_S_300PS_TR_30N6,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
|
||||
AirDefence.EWR_55G6,
|
||||
AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
|
||||
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
|
||||
AirDefence.CP_9S80M1_Sborka,
|
||||
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
|
||||
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
|
||||
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
|
||||
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
|
||||
AirDefence.SAM_SR_P_19,
|
||||
AirDefence.SAM_Roland_EWR,
|
||||
AirDefence.SAM_SA_3_S_125_TR_SNR,
|
||||
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
|
||||
AirDefence.HQ_7_Self_Propelled_STR,
|
||||
|
||||
# Ships
|
||||
ALBATROS,
|
||||
CVN_71,
|
||||
CVN_72,
|
||||
CVN_73,
|
||||
CV_1143_5,
|
||||
Forrestal,
|
||||
KUZNECOW,
|
||||
LHA_Tarawa,
|
||||
MOLNIYA,
|
||||
MOSCOW,
|
||||
NEUSTRASH,
|
||||
PERRY,
|
||||
PIOTR,
|
||||
REZKY,
|
||||
Stennis,
|
||||
TICONDEROG,
|
||||
Type_052B,
|
||||
Type_052C,
|
||||
Type_054A,
|
||||
CVN_70_Carl_Vinson,
|
||||
Oliver_Hazzard_Perry_class,
|
||||
Ticonderoga_class,
|
||||
FFL_1124_4_Grisha,
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
FSG_1241_1MP_Molniya,
|
||||
CG_1164_Moskva,
|
||||
FFG_11540_Neustrashimy,
|
||||
CGN_1144_2_Pyotr_Velikiy,
|
||||
FF_1135M_Rezky,
|
||||
CV_1143_5_Admiral_Kuznetsov_2017,
|
||||
CVN_74_John_C__Stennis,
|
||||
CVN_71_Theodore_Roosevelt,
|
||||
CVN_72_Abraham_Lincoln,
|
||||
CVN_73_George_Washington,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
VINSON,
|
||||
}
|
||||
LHA_1_Tarawa,
|
||||
Type_052B_Destroyer,
|
||||
Type_054A_Frigate,
|
||||
Type_052C_Destroyer
|
||||
]
|
||||
@@ -1,281 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import inspect
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from enum import unique, Enum
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import Iterator, Optional, Any, ClassVar
|
||||
|
||||
import yaml
|
||||
from dcs.unitgroup import FlyingGroup
|
||||
from dcs.weapons_data import weapon_ids
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
|
||||
PydcsWeapon = Any
|
||||
PydcsWeaponAssignment = tuple[int, PydcsWeapon]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Weapon:
|
||||
"""Wrapper for DCS weapons."""
|
||||
|
||||
#: The CLSID used by DCS.
|
||||
clsid: str
|
||||
|
||||
#: The group this weapon belongs to.
|
||||
weapon_group: WeaponGroup = field(compare=False)
|
||||
|
||||
_by_clsid: ClassVar[dict[str, Weapon]] = {}
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def pydcs_data(self) -> PydcsWeapon:
|
||||
if self.clsid == "<CLEAN>":
|
||||
# Special case for a "weapon" that isn't exposed by pydcs.
|
||||
return {
|
||||
"clsid": self.clsid,
|
||||
"name": "Clean",
|
||||
"weight": 0,
|
||||
}
|
||||
return weapon_ids[self.clsid]
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.pydcs_data["name"]
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
try:
|
||||
updated = Weapon.with_clsid(state["clsid"])
|
||||
state.update(updated.__dict__)
|
||||
except KeyError:
|
||||
logging.exception(
|
||||
f'CLSID {state["clsid"]} is not available anymore. This could potentially lead to some unexpected results when generating the next turn miz. To solve this issue it is recommended to update the loadout of all affected flights.'
|
||||
)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@classmethod
|
||||
def register(cls, weapon: Weapon) -> None:
|
||||
if weapon.clsid in cls._by_clsid:
|
||||
duplicate = cls._by_clsid[weapon.clsid]
|
||||
raise ValueError(
|
||||
"Weapon CLSID used in more than one weapon type: "
|
||||
f"{duplicate.name} and {weapon.name}: {weapon.clsid}"
|
||||
)
|
||||
cls._by_clsid[weapon.clsid] = weapon
|
||||
|
||||
@classmethod
|
||||
def with_clsid(cls, clsid: str) -> Weapon:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
return cls._by_clsid[clsid]
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
WeaponGroup.load_all()
|
||||
cls._loaded = True
|
||||
|
||||
def available_on(self, date: datetime.date) -> bool:
|
||||
introduction_year = self.weapon_group.introduction_year
|
||||
if introduction_year is None:
|
||||
return True
|
||||
return date >= datetime.date(introduction_year, 1, 1)
|
||||
|
||||
@property
|
||||
def fallbacks(self) -> Iterator[Weapon]:
|
||||
yield self
|
||||
fallback: Optional[WeaponGroup] = self.weapon_group
|
||||
while fallback is not None:
|
||||
yield from fallback.weapons
|
||||
fallback = fallback.fallback
|
||||
|
||||
|
||||
@unique
|
||||
class WeaponType(Enum):
|
||||
LGB = "LGB"
|
||||
TGP = "TGP"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class WeaponGroup:
|
||||
"""Group of "identical" weapons loaded from resources/weapons.
|
||||
|
||||
DCS has multiple unique "weapons" for each type of weapon. There are four distinct
|
||||
class IDs for the AIM-7M, some unique to certain aircraft. We group them in the
|
||||
resources to make year/fallback data easier to track.
|
||||
"""
|
||||
|
||||
#: The name of the weapon group in the resource file.
|
||||
name: str
|
||||
|
||||
#: The type of the weapon group.
|
||||
type: WeaponType = field(compare=False)
|
||||
|
||||
#: The year of introduction.
|
||||
introduction_year: Optional[int] = field(compare=False)
|
||||
|
||||
#: The name of the fallback weapon group.
|
||||
fallback_name: Optional[str] = field(compare=False)
|
||||
|
||||
#: The specific weapons that belong to this weapon group.
|
||||
weapons: list[Weapon] = field(init=False, default_factory=list)
|
||||
|
||||
_by_name: ClassVar[dict[str, WeaponGroup]] = {}
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def fallback(self) -> Optional[WeaponGroup]:
|
||||
if self.fallback_name is None:
|
||||
return None
|
||||
return WeaponGroup.named(self.fallback_name)
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
updated = WeaponGroup.named(state["name"])
|
||||
state.update(updated.__dict__)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@classmethod
|
||||
def register(cls, group: WeaponGroup) -> None:
|
||||
if group.name in cls._by_name:
|
||||
duplicate = cls._by_name[group.name]
|
||||
raise ValueError(
|
||||
"Weapon group name used in more than one weapon type: "
|
||||
f"{duplicate.name} and {group.name}"
|
||||
)
|
||||
cls._by_name[group.name] = group
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> WeaponGroup:
|
||||
if not cls._loaded:
|
||||
cls.load_all()
|
||||
return cls._by_name[name]
|
||||
|
||||
@classmethod
|
||||
def _each_weapon_group(cls) -> Iterator[WeaponGroup]:
|
||||
for group_file_path in Path("resources/weapons").glob("**/*.yaml"):
|
||||
with group_file_path.open(encoding="utf8") as group_file:
|
||||
data = yaml.safe_load(group_file)
|
||||
name = data["name"]
|
||||
try:
|
||||
weapon_type = WeaponType(data["type"])
|
||||
except KeyError:
|
||||
weapon_type = WeaponType.UNKNOWN
|
||||
year = data.get("year")
|
||||
fallback_name = data.get("fallback")
|
||||
group = WeaponGroup(name, weapon_type, year, fallback_name)
|
||||
for clsid in data["clsids"]:
|
||||
weapon = Weapon(clsid, group)
|
||||
Weapon.register(weapon)
|
||||
group.weapons.append(weapon)
|
||||
yield group
|
||||
|
||||
@classmethod
|
||||
def register_clean_pylon(cls) -> None:
|
||||
group = WeaponGroup(
|
||||
"Clean pylon",
|
||||
type=WeaponType.UNKNOWN,
|
||||
introduction_year=None,
|
||||
fallback_name=None,
|
||||
)
|
||||
cls.register(group)
|
||||
weapon = Weapon("<CLEAN>", group)
|
||||
Weapon.register(weapon)
|
||||
group.weapons.append(weapon)
|
||||
|
||||
@classmethod
|
||||
def register_unknown_weapons(cls, seen_clsids: set[str]) -> None:
|
||||
unknown_weapons = set(weapon_ids.keys()) - seen_clsids
|
||||
group = WeaponGroup(
|
||||
"Unknown",
|
||||
type=WeaponType.UNKNOWN,
|
||||
introduction_year=None,
|
||||
fallback_name=None,
|
||||
)
|
||||
cls.register(group)
|
||||
for clsid in unknown_weapons:
|
||||
weapon = Weapon(clsid, group)
|
||||
Weapon.register(weapon)
|
||||
group.weapons.append(weapon)
|
||||
|
||||
@classmethod
|
||||
def load_all(cls) -> None:
|
||||
if cls._loaded:
|
||||
return
|
||||
seen_clsids: set[str] = set()
|
||||
for group in cls._each_weapon_group():
|
||||
cls.register(group)
|
||||
seen_clsids.update(w.clsid for w in group.weapons)
|
||||
cls.register_clean_pylon()
|
||||
cls.register_unknown_weapons(seen_clsids)
|
||||
cls._loaded = True
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Pylon:
|
||||
number: int
|
||||
allowed: set[Weapon]
|
||||
|
||||
def can_equip(self, weapon: Weapon) -> bool:
|
||||
# TODO: Fix pydcs to support the <CLEAN> "weapon".
|
||||
# <CLEAN> is a special case because pydcs doesn't know about that "weapon", so
|
||||
# it's not compatible with *any* pylon. Just trust the loadout and try to equip
|
||||
# it.
|
||||
#
|
||||
# A similar hack exists in QPylonEditor to forcibly add "Clean" to the list of
|
||||
# valid configurations for that pylon if a loadout has been seen with that
|
||||
# configuration.
|
||||
return weapon in self.allowed or weapon.clsid == "<CLEAN>"
|
||||
|
||||
def equip(self, group: FlyingGroup[Any], weapon: Weapon) -> None:
|
||||
if not self.can_equip(weapon):
|
||||
logging.error(f"Pylon {self.number} cannot equip {weapon.name}")
|
||||
group.load_pylon(self.make_pydcs_assignment(weapon), self.number)
|
||||
|
||||
def make_pydcs_assignment(self, weapon: Weapon) -> PydcsWeaponAssignment:
|
||||
return self.number, weapon.pydcs_data
|
||||
|
||||
def available_on(self, date: datetime.date) -> Iterator[Weapon]:
|
||||
for weapon in self.allowed:
|
||||
if weapon.available_on(date):
|
||||
yield weapon
|
||||
|
||||
@classmethod
|
||||
def for_aircraft(cls, aircraft: AircraftType, number: int) -> Pylon:
|
||||
# In pydcs these are all arbitrary inner classes of the aircraft type.
|
||||
# The only way to identify them is by their name.
|
||||
pylons = [
|
||||
v
|
||||
for v in aircraft.dcs_unit_type.__dict__.values()
|
||||
if inspect.isclass(v) and v.__name__.startswith("Pylon")
|
||||
]
|
||||
|
||||
# And that Pylon class has members with irrelevant names that have
|
||||
# values of (pylon number, allowed weapon).
|
||||
allowed = set()
|
||||
for pylon in pylons:
|
||||
for key, value in pylon.__dict__.items():
|
||||
if key.startswith("__"):
|
||||
continue
|
||||
pylon_number, weapon = value
|
||||
if pylon_number != number:
|
||||
continue
|
||||
allowed.add(Weapon.with_clsid(weapon["clsid"]))
|
||||
|
||||
return cls(number, allowed)
|
||||
|
||||
@classmethod
|
||||
def iter_pylons(cls, aircraft: AircraftType) -> Iterator[Pylon]:
|
||||
for pylon in sorted(list(aircraft.dcs_unit_type.pylons)):
|
||||
yield cls.for_aircraft(aircraft, pylon)
|
||||
1407
game/db.py
1407
game/db.py
File diff suppressed because it is too large
Load Diff
@@ -1,396 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from pathlib import Path
|
||||
from typing import ClassVar, Type, Iterator, TYPE_CHECKING, Optional, Any
|
||||
|
||||
import yaml
|
||||
from dcs.helicopters import helicopter_map
|
||||
from dcs.planes import plane_map
|
||||
from dcs.unittype import FlyingType
|
||||
|
||||
from game.dcs.unittype import UnitType
|
||||
from game.radio.channels import (
|
||||
ApacheChannelNamer,
|
||||
ChannelNamer,
|
||||
RadioChannelAllocator,
|
||||
CommonRadioChannelAllocator,
|
||||
HueyChannelNamer,
|
||||
SCR522ChannelNamer,
|
||||
ViggenChannelNamer,
|
||||
ViperChannelNamer,
|
||||
TomcatChannelNamer,
|
||||
MirageChannelNamer,
|
||||
SingleRadioChannelNamer,
|
||||
FarmerRadioChannelAllocator,
|
||||
SCR522RadioChannelAllocator,
|
||||
ViggenRadioChannelAllocator,
|
||||
NoOpChannelAllocator,
|
||||
)
|
||||
from game.utils import (
|
||||
Distance,
|
||||
SPEED_OF_SOUND_AT_SEA_LEVEL,
|
||||
Speed,
|
||||
feet,
|
||||
kph,
|
||||
knots,
|
||||
nautical_miles,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gen.aircraft import FlightData
|
||||
from gen.airsupport import AirSupport
|
||||
from gen.radios import Radio, RadioFrequency, RadioRegistry
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RadioConfig:
|
||||
inter_flight: Optional[Radio]
|
||||
intra_flight: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> RadioConfig:
|
||||
return RadioConfig(
|
||||
cls.make_radio(data.get("inter_flight", None)),
|
||||
cls.make_radio(data.get("intra_flight", None)),
|
||||
cls.make_allocator(data.get("channels", {})),
|
||||
cls.make_namer(data.get("channels", {})),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_radio(cls, name: Optional[str]) -> Optional[Radio]:
|
||||
from gen.radios import get_radio
|
||||
|
||||
if name is None:
|
||||
return None
|
||||
return get_radio(name)
|
||||
|
||||
@classmethod
|
||||
def make_allocator(cls, data: dict[str, Any]) -> Optional[RadioChannelAllocator]:
|
||||
try:
|
||||
alloc_type = data["type"]
|
||||
except KeyError:
|
||||
return None
|
||||
allocator_type: Type[RadioChannelAllocator] = {
|
||||
"SCR-522": SCR522RadioChannelAllocator,
|
||||
"common": CommonRadioChannelAllocator,
|
||||
"farmer": FarmerRadioChannelAllocator,
|
||||
"noop": NoOpChannelAllocator,
|
||||
"viggen": ViggenRadioChannelAllocator,
|
||||
}[alloc_type]
|
||||
return allocator_type.from_cfg(data)
|
||||
|
||||
@classmethod
|
||||
def make_namer(cls, config: dict[str, Any]) -> Type[ChannelNamer]:
|
||||
return {
|
||||
"SCR-522": SCR522ChannelNamer,
|
||||
"default": ChannelNamer,
|
||||
"huey": HueyChannelNamer,
|
||||
"mirage": MirageChannelNamer,
|
||||
"single": SingleRadioChannelNamer,
|
||||
"tomcat": TomcatChannelNamer,
|
||||
"viggen": ViggenChannelNamer,
|
||||
"viper": ViperChannelNamer,
|
||||
"apache": ApacheChannelNamer,
|
||||
}[config.get("namer", "default")]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PatrolConfig:
|
||||
altitude: Optional[Distance]
|
||||
speed: Optional[Speed]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> PatrolConfig:
|
||||
altitude = data.get("altitude", None)
|
||||
speed = data.get("speed", None)
|
||||
return PatrolConfig(
|
||||
feet(altitude) if altitude is not None else None,
|
||||
knots(speed) if speed is not None else None,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FuelConsumption:
|
||||
#: The estimated taxi fuel requirement, in pounds.
|
||||
taxi: int
|
||||
|
||||
#: The estimated fuel consumption for a takeoff climb, in pounds per nautical mile.
|
||||
climb: float
|
||||
|
||||
#: The estimated fuel consumption for cruising, in pounds per nautical mile.
|
||||
cruise: float
|
||||
|
||||
#: The estimated fuel consumption for combat speeds, in pounds per nautical mile.
|
||||
combat: float
|
||||
|
||||
#: The minimum amount of fuel that the aircraft should land with, in pounds. This is
|
||||
#: a reserve amount for landing delays or emergencies.
|
||||
min_safe: int
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: dict[str, Any]) -> FuelConsumption:
|
||||
return FuelConsumption(
|
||||
int(data["taxi"]),
|
||||
float(data["climb_ppm"]),
|
||||
float(data["cruise_ppm"]),
|
||||
float(data["combat_ppm"]),
|
||||
int(data["min_safe"]),
|
||||
)
|
||||
|
||||
|
||||
# TODO: Split into PlaneType and HelicopterType?
|
||||
@dataclass(frozen=True)
|
||||
class AircraftType(UnitType[Type[FlyingType]]):
|
||||
carrier_capable: bool
|
||||
lha_capable: bool
|
||||
always_keeps_gun: bool
|
||||
|
||||
# If true, the aircraft does not use the guns as the last resort weapons, but as a
|
||||
# main weapon. It'll RTB when it doesn't have gun ammo left.
|
||||
gunfighter: bool
|
||||
|
||||
max_group_size: int
|
||||
patrol_altitude: Optional[Distance]
|
||||
patrol_speed: Optional[Speed]
|
||||
|
||||
#: The maximum range between the origin airfield and the target for which the auto-
|
||||
#: planner will consider this aircraft usable for a mission.
|
||||
max_mission_range: Distance
|
||||
|
||||
fuel_consumption: Optional[FuelConsumption]
|
||||
|
||||
intra_flight_radio: Optional[Radio]
|
||||
channel_allocator: Optional[RadioChannelAllocator]
|
||||
channel_namer: Type[ChannelNamer]
|
||||
|
||||
_by_name: ClassVar[dict[str, AircraftType]] = {}
|
||||
_by_unit_type: ClassVar[dict[Type[FlyingType], list[AircraftType]]] = defaultdict(
|
||||
list
|
||||
)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def dcs_id(self) -> str:
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@property
|
||||
def flyable(self) -> bool:
|
||||
return self.dcs_unit_type.flyable
|
||||
|
||||
@property
|
||||
def helicopter(self) -> bool:
|
||||
return self.dcs_unit_type.helicopter
|
||||
|
||||
@cached_property
|
||||
def max_speed(self) -> Speed:
|
||||
return kph(self.dcs_unit_type.max_speed)
|
||||
|
||||
@property
|
||||
def preferred_patrol_altitude(self) -> Distance:
|
||||
if self.patrol_altitude is not None:
|
||||
return self.patrol_altitude
|
||||
else:
|
||||
# Estimate based on max speed.
|
||||
# Aircaft with max speed 600 kph will prefer patrol at 10 000 ft
|
||||
# Aircraft with max speed 2800 kph will prefer pratrol at 33 000 ft
|
||||
altitude_for_lowest_speed = feet(10 * 1000)
|
||||
altitude_for_highest_speed = feet(33 * 1000)
|
||||
lowest_speed = kph(600)
|
||||
highest_speed = kph(2800)
|
||||
factor = (self.max_speed - lowest_speed).kph / (
|
||||
highest_speed - lowest_speed
|
||||
).kph
|
||||
altitude = (
|
||||
altitude_for_lowest_speed
|
||||
+ (altitude_for_highest_speed - altitude_for_lowest_speed) * factor
|
||||
)
|
||||
logging.debug(
|
||||
f"Preferred patrol altitude for {self.dcs_unit_type.id}: {altitude.feet}"
|
||||
)
|
||||
rounded_altitude = feet(round(1000 * round(altitude.feet / 1000)))
|
||||
return max(
|
||||
altitude_for_lowest_speed,
|
||||
min(altitude_for_highest_speed, rounded_altitude),
|
||||
)
|
||||
|
||||
def preferred_patrol_speed(self, altitude: Distance) -> Speed:
|
||||
"""Preferred true airspeed when patrolling"""
|
||||
if self.patrol_speed is not None:
|
||||
return self.patrol_speed
|
||||
else:
|
||||
# Estimate based on max speed.
|
||||
max_speed = self.max_speed
|
||||
if max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.6:
|
||||
# Fast airplanes, should manage pretty high patrol speed
|
||||
return (
|
||||
Speed.from_mach(0.85, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.7, altitude)
|
||||
)
|
||||
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 1.2:
|
||||
# Medium-fast like F/A-18C
|
||||
return (
|
||||
Speed.from_mach(0.8, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.65, altitude)
|
||||
)
|
||||
elif max_speed > SPEED_OF_SOUND_AT_SEA_LEVEL * 0.7:
|
||||
# Semi-fast like airliners or similar
|
||||
return (
|
||||
Speed.from_mach(0.5, altitude)
|
||||
if altitude.feet > 20000
|
||||
else Speed.from_mach(0.4, altitude)
|
||||
)
|
||||
else:
|
||||
# Slow like warbirds or helicopters
|
||||
# Use whichever is slowest - mach 0.35 or 70% of max speed
|
||||
logging.debug(f"{self.name} max_speed * 0.7 is {max_speed * 0.7}")
|
||||
return min(Speed.from_mach(0.35, altitude), max_speed * 0.7)
|
||||
|
||||
def alloc_flight_radio(self, radio_registry: RadioRegistry) -> RadioFrequency:
|
||||
from gen.radios import ChannelInUseError, kHz
|
||||
|
||||
if self.intra_flight_radio is not None:
|
||||
return radio_registry.alloc_for_radio(self.intra_flight_radio)
|
||||
|
||||
# The default radio frequency is set in megahertz. For some aircraft, it is a
|
||||
# floating point value. For all current aircraft, adjusting to kilohertz will be
|
||||
# sufficient to convert to an integer.
|
||||
in_khz = float(self.dcs_unit_type.radio_frequency) * 1000
|
||||
if not in_khz.is_integer():
|
||||
logging.warning(
|
||||
f"Found unexpected sub-kHz default radio for {self}: {in_khz} kHz. "
|
||||
"Truncating to integer. The truncated frequency may not be valid for "
|
||||
"the aircraft."
|
||||
)
|
||||
|
||||
freq = kHz(int(in_khz))
|
||||
try:
|
||||
radio_registry.reserve(freq)
|
||||
except ChannelInUseError:
|
||||
pass
|
||||
return freq
|
||||
|
||||
def assign_channels_for_flight(
|
||||
self, flight: FlightData, air_support: AirSupport
|
||||
) -> None:
|
||||
if self.channel_allocator is not None:
|
||||
self.channel_allocator.assign_channels_for_flight(flight, air_support)
|
||||
|
||||
def channel_name(self, radio_id: int, channel_id: int) -> str:
|
||||
return self.channel_namer.channel_name(radio_id, channel_id)
|
||||
|
||||
def __setstate__(self, state: dict[str, Any]) -> None:
|
||||
# Update any existing models with new data on load.
|
||||
updated = AircraftType.named(state["name"])
|
||||
state.update(updated.__dict__)
|
||||
self.__dict__.update(state)
|
||||
|
||||
@classmethod
|
||||
def register(cls, aircraft_type: AircraftType) -> None:
|
||||
cls._by_name[aircraft_type.name] = aircraft_type
|
||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> AircraftType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
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()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def _each_unit_type() -> Iterator[Type[FlyingType]]:
|
||||
yield from helicopter_map.values()
|
||||
yield from plane_map.values()
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for unit_type in cls._each_unit_type():
|
||||
for data in cls._each_variant_of(unit_type):
|
||||
cls.register(data)
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, aircraft: Type[FlyingType]) -> Iterator[AircraftType]:
|
||||
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")
|
||||
return
|
||||
|
||||
with data_path.open(encoding="utf-8") as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
price = data["price"]
|
||||
except KeyError as ex:
|
||||
raise KeyError(f"Missing required price field: {data_path}") from ex
|
||||
|
||||
radio_config = RadioConfig.from_data(data.get("radios", {}))
|
||||
patrol_config = PatrolConfig.from_data(data.get("patrol", {}))
|
||||
|
||||
try:
|
||||
mission_range = nautical_miles(int(data["max_range"]))
|
||||
except (KeyError, ValueError):
|
||||
mission_range = (
|
||||
nautical_miles(50) if aircraft.helicopter else nautical_miles(150)
|
||||
)
|
||||
logging.warning(
|
||||
f"{aircraft.id} does not specify a max_range. Defaulting to "
|
||||
f"{mission_range.nautical_miles}NM"
|
||||
)
|
||||
|
||||
fuel_data = data.get("fuel")
|
||||
if fuel_data is not None:
|
||||
fuel_consumption: Optional[FuelConsumption] = FuelConsumption.from_data(
|
||||
fuel_data
|
||||
)
|
||||
else:
|
||||
fuel_consumption = None
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
if introduction is None:
|
||||
introduction = "N/A"
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
for variant in data.get("variants", [aircraft.id]):
|
||||
yield AircraftType(
|
||||
dcs_unit_type=aircraft,
|
||||
name=variant,
|
||||
description=data.get(
|
||||
"description",
|
||||
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
|
||||
),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=price,
|
||||
carrier_capable=data.get("carrier_capable", False),
|
||||
lha_capable=data.get("lha_capable", False),
|
||||
always_keeps_gun=data.get("always_keeps_gun", False),
|
||||
gunfighter=data.get("gunfighter", False),
|
||||
max_group_size=data.get("max_group_size", aircraft.group_size_max),
|
||||
patrol_altitude=patrol_config.altitude,
|
||||
patrol_speed=patrol_config.speed,
|
||||
max_mission_range=mission_range,
|
||||
fuel_consumption=fuel_consumption,
|
||||
intra_flight_radio=radio_config.intra_flight,
|
||||
channel_allocator=radio_config.channel_allocator,
|
||||
channel_namer=radio_config.channel_namer,
|
||||
)
|
||||
@@ -1,100 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Type, Optional, ClassVar, Iterator
|
||||
|
||||
import yaml
|
||||
from dcs.unittype import VehicleType
|
||||
from dcs.vehicles import vehicle_map
|
||||
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.unittype import UnitType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GroundUnitType(UnitType[Type[VehicleType]]):
|
||||
unit_class: Optional[GroundUnitClass]
|
||||
spawn_weight: int
|
||||
|
||||
_by_name: ClassVar[dict[str, GroundUnitType]] = {}
|
||||
_by_unit_type: ClassVar[
|
||||
dict[Type[VehicleType], list[GroundUnitType]]
|
||||
] = defaultdict(list)
|
||||
_loaded: ClassVar[bool] = False
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def dcs_id(self) -> str:
|
||||
return self.dcs_unit_type.id
|
||||
|
||||
@classmethod
|
||||
def register(cls, aircraft_type: GroundUnitType) -> None:
|
||||
cls._by_name[aircraft_type.name] = aircraft_type
|
||||
cls._by_unit_type[aircraft_type.dcs_unit_type].append(aircraft_type)
|
||||
|
||||
@classmethod
|
||||
def named(cls, name: str) -> GroundUnitType:
|
||||
if not cls._loaded:
|
||||
cls._load_all()
|
||||
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()
|
||||
yield from cls._by_unit_type[dcs_unit_type]
|
||||
|
||||
@staticmethod
|
||||
def _each_unit_type() -> Iterator[Type[VehicleType]]:
|
||||
yield from vehicle_map.values()
|
||||
|
||||
@classmethod
|
||||
def _load_all(cls) -> None:
|
||||
for unit_type in cls._each_unit_type():
|
||||
for data in cls._each_variant_of(unit_type):
|
||||
cls.register(data)
|
||||
cls._loaded = True
|
||||
|
||||
@classmethod
|
||||
def _each_variant_of(cls, vehicle: Type[VehicleType]) -> Iterator[GroundUnitType]:
|
||||
data_path = Path("resources/units/ground_units") / f"{vehicle.id}.yaml"
|
||||
if not data_path.exists():
|
||||
logging.warning(f"No data for {vehicle.id}; it will not be available")
|
||||
return
|
||||
|
||||
with data_path.open(encoding="utf-8") as data_file:
|
||||
data = yaml.safe_load(data_file)
|
||||
|
||||
try:
|
||||
introduction = data["introduced"]
|
||||
if introduction is None:
|
||||
introduction = "N/A"
|
||||
except KeyError:
|
||||
introduction = "No data."
|
||||
|
||||
class_name = data.get("class")
|
||||
unit_class: Optional[GroundUnitClass] = None
|
||||
if class_name is not None:
|
||||
unit_class = GroundUnitClass(class_name)
|
||||
|
||||
for variant in data.get("variants", [vehicle.id]):
|
||||
yield GroundUnitType(
|
||||
dcs_unit_type=vehicle,
|
||||
unit_class=unit_class,
|
||||
spawn_weight=data.get("spawn_weight", 0),
|
||||
name=variant,
|
||||
description=data.get(
|
||||
"description",
|
||||
f"No data. <a href=\"https://google.com/search?q=DCS+{variant.replace(' ', '+')}\"><span style=\"color:#FFFFFF\">Google {variant}</span></a>",
|
||||
),
|
||||
year_introduced=introduction,
|
||||
country_of_origin=data.get("origin", "No data."),
|
||||
manufacturer=data.get("manufacturer", "No data."),
|
||||
role=data.get("role", "No data."),
|
||||
price=data.get("price", 1),
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
from typing import TypeVar, Generic, Type
|
||||
|
||||
from dcs.unittype import UnitType as DcsUnitType
|
||||
|
||||
DcsUnitTypeT = TypeVar("DcsUnitTypeT", bound=Type[DcsUnitType])
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class UnitType(Generic[DcsUnitTypeT]):
|
||||
dcs_unit_type: DcsUnitTypeT
|
||||
name: str
|
||||
description: str
|
||||
year_introduced: str
|
||||
country_of_origin: str
|
||||
manufacturer: str
|
||||
role: str
|
||||
price: int
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name
|
||||
|
||||
@cached_property
|
||||
def eplrs_capable(self) -> bool:
|
||||
return getattr(self.dcs_unit_type, "eplrs", False)
|
||||
@@ -1,407 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterator,
|
||||
List,
|
||||
TYPE_CHECKING,
|
||||
Union,
|
||||
)
|
||||
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
from game.theater import Airfield, ControlPoint
|
||||
from game.transfers import CargoShip
|
||||
from game.unitmap import (
|
||||
AirliftUnits,
|
||||
Building,
|
||||
ConvoyUnit,
|
||||
FrontLineUnit,
|
||||
GroundObjectUnit,
|
||||
UnitMap,
|
||||
FlyingUnit,
|
||||
)
|
||||
from gen.flights.flight import Flight
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game import Game
|
||||
|
||||
DEBRIEFING_LOG_EXTENSION = "log"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AirLosses:
|
||||
player: List[FlyingUnit]
|
||||
enemy: List[FlyingUnit]
|
||||
|
||||
@property
|
||||
def losses(self) -> Iterator[FlyingUnit]:
|
||||
return itertools.chain(self.player, self.enemy)
|
||||
|
||||
def by_type(self, player: bool) -> Dict[AircraftType, int]:
|
||||
losses_by_type: Dict[AircraftType, int] = defaultdict(int)
|
||||
losses = self.player if player else self.enemy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.flight.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def surviving_flight_members(self, flight: Flight) -> int:
|
||||
losses = 0
|
||||
for loss in self.losses:
|
||||
if loss.flight == flight:
|
||||
losses += 1
|
||||
return flight.count - losses
|
||||
|
||||
|
||||
@dataclass
|
||||
class GroundLosses:
|
||||
player_front_line: List[FrontLineUnit] = field(default_factory=list)
|
||||
enemy_front_line: List[FrontLineUnit] = field(default_factory=list)
|
||||
|
||||
player_convoy: List[ConvoyUnit] = field(default_factory=list)
|
||||
enemy_convoy: List[ConvoyUnit] = field(default_factory=list)
|
||||
|
||||
player_cargo_ships: List[CargoShip] = field(default_factory=list)
|
||||
enemy_cargo_ships: List[CargoShip] = field(default_factory=list)
|
||||
|
||||
player_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
enemy_airlifts: List[AirliftUnits] = field(default_factory=list)
|
||||
|
||||
player_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||
enemy_ground_objects: List[GroundObjectUnit[Any]] = field(default_factory=list)
|
||||
|
||||
player_buildings: List[Building] = field(default_factory=list)
|
||||
enemy_buildings: List[Building] = field(default_factory=list)
|
||||
|
||||
player_airfields: List[Airfield] = field(default_factory=list)
|
||||
enemy_airfields: List[Airfield] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BaseCaptureEvent:
|
||||
control_point: ControlPoint
|
||||
captured_by_player: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StateData:
|
||||
#: True if the mission ended. If False, the mission exited abnormally.
|
||||
mission_ended: bool
|
||||
|
||||
#: Names of aircraft units that were killed during the mission.
|
||||
killed_aircraft: List[str]
|
||||
|
||||
#: Names of vehicles, ships or buildings that were killed during the mission.
|
||||
killed_ground_units: List[str]
|
||||
|
||||
#: List of descriptions of destroyed statics. Format of each element is a mapping of
|
||||
#: the coordinate type ("x", "y", "z", "type", "orientation") to the value.
|
||||
destroyed_statics: List[dict[str, Union[float, str]]]
|
||||
|
||||
#: Mangled names of bases that were captured during the mission.
|
||||
base_capture_events: List[str]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, data: Dict[str, Any]) -> StateData:
|
||||
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"]}),
|
||||
destroyed_statics=data["destroyed_objects_positions"],
|
||||
base_capture_events=data["base_capture_events"],
|
||||
)
|
||||
|
||||
|
||||
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.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
self.player_country = game.blue.country_name
|
||||
self.enemy_country = game.red.country_name
|
||||
|
||||
self.air_losses = self.dead_aircraft()
|
||||
self.ground_losses = self.dead_ground_units()
|
||||
self.base_captures = self.base_capture_events()
|
||||
|
||||
@property
|
||||
def front_line_losses(self) -> Iterator[FrontLineUnit]:
|
||||
yield from self.ground_losses.player_front_line
|
||||
yield from self.ground_losses.enemy_front_line
|
||||
|
||||
@property
|
||||
def convoy_losses(self) -> Iterator[ConvoyUnit]:
|
||||
yield from self.ground_losses.player_convoy
|
||||
yield from self.ground_losses.enemy_convoy
|
||||
|
||||
@property
|
||||
def cargo_ship_losses(self) -> Iterator[CargoShip]:
|
||||
yield from self.ground_losses.player_cargo_ships
|
||||
yield from self.ground_losses.enemy_cargo_ships
|
||||
|
||||
@property
|
||||
def airlift_losses(self) -> Iterator[AirliftUnits]:
|
||||
yield from self.ground_losses.player_airlifts
|
||||
yield from self.ground_losses.enemy_airlifts
|
||||
|
||||
@property
|
||||
def ground_object_losses(self) -> Iterator[GroundObjectUnit[Any]]:
|
||||
yield from self.ground_losses.player_ground_objects
|
||||
yield from self.ground_losses.enemy_ground_objects
|
||||
|
||||
@property
|
||||
def building_losses(self) -> Iterator[Building]:
|
||||
yield from self.ground_losses.player_buildings
|
||||
yield from self.ground_losses.enemy_buildings
|
||||
|
||||
@property
|
||||
def damaged_runways(self) -> Iterator[Airfield]:
|
||||
yield from self.ground_losses.player_airfields
|
||||
yield from self.ground_losses.enemy_airfields
|
||||
|
||||
def casualty_count(self, control_point: ControlPoint) -> int:
|
||||
return len([x for x in self.front_line_losses if x.origin == control_point])
|
||||
|
||||
def front_line_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_front_line
|
||||
else:
|
||||
losses = self.ground_losses.enemy_front_line
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def convoy_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_convoy
|
||||
else:
|
||||
losses = self.ground_losses.enemy_convoy
|
||||
for loss in losses:
|
||||
losses_by_type[loss.unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def cargo_ship_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
ships = self.ground_losses.player_cargo_ships
|
||||
else:
|
||||
ships = self.ground_losses.enemy_cargo_ships
|
||||
for ship in ships:
|
||||
for unit_type, count in ship.units.items():
|
||||
losses_by_type[unit_type] += count
|
||||
return losses_by_type
|
||||
|
||||
def airlift_losses_by_type(self, player: bool) -> dict[GroundUnitType, int]:
|
||||
losses_by_type: dict[GroundUnitType, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_airlifts
|
||||
else:
|
||||
losses = self.ground_losses.enemy_airlifts
|
||||
for loss in losses:
|
||||
for unit_type in loss.cargo:
|
||||
losses_by_type[unit_type] += 1
|
||||
return losses_by_type
|
||||
|
||||
def building_losses_by_type(self, player: bool) -> Dict[str, int]:
|
||||
losses_by_type: Dict[str, int] = defaultdict(int)
|
||||
if player:
|
||||
losses = self.ground_losses.player_buildings
|
||||
else:
|
||||
losses = self.ground_losses.enemy_buildings
|
||||
for loss in losses:
|
||||
if loss.ground_object.control_point.captured != player:
|
||||
continue
|
||||
|
||||
losses_by_type[loss.ground_object.dcs_identifier] += 1
|
||||
return losses_by_type
|
||||
|
||||
def dead_aircraft(self) -> AirLosses:
|
||||
player_losses = []
|
||||
enemy_losses = []
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
aircraft = self.unit_map.flight(unit_name)
|
||||
if aircraft is None:
|
||||
logging.error(f"Could not find Flight matching {unit_name}")
|
||||
continue
|
||||
if aircraft.flight.departure.captured:
|
||||
player_losses.append(aircraft)
|
||||
else:
|
||||
enemy_losses.append(aircraft)
|
||||
return AirLosses(player_losses, enemy_losses)
|
||||
|
||||
def dead_ground_units(self) -> GroundLosses:
|
||||
losses = GroundLosses()
|
||||
for unit_name in self.state_data.killed_ground_units:
|
||||
front_line_unit = self.unit_map.front_line_unit(unit_name)
|
||||
if front_line_unit is not None:
|
||||
if front_line_unit.origin.captured:
|
||||
losses.player_front_line.append(front_line_unit)
|
||||
else:
|
||||
losses.enemy_front_line.append(front_line_unit)
|
||||
continue
|
||||
|
||||
convoy_unit = self.unit_map.convoy_unit(unit_name)
|
||||
if convoy_unit is not None:
|
||||
if convoy_unit.convoy.player_owned:
|
||||
losses.player_convoy.append(convoy_unit)
|
||||
else:
|
||||
losses.enemy_convoy.append(convoy_unit)
|
||||
continue
|
||||
|
||||
cargo_ship = self.unit_map.cargo_ship(unit_name)
|
||||
if cargo_ship is not None:
|
||||
if cargo_ship.player_owned:
|
||||
losses.player_cargo_ships.append(cargo_ship)
|
||||
else:
|
||||
losses.enemy_cargo_ships.append(cargo_ship)
|
||||
continue
|
||||
|
||||
ground_object_unit = self.unit_map.ground_object_unit(unit_name)
|
||||
if ground_object_unit is not None:
|
||||
if ground_object_unit.ground_object.control_point.captured:
|
||||
losses.player_ground_objects.append(ground_object_unit)
|
||||
else:
|
||||
losses.enemy_ground_objects.append(ground_object_unit)
|
||||
continue
|
||||
|
||||
building = self.unit_map.building_or_fortification(unit_name)
|
||||
# Try appending object to the name, because we do this for building statics.
|
||||
if building is None:
|
||||
building = self.unit_map.building_or_fortification(
|
||||
f"{unit_name} object"
|
||||
)
|
||||
if building is not None:
|
||||
if building.ground_object.control_point.captured:
|
||||
losses.player_buildings.append(building)
|
||||
else:
|
||||
losses.enemy_buildings.append(building)
|
||||
continue
|
||||
|
||||
airfield = self.unit_map.airfield(unit_name)
|
||||
if airfield is not None:
|
||||
if airfield.captured:
|
||||
losses.player_airfields.append(airfield)
|
||||
else:
|
||||
losses.enemy_airfields.append(airfield)
|
||||
continue
|
||||
|
||||
# Only logging as debug because we don't currently track infantry
|
||||
# deaths, so we expect to see quite a few unclaimed dead ground
|
||||
# units. We should start tracking those and covert this to a
|
||||
# warning.
|
||||
logging.debug(
|
||||
f"Death of untracked ground unit {unit_name} will "
|
||||
"have no effect. This may be normal behavior."
|
||||
)
|
||||
|
||||
for unit_name in self.state_data.killed_aircraft:
|
||||
airlift_unit = self.unit_map.airlift_unit(unit_name)
|
||||
if airlift_unit is not None:
|
||||
if airlift_unit.transfer.player:
|
||||
losses.player_airlifts.append(airlift_unit)
|
||||
else:
|
||||
losses.enemy_airlifts.append(airlift_unit)
|
||||
continue
|
||||
|
||||
return losses
|
||||
|
||||
def base_capture_events(self) -> List[BaseCaptureEvent]:
|
||||
"""Keeps only the last instance of a base capture event for each base ID."""
|
||||
blue_coalition_id = 2
|
||||
seen = set()
|
||||
captures = []
|
||||
for capture in reversed(self.state_data.base_capture_events):
|
||||
cp_id_str, new_owner_id_str, _name = capture.split("||")
|
||||
cp_id = int(cp_id_str)
|
||||
|
||||
# Only the most recent capture event matters.
|
||||
if cp_id in seen:
|
||||
continue
|
||||
seen.add(cp_id)
|
||||
|
||||
try:
|
||||
control_point = self.game.theater.find_control_point_by_id(cp_id)
|
||||
except KeyError:
|
||||
# Captured base is not a part of the campaign. This happens when neutral
|
||||
# bases are near the conflict. Nothing to do.
|
||||
continue
|
||||
|
||||
captured_by_player = int(new_owner_id_str) == blue_coalition_id
|
||||
if control_point.is_friendly(to_player=captured_by_player):
|
||||
# Base is currently friendly to the new owner. Was captured and
|
||||
# recaptured in the same mission. Nothing to do.
|
||||
continue
|
||||
|
||||
captures.append(BaseCaptureEvent(control_point, captured_by_player))
|
||||
return captures
|
||||
|
||||
|
||||
class PollDebriefingFileThread(threading.Thread):
|
||||
"""Thread class with a stop() method. The thread itself has to check
|
||||
regularly for the stopped() condition."""
|
||||
|
||||
def __init__(
|
||||
self, callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._stop_event = threading.Event()
|
||||
self.callback = callback
|
||||
self.game = game
|
||||
self.unit_map = unit_map
|
||||
|
||||
def stop(self) -> None:
|
||||
self._stop_event.set()
|
||||
|
||||
def stopped(self) -> bool:
|
||||
return self._stop_event.is_set()
|
||||
|
||||
def run(self) -> None:
|
||||
if os.path.isfile("state.json"):
|
||||
last_modified = os.path.getmtime("state.json")
|
||||
else:
|
||||
last_modified = 0
|
||||
while not self.stopped():
|
||||
try:
|
||||
if (
|
||||
os.path.isfile("state.json")
|
||||
and os.path.getmtime("state.json") > last_modified
|
||||
):
|
||||
with open("state.json", "r", encoding="utf-8") as json_file:
|
||||
json_data = json.load(json_file)
|
||||
debriefing = Debriefing(json_data, self.game, self.unit_map)
|
||||
self.callback(debriefing)
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
logging.exception(
|
||||
"Failed to decode state.json. Probably attempted read while DCS "
|
||||
"was still writing the file. Will retry in 5 seconds."
|
||||
)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
def wait_for_debriefing(
|
||||
callback: Callable[[Debriefing], None], game: Game, unit_map: UnitMap
|
||||
) -> PollDebriefingFileThread:
|
||||
thread = PollDebriefingFileThread(callback, game, unit_map)
|
||||
thread.start()
|
||||
return thread
|
||||
@@ -1,10 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from .event import Event
|
||||
|
||||
|
||||
class AirWarEvent(Event):
|
||||
"""Event handler for the air battle"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "AirWar"
|
||||
@@ -1,48 +1,53 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
import logging
|
||||
from typing import List, TYPE_CHECKING, Type
|
||||
|
||||
from dcs.mapping import Point
|
||||
from dcs.task import Task
|
||||
from dcs.action import Coalition
|
||||
from dcs.unittype import UnitType
|
||||
from dcs.task import *
|
||||
from dcs.vehicles import AirDefence
|
||||
from dcs.unittype import UnitType
|
||||
|
||||
from game import persistency
|
||||
from game.debriefing import Debriefing
|
||||
from game.operation.operation import Operation
|
||||
from game.theater import ControlPoint
|
||||
from gen.ato import AirTaskingOrder
|
||||
from gen.ground_forces.combat_stance import CombatStance
|
||||
from ..unitmap import UnitMap
|
||||
from game import *
|
||||
from game.infos.information import Information
|
||||
from theater import *
|
||||
from gen.environmentgen import EnvironmentSettings
|
||||
from gen.conflictgen import Conflict
|
||||
from game.db import assigned_units_from, unitdict_from
|
||||
from theater.start_generator import generate_airbase_defense_group
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..game import Game
|
||||
from userdata.debriefing import Debriefing
|
||||
from userdata import persistency
|
||||
|
||||
import game.db as db
|
||||
|
||||
DIFFICULTY_LOG_BASE = 1.1
|
||||
EVENT_DEPARTURE_MAX_DISTANCE = 340000
|
||||
|
||||
|
||||
MINOR_DEFEAT_INFLUENCE = 0.1
|
||||
DEFEAT_INFLUENCE = 0.3
|
||||
STRONG_DEFEAT_INFLUENCE = 0.5
|
||||
|
||||
|
||||
class Event:
|
||||
silent = False
|
||||
informational = False
|
||||
is_awacs_enabled = False
|
||||
ca_slots = 0
|
||||
|
||||
game = None # type: Game
|
||||
location = None # type: Point
|
||||
from_cp = None # type: ControlPoint
|
||||
departure_cp = None # type: ControlPoint
|
||||
to_cp = None # type: ControlPoint
|
||||
difficulty = 1 # type: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
game: Game,
|
||||
from_cp: ControlPoint,
|
||||
target_cp: ControlPoint,
|
||||
location: Point,
|
||||
attacker_name: str,
|
||||
defender_name: str,
|
||||
) -> None:
|
||||
operation = None # type: Operation
|
||||
difficulty = 1 # type: int
|
||||
environment_settings = None # type: EnvironmentSettings
|
||||
BONUS_BASE = 5
|
||||
|
||||
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
|
||||
self.game = game
|
||||
self.departure_cp = None
|
||||
self.from_cp = from_cp
|
||||
self.to_cp = target_cp
|
||||
self.location = location
|
||||
@@ -51,175 +56,194 @@ class Event:
|
||||
|
||||
@property
|
||||
def is_player_attacking(self) -> bool:
|
||||
return self.attacker_name == self.game.blue.faction.name
|
||||
return self.attacker_name == self.game.player_name
|
||||
|
||||
@property
|
||||
def tasks(self) -> List[Type[Task]]:
|
||||
def enemy_cp(self) -> ControlPoint:
|
||||
if self.attacker_name == self.game.player_name:
|
||||
return self.to_cp
|
||||
else:
|
||||
return self.departure_cp
|
||||
|
||||
@property
|
||||
def tasks(self) -> typing.Collection[typing.Type[Task]]:
|
||||
return []
|
||||
|
||||
def generate(self) -> UnitMap:
|
||||
Operation.prepare(self.game)
|
||||
unit_map = Operation.generate()
|
||||
Operation.current_mission.save(
|
||||
persistency.mission_path_for("liberation_nextturn.miz")
|
||||
)
|
||||
return unit_map
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return False
|
||||
|
||||
def commit_air_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.air_losses.losses:
|
||||
if loss.pilot is not None and (
|
||||
not loss.pilot.player
|
||||
or not self.game.settings.invulnerable_player_pilots
|
||||
):
|
||||
loss.pilot.kill()
|
||||
squadron = loss.flight.squadron
|
||||
aircraft = loss.flight.unit_type
|
||||
available = squadron.owned_aircraft
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {aircraft} from {squadron} but that airbase has "
|
||||
"none available."
|
||||
)
|
||||
continue
|
||||
def is_departure_available_from(self, cp: ControlPoint) -> bool:
|
||||
if not cp.captured:
|
||||
return False
|
||||
|
||||
logging.info(f"{aircraft} destroyed from {squadron}")
|
||||
squadron.owned_aircraft -= 1
|
||||
if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _commit_pilot_experience(ato: AirTaskingOrder) -> None:
|
||||
for package in ato.packages:
|
||||
for flight in package.flights:
|
||||
for idx, pilot in enumerate(flight.roster.pilots):
|
||||
if pilot is None:
|
||||
logging.error(
|
||||
f"Cannot award experience to pilot #{idx} of {flight} "
|
||||
"because no pilot is assigned"
|
||||
)
|
||||
continue
|
||||
pilot.record.missions_flown += 1
|
||||
if cp.is_global and not self.global_cp_available:
|
||||
return False
|
||||
|
||||
def commit_pilot_experience(self) -> None:
|
||||
self._commit_pilot_experience(self.game.blue.ato)
|
||||
self._commit_pilot_experience(self.game.red.ato)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def commit_front_line_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.front_line_losses:
|
||||
unit_type = loss.unit_type
|
||||
control_point = loss.origin
|
||||
available = control_point.base.total_units_of_type(unit_type)
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} from {control_point} but that "
|
||||
"airbase has none available."
|
||||
)
|
||||
continue
|
||||
def bonus(self) -> int:
|
||||
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
|
||||
|
||||
logging.info(f"{unit_type} destroyed from {control_point}")
|
||||
control_point.base.armor[unit_type] -= 1
|
||||
def is_successfull(self, debriefing: Debriefing) -> bool:
|
||||
return self.operation.is_successfull(debriefing)
|
||||
|
||||
@staticmethod
|
||||
def commit_convoy_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.convoy_losses:
|
||||
unit_type = loss.unit_type
|
||||
convoy = loss.convoy
|
||||
available = loss.convoy.units.get(unit_type, 0)
|
||||
convoy_name = f"convoy from {convoy.origin} to {convoy.destination}"
|
||||
if available <= 0:
|
||||
logging.error(
|
||||
f"Found killed {unit_type} in {convoy_name} but that convoy has "
|
||||
"none available."
|
||||
)
|
||||
continue
|
||||
def player_attacking(self, cp: ControlPoint, flights: db.TaskForceDict):
|
||||
if self.is_player_attacking:
|
||||
self.departure_cp = cp
|
||||
else:
|
||||
self.to_cp = cp
|
||||
|
||||
logging.info(f"{unit_type} destroyed in {convoy_name}")
|
||||
convoy.kill_unit(unit_type)
|
||||
def player_defending(self, cp: ControlPoint, flights: db.TaskForceDict):
|
||||
if self.is_player_attacking:
|
||||
self.departure_cp = cp
|
||||
else:
|
||||
self.to_cp = cp
|
||||
|
||||
@staticmethod
|
||||
def commit_cargo_ship_losses(debriefing: Debriefing) -> None:
|
||||
for ship in debriefing.cargo_ship_losses:
|
||||
logging.info(
|
||||
f"All units destroyed in cargo ship from {ship.origin} to "
|
||||
f"{ship.destination}."
|
||||
)
|
||||
ship.kill_all()
|
||||
def generate(self):
|
||||
self.operation.is_awacs_enabled = self.is_awacs_enabled
|
||||
self.operation.ca_slots = self.ca_slots
|
||||
|
||||
@staticmethod
|
||||
def commit_airlift_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.airlift_losses:
|
||||
transfer = loss.transfer
|
||||
airlift_name = f"airlift from {transfer.origin} to {transfer.destination}"
|
||||
for unit_type in loss.cargo:
|
||||
try:
|
||||
transfer.kill_unit(unit_type)
|
||||
logging.info(f"{unit_type} destroyed in {airlift_name}")
|
||||
except KeyError:
|
||||
logging.exception(
|
||||
f"Found killed {unit_type} in {airlift_name} but that airlift "
|
||||
"has none available."
|
||||
)
|
||||
self.operation.prepare(self.game.theater.terrain, is_quick=False)
|
||||
self.operation.generate()
|
||||
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
|
||||
self.environment_settings = self.operation.environment_settings
|
||||
|
||||
@staticmethod
|
||||
def commit_ground_object_losses(debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.ground_object_losses:
|
||||
# TODO: This should be stored in the TGO, not in the pydcs Group.
|
||||
if not hasattr(loss.group, "units_losts"):
|
||||
loss.group.units_losts = [] # type: ignore
|
||||
def commit(self, debriefing: Debriefing):
|
||||
|
||||
loss.group.units.remove(loss.unit)
|
||||
loss.group.units_losts.append(loss.unit) # type: ignore
|
||||
logging.info("Commiting mission results")
|
||||
|
||||
def commit_building_losses(self, debriefing: Debriefing) -> None:
|
||||
for loss in debriefing.building_losses:
|
||||
loss.ground_object.kill()
|
||||
self.game.message(
|
||||
"Building destroyed",
|
||||
f"{loss.ground_object.dcs_identifier} has been destroyed at "
|
||||
f"location {loss.ground_object.obj_name}",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def commit_damaged_runways(debriefing: Debriefing) -> None:
|
||||
for damaged_runway in debriefing.damaged_runways:
|
||||
damaged_runway.damage_runway()
|
||||
|
||||
def commit_captures(self, debriefing: Debriefing) -> None:
|
||||
for captured in debriefing.base_captures:
|
||||
# ------------------------------
|
||||
# Destroyed aircrafts
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for destroyed_aircraft in debriefing.killed_aircrafts:
|
||||
try:
|
||||
if captured.captured_by_player:
|
||||
self.game.message(
|
||||
f"{captured.control_point} captured!",
|
||||
f"We took control of {captured.control_point}.",
|
||||
)
|
||||
else:
|
||||
self.game.message(
|
||||
f"{captured.control_point} lost!",
|
||||
f"The enemy took control of {captured.control_point}.",
|
||||
)
|
||||
cpid = int(destroyed_aircraft.split("|")[3])
|
||||
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.aircraft.keys():
|
||||
logging.info("Aircraft destroyed : " + str(type))
|
||||
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
captured.control_point.capture(self.game, captured.captured_by_player)
|
||||
logging.info(f"Will run redeploy for {captured.control_point}")
|
||||
self.redeploy_units(captured.control_point)
|
||||
except Exception:
|
||||
logging.exception(f"Could not process base capture {captured}")
|
||||
# ------------------------------
|
||||
# Destroyed ground units
|
||||
killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints}
|
||||
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
|
||||
for killed_ground_unit in debriefing.killed_ground_units:
|
||||
try:
|
||||
cpid = int(killed_ground_unit.split("|")[3])
|
||||
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
|
||||
if cpid in cp_map.keys():
|
||||
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
|
||||
cp = cp_map[cpid]
|
||||
if type in cp.base.armor.keys():
|
||||
logging.info("Ground unit destroyed : " + str(type))
|
||||
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
def commit(self, debriefing: Debriefing) -> None:
|
||||
logging.info("Committing mission results")
|
||||
# ------------------------------
|
||||
# Static ground objects
|
||||
for destroyed_ground_unit_name in debriefing.killed_ground_units:
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if not cp.ground_objects:
|
||||
continue
|
||||
|
||||
self.commit_air_losses(debriefing)
|
||||
self.commit_pilot_experience()
|
||||
self.commit_front_line_losses(debriefing)
|
||||
self.commit_convoy_losses(debriefing)
|
||||
self.commit_cargo_ship_losses(debriefing)
|
||||
self.commit_airlift_losses(debriefing)
|
||||
self.commit_ground_object_losses(debriefing)
|
||||
self.commit_building_losses(debriefing)
|
||||
self.commit_damaged_runways(debriefing)
|
||||
self.commit_captures(debriefing)
|
||||
# -- Static ground objects
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.is_dead:
|
||||
continue
|
||||
|
||||
if ground_object.matches_string_identifier(destroyed_ground_unit_name):
|
||||
logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier))
|
||||
cp.ground_objects[i].is_dead = True
|
||||
|
||||
info = Information("Building destroyed",
|
||||
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
|
||||
# -- AA Site groups
|
||||
destroyed_units = 0
|
||||
info = Information("Units destroyed at " + ground_object.obj_name,
|
||||
"",
|
||||
self.game.turn)
|
||||
for i, ground_object in enumerate(cp.ground_objects):
|
||||
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
|
||||
for g in ground_object.groups:
|
||||
if not hasattr(g, "units_losts"):
|
||||
g.units_losts = []
|
||||
for u in g.units:
|
||||
if u.name == destroyed_ground_unit_name:
|
||||
g.units.remove(u)
|
||||
g.units_losts.append(u)
|
||||
destroyed_units = destroyed_units + 1
|
||||
info.text = u.type
|
||||
ucount = sum([len(g.units) for g in ground_object.groups])
|
||||
if ucount == 0:
|
||||
ground_object.is_dead = True
|
||||
if destroyed_units > 0:
|
||||
self.game.informations.append(info)
|
||||
|
||||
# ------------------------------
|
||||
# Captured bases
|
||||
#if self.game.player_country in db.BLUEFOR_FACTIONS:
|
||||
coalition = 2 # Value in DCS mission event for BLUE
|
||||
#else:
|
||||
# coalition = 1 # Value in DCS mission event for RED
|
||||
|
||||
for captured in debriefing.base_capture_events:
|
||||
try:
|
||||
id = int(captured.split("||")[0])
|
||||
new_owner_coalition = int(captured.split("||")[1])
|
||||
|
||||
captured_cps = []
|
||||
for cp in self.game.theater.controlpoints:
|
||||
if cp.id == id:
|
||||
|
||||
if cp.captured and new_owner_coalition != coalition:
|
||||
cp.captured = False
|
||||
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
pname = self.game.enemy_name
|
||||
captured_cps.append(cp)
|
||||
elif not(cp.captured) and new_owner_coalition == coalition:
|
||||
cp.captured = True
|
||||
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
pname = self.game.player_name
|
||||
captured_cps.append(cp)
|
||||
else:
|
||||
continue
|
||||
|
||||
cp.base.aircraft = {}
|
||||
cp.base.armor = {}
|
||||
|
||||
airbase_def_id = 0
|
||||
for g in cp.ground_objects:
|
||||
g.groups = []
|
||||
if g.airbase_group and pname != "":
|
||||
generate_airbase_defense_group(airbase_def_id, g, pname, self.game, cp)
|
||||
airbase_def_id = airbase_def_id + 1
|
||||
|
||||
for cp in captured_cps:
|
||||
logging.info("Will run redeploy for " + cp.name)
|
||||
self.redeploy_units(cp)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# Destroyed units carcass
|
||||
# -------------------------
|
||||
for destroyed_unit in debriefing.state_data.destroyed_statics:
|
||||
for destroyed_unit in debriefing.destroyed_units:
|
||||
self.game.add_destroyed_units(destroyed_unit)
|
||||
|
||||
# -----------------------------------
|
||||
@@ -227,179 +251,154 @@ class Event:
|
||||
for cp in self.game.theater.player_points():
|
||||
enemy_cps = [e for e in cp.connected_points if not e.captured]
|
||||
for enemy_cp in enemy_cps:
|
||||
print(
|
||||
"Compute frontline progression for : "
|
||||
+ cp.name
|
||||
+ " to "
|
||||
+ enemy_cp.name
|
||||
)
|
||||
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
|
||||
|
||||
delta = 0.0
|
||||
delta = 0
|
||||
player_won = True
|
||||
status_msg: str = ""
|
||||
ally_casualties = debriefing.casualty_count(cp)
|
||||
enemy_casualties = debriefing.casualty_count(enemy_cp)
|
||||
ally_casualties = killed_unit_count_by_cp[cp.id]
|
||||
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
|
||||
ally_units_alive = cp.base.total_armor
|
||||
enemy_units_alive = enemy_cp.base.total_armor
|
||||
|
||||
print(f"Remaining allied units: {ally_units_alive}")
|
||||
print(f"Remaining enemy units: {enemy_units_alive}")
|
||||
print(f"Allied casualties {ally_casualties}")
|
||||
print(f"Enemy casualties {enemy_casualties}")
|
||||
print(ally_units_alive)
|
||||
print(enemy_units_alive)
|
||||
print(ally_casualties)
|
||||
print(enemy_casualties)
|
||||
|
||||
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
|
||||
|
||||
player_aggresive = cp.stances[enemy_cp.id] in [
|
||||
CombatStance.AGGRESSIVE,
|
||||
CombatStance.ELIMINATION,
|
||||
CombatStance.BREAKTHROUGH,
|
||||
]
|
||||
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]
|
||||
|
||||
if ally_units_alive == 0:
|
||||
player_won = False
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"No allied units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces suffer a strong defeat."
|
||||
elif enemy_units_alive == 0:
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"No enemy units alive at {cp.name}-{enemy_cp.name} frontline. Allied ground forces win a strong victory."
|
||||
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
|
||||
player_won = False
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied forces are retreating along the {cp.name}-{enemy_cp.name} frontline, suffering a strong defeat."
|
||||
else:
|
||||
if enemy_casualties > ally_casualties:
|
||||
player_won = True
|
||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied forces break through the {cp.name}-{enemy_cp.name} frontline, winning a strong victory"
|
||||
else:
|
||||
if ratio > 3:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties massively outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a strong victory."
|
||||
elif ratio < 1.5:
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties minorly outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces win a minor victory."
|
||||
else:
|
||||
delta = DEFEAT_INFLUENCE
|
||||
status_msg = f"Enemy casualties outnumber allied casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces claim a victory."
|
||||
elif ally_casualties > enemy_casualties:
|
||||
|
||||
if (
|
||||
ally_units_alive > 2 * enemy_units_alive
|
||||
and player_aggresive
|
||||
):
|
||||
if ally_units_alive > 2*enemy_units_alive and player_aggresive:
|
||||
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
|
||||
player_won = True
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
status_msg = f"Despite suffering losses, allied forces still outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a minor victory."
|
||||
elif (
|
||||
ally_units_alive > 3 * enemy_units_alive
|
||||
and player_aggresive
|
||||
):
|
||||
elif ally_units_alive > 3*enemy_units_alive and player_aggresive:
|
||||
player_won = True
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Despite suffering losses, allied forces still heavily outnumber enemy forces along the {cp.name}-{enemy_cp.name} frontline. Due to allied force's aggressive posture, allied forces claim a major victory."
|
||||
else:
|
||||
# But if the enemy is not outnumbered, we lose
|
||||
# But is the enemy is not outnumbered, we lose
|
||||
player_won = False
|
||||
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces have overextended themselves, suffering a major defeat."
|
||||
else:
|
||||
delta = DEFEAT_INFLUENCE
|
||||
status_msg = f"Allied casualties outnumber enemy casualties along the {cp.name}-{enemy_cp.name} frontline. Allied forces suffer a defeat."
|
||||
delta = STRONG_DEFEAT_INFLUENCE
|
||||
|
||||
# No progress with defensive strategies
|
||||
if player_won and cp.stances[enemy_cp.id] in [
|
||||
CombatStance.DEFENSIVE,
|
||||
CombatStance.AMBUSH,
|
||||
]:
|
||||
print(
|
||||
f"Allied forces have adopted a defensive stance along the {cp.name}-{enemy_cp.name} "
|
||||
f"frontline, making only limited progress."
|
||||
)
|
||||
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
|
||||
print("Defensive stance, progress is limited")
|
||||
delta = MINOR_DEFEAT_INFLUENCE
|
||||
|
||||
# Handle the case where there are no casualties at all on either side but both sides still have units
|
||||
if delta == 0.0:
|
||||
print(status_msg)
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.",
|
||||
)
|
||||
if player_won:
|
||||
print(cp.name + " won ! factor > " + str(delta))
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
info = Information("Frontline Report",
|
||||
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
else:
|
||||
if player_won:
|
||||
print(status_msg)
|
||||
cp.base.affect_strength(delta)
|
||||
enemy_cp.base.affect_strength(-delta)
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}",
|
||||
)
|
||||
else:
|
||||
print(status_msg)
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
self.game.message(
|
||||
"Frontline Report",
|
||||
f"Our ground forces from {cp.name} are losing ground against the enemy forces from "
|
||||
f"{enemy_cp.name}. {status_msg}",
|
||||
)
|
||||
print(cp.name + " lost ! factor > " + str(delta))
|
||||
enemy_cp.base.affect_strength(delta)
|
||||
cp.base.affect_strength(-delta)
|
||||
info = Information("Frontline Report",
|
||||
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name,
|
||||
self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
def redeploy_units(self, cp: ControlPoint) -> None:
|
||||
""" "
|
||||
def skip(self):
|
||||
pass
|
||||
|
||||
def redeploy_units(self, cp):
|
||||
""""
|
||||
Auto redeploy units to newly captured base
|
||||
"""
|
||||
|
||||
ally_connected_cps = [
|
||||
ocp for ocp in cp.connected_points if cp.captured == ocp.captured
|
||||
]
|
||||
enemy_connected_cps = [
|
||||
ocp for ocp in cp.connected_points if cp.captured != ocp.captured
|
||||
]
|
||||
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured]
|
||||
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured]
|
||||
|
||||
# If the newly captured cp does not have enemy connected cp,
|
||||
# then it is not necessary to redeploy frontline units there.
|
||||
if len(enemy_connected_cps) == 0:
|
||||
return
|
||||
|
||||
# From each ally cp, send reinforcements
|
||||
for ally_cp in ally_connected_cps:
|
||||
self.redeploy_between(cp, ally_cp)
|
||||
|
||||
def redeploy_between(self, destination: ControlPoint, source: ControlPoint) -> None:
|
||||
total_units_redeployed = 0
|
||||
moved_units = {}
|
||||
|
||||
if source.has_active_frontline or not destination.captured:
|
||||
# If there are still active front lines to defend at the
|
||||
# transferring CP we should not transfer all units.
|
||||
#
|
||||
# Opfor also does not transfer all of their units.
|
||||
# TODO: Balance the CPs rather than moving half from everywhere.
|
||||
move_factor = 0.5
|
||||
else:
|
||||
# Otherwise we can move everything.
|
||||
move_factor = 1
|
||||
# From each ally cp, send reinforcements
|
||||
for ally_cp in ally_connected_cps:
|
||||
total_units_redeployed = 0
|
||||
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
|
||||
|
||||
for frontline_unit, count in source.base.armor.items():
|
||||
moved_units[frontline_unit] = int(count * move_factor)
|
||||
total_units_redeployed = total_units_redeployed + int(count * move_factor)
|
||||
moved_units = {}
|
||||
|
||||
destination.base.commission_units(moved_units)
|
||||
source.base.commit_losses(moved_units)
|
||||
# If the connected base, does not have any more enemy cp connected.
|
||||
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once)
|
||||
if len(own_enemy_cp) > 0 or not cp.captured:
|
||||
for frontline_unit, count in ally_cp.base.armor.items():
|
||||
moved_units[frontline_unit] = int(count/2)
|
||||
total_units_redeployed = total_units_redeployed + int(count/2)
|
||||
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
|
||||
for frontline_unit, count in ally_cp.base.armor.items():
|
||||
moved_units[frontline_unit] = count
|
||||
total_units_redeployed = total_units_redeployed + count
|
||||
|
||||
# Also transfer pending deliveries.
|
||||
for unit_type, count in source.ground_unit_orders.units.items():
|
||||
move_count = int(count * move_factor)
|
||||
source.ground_unit_orders.sell({unit_type: move_count})
|
||||
destination.ground_unit_orders.order({unit_type: move_count})
|
||||
total_units_redeployed += move_count
|
||||
cp.base.commision_units(moved_units)
|
||||
ally_cp.base.commit_losses(moved_units)
|
||||
|
||||
if total_units_redeployed > 0:
|
||||
self.game.message(
|
||||
"Units redeployed",
|
||||
f"{total_units_redeployed} units have been redeployed from "
|
||||
f"{source.name} to {destination.name}",
|
||||
)
|
||||
if total_units_redeployed > 0:
|
||||
info = Information("Units redeployed", "", self.game.turn)
|
||||
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name
|
||||
self.game.informations.append(info)
|
||||
logging.info(info.text)
|
||||
|
||||
|
||||
|
||||
class UnitsDeliveryEvent(Event):
|
||||
informational = True
|
||||
units = None # type: typing.Dict[UnitType, int]
|
||||
|
||||
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
|
||||
super(UnitsDeliveryEvent, self).__init__(game=game,
|
||||
location=to_cp.position,
|
||||
from_cp=from_cp,
|
||||
target_cp=to_cp,
|
||||
attacker_name=attacker_name,
|
||||
defender_name=defender_name)
|
||||
|
||||
self.units = {}
|
||||
|
||||
def __str__(self):
|
||||
return "Pending delivery to {}".format(self.to_cp)
|
||||
|
||||
def deliver(self, units: typing.Dict[UnitType, int]):
|
||||
for k, v in units.items():
|
||||
self.units[k] = self.units.get(k, 0) + v
|
||||
|
||||
def skip(self):
|
||||
|
||||
for k, v in self.units.items():
|
||||
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
|
||||
self.game.informations.append(info)
|
||||
|
||||
self.to_cp.base.commision_units(self.units)
|
||||
|
||||
@@ -1,12 +1,53 @@
|
||||
from .event import Event
|
||||
from game.event import *
|
||||
from game.operation.frontlineattack import FrontlineAttackOperation
|
||||
from userdata.debriefing import Debriefing
|
||||
|
||||
|
||||
class FrontlineAttackEvent(Event):
|
||||
"""
|
||||
An event centered on a FrontLine Conflict.
|
||||
Currently the same as its parent, but here for legacy compatibility as well as to allow for
|
||||
future unique Event handling
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
@property
|
||||
def tasks(self) -> typing.Collection[typing.Type[Task]]:
|
||||
if self.is_player_attacking:
|
||||
return [CAS, CAP]
|
||||
else:
|
||||
return [CAP]
|
||||
|
||||
@property
|
||||
def global_cp_available(self) -> bool:
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return "Frontline attack"
|
||||
|
||||
def is_successfull(self, debriefing: Debriefing):
|
||||
attackers_success = True
|
||||
if self.from_cp.captured:
|
||||
return attackers_success
|
||||
else:
|
||||
return not attackers_success
|
||||
|
||||
def commit(self, debriefing: Debriefing):
|
||||
super(FrontlineAttackEvent, self).commit(debriefing)
|
||||
|
||||
def skip(self):
|
||||
if self.to_cp.captured:
|
||||
self.to_cp.base.affect_strength(-0.1)
|
||||
|
||||
def player_attacking(self, flights: db.TaskForceDict):
|
||||
op = FrontlineAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
from_cp=self.from_cp,
|
||||
departure_cp=self.departure_cp,
|
||||
to_cp=self.to_cp)
|
||||
self.operation = op
|
||||
|
||||
def player_defending(self, flights: db.TaskForceDict):
|
||||
op = FrontlineAttackOperation(game=self.game,
|
||||
attacker_name=self.attacker_name,
|
||||
defender_name=self.defender_name,
|
||||
from_cp=self.from_cp,
|
||||
departure_cp=self.departure_cp,
|
||||
to_cp=self.to_cp)
|
||||
self.operation = op
|
||||
|
||||
|
||||
50
game/factions/australia_2005.py
Normal file
50
game/factions/australia_2005.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Australia_2005 = {
|
||||
"country": "Australia",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
FA_18C_hornet,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.APC_M113,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.IFV_MCV_80,
|
||||
|
||||
UH_1H,
|
||||
AH_1W, # Standing as EC Tiger
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.Rapier_FSA_Launcher,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.Rapier_FSA_Launcher,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "lhanames": [
|
||||
"HMAS Canberra",
|
||||
"HMAS Adelaide"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
58
game/factions/bluefor_coldwar.py
Normal file
58
game/factions/bluefor_coldwar.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
BLUEFOR_COLDWAR = {
|
||||
"country": "Combined Joint Task Forces Blue",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_14B,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
A_10A,
|
||||
AJS37,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat": [
|
||||
], "has_jtac": True
|
||||
}
|
||||
65
game/factions/bluefor_coldwar_a4.py
Normal file
65
game/factions/bluefor_coldwar_a4.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
|
||||
BLUEFOR_COLDWAR_A4 = {
|
||||
"country": "Combined Joint Task Forces Blue",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_14B,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
A_10A,
|
||||
AJS37,
|
||||
A_4E_C,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat": [
|
||||
], "requirements": {
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
}, "has_jtac": True
|
||||
}
|
||||
68
game/factions/bluefor_coldwar_mods.py
Normal file
68
game/factions/bluefor_coldwar_mods.py
Normal file
@@ -0,0 +1,68 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
from pydcs_extensions.a4ec.a4ec import A_4E_C
|
||||
from pydcs_extensions.mb339.mb339 import MB_339PAN
|
||||
|
||||
BLUEFOR_COLDWAR_MODS = {
|
||||
"country": "USA",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_14B,
|
||||
F_4E,
|
||||
F_5E_3,
|
||||
A_10A,
|
||||
AJS37,
|
||||
A_4E_C,
|
||||
MB_339PAN,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M60A3_Patton,
|
||||
Armor.APC_M113,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Chaparral_M48,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.AAA_Vulcan_M163,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat": [
|
||||
], "requirements": {
|
||||
"MB-339A": "http://www.freccetricolorivirtuali.net/",
|
||||
"Community A-4E": "https://heclak.github.io/community-a4e-c/",
|
||||
}, "has_jtac": True
|
||||
}
|
||||
83
game/factions/bluefor_modern.py
Normal file
83
game/factions/bluefor_modern.py
Normal file
@@ -0,0 +1,83 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
BLUEFOR_MODERN = {
|
||||
"country": "Combined Joint Task Forces Blue",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
|
||||
F_15C,
|
||||
F_14B,
|
||||
FA_18C_hornet,
|
||||
F_16C_50,
|
||||
JF_17,
|
||||
M_2000C,
|
||||
F_5E_3,
|
||||
Su_27,
|
||||
|
||||
Su_25T,
|
||||
A_10A,
|
||||
A_10C,
|
||||
A_10C_2,
|
||||
AV8BNA,
|
||||
AJS37,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
UH_1H,
|
||||
AH_64D,
|
||||
Ka_50,
|
||||
SA342M,
|
||||
SA342L,
|
||||
|
||||
Armor.MBT_M1A2_Abrams,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.ATGM_M1134_Stryker,
|
||||
Armor.IFV_M2A2_Bradley,
|
||||
Armor.IFV_Marder,
|
||||
Armor.APC_M1043_HMMWV_Armament,
|
||||
|
||||
Artillery.MLRS_M270,
|
||||
Artillery.SPH_M109_Paladin,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.SAM_Patriot_EPP_III,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis,
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa,
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"CVN-71 Theodore Roosevelt",
|
||||
"CVN-72 Abraham Lincoln",
|
||||
"CVN-73 George Washington",
|
||||
"CVN-74 John C. Stennis",
|
||||
], "lhanames": [
|
||||
"LHA-1 Tarawa",
|
||||
"LHA-2 Saipan",
|
||||
"LHA-3 Belleau Wood",
|
||||
"LHA-4 Nassau",
|
||||
"LHA-5 Peleliu"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
43
game/factions/canada_2005.py
Normal file
43
game/factions/canada_2005.py
Normal file
@@ -0,0 +1,43 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
Canada_2005 = {
|
||||
"country": "Canada",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
FA_18C_hornet,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
Armor.MBT_Leopard_1A3,
|
||||
Armor.MBT_Leopard_2,
|
||||
Armor.IFV_LAV_25,
|
||||
Armor.APC_M113,
|
||||
Armor.IFV_MCV_80,
|
||||
|
||||
UH_1H,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
], "shorad": [
|
||||
AirDefence.SAM_Avenger_M1097,
|
||||
], "destroyer": [
|
||||
USS_Arleigh_Burke_IIa,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
80
game/factions/china_2010.py
Normal file
80
game/factions/china_2010.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
China_2010 = {
|
||||
"country": "China",
|
||||
"side": "red",
|
||||
"units": [
|
||||
|
||||
MiG_21Bis, # Standing as J-7
|
||||
Su_30,
|
||||
Su_33,
|
||||
J_11A,
|
||||
JF_17,
|
||||
|
||||
IL_76MD,
|
||||
IL_78M,
|
||||
An_26B,
|
||||
An_30M,
|
||||
Yak_40,
|
||||
|
||||
KJ_2000,
|
||||
|
||||
Mi_8MT,
|
||||
Mi_28N,
|
||||
|
||||
AirDefence.SAM_SA_10_S_300PS_LN_5P85C, # Standing as HQ-9+
|
||||
AirDefence.SAM_SA_6_Kub_LN_2P25,
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
|
||||
Armor.ZTZ_96B,
|
||||
Armor.MBT_T_55,
|
||||
Armor.ZBD_04A,
|
||||
Armor.IFV_BMP_1,
|
||||
Artillery.MLRS_9A52_Smerch,
|
||||
Artillery.SPH_2S9_Nona,
|
||||
|
||||
Unarmed.Transport_Ural_375,
|
||||
Unarmed.Transport_UAZ_469,
|
||||
|
||||
Infantry.Paratrooper_AKS,
|
||||
Infantry.Infantry_Soldier_Rus,
|
||||
Infantry.Paratrooper_RPG_16,
|
||||
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
Bulk_cargo_ship_Yakushev,
|
||||
Dry_cargo_ship_Ivanov,
|
||||
Tanker_Elnya_160
|
||||
],
|
||||
"shorad":[
|
||||
AirDefence.SPAAA_ZSU_23_4_Shilka,
|
||||
AirDefence.Rapier_FSA_Launcher, # Standing as PL-9C Shorad
|
||||
AirDefence.HQ_7_Self_Propelled_LN
|
||||
], "aircraft_carrier": [
|
||||
CV_1143_5_Admiral_Kuznetsov,
|
||||
], "destroyer": [
|
||||
Type_052B_Destroyer,
|
||||
Type_052C_Destroyer
|
||||
], "cruiser": [
|
||||
Type_054A_Frigate,
|
||||
], "helicopter_carrier": [
|
||||
Type_071_Amphibious_Transport_Dock,
|
||||
], "lhanames": [
|
||||
"Kunlun Shan",
|
||||
"Jinggang Shan",
|
||||
"Changbai Shan",
|
||||
"Yimeng Shan",
|
||||
"Longhu Shan",
|
||||
"Wuzhi Shan",
|
||||
"Wudang Shan"
|
||||
], "carrier_names": [
|
||||
"001 Liaoning",
|
||||
"002 Shandong",
|
||||
], "boat":[
|
||||
"Type54GroupGenerator"
|
||||
],
|
||||
"has_jtac": True,
|
||||
"jtac_unit": WingLoong_I
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional, Dict, Type, List, Any, Iterator, TYPE_CHECKING
|
||||
|
||||
import dcs
|
||||
from dcs.countries import country_dict
|
||||
from dcs.unittype import ShipType, UnitType
|
||||
|
||||
from game.data.building_data import (
|
||||
WW2_ALLIES_BUILDINGS,
|
||||
DEFAULT_AVAILABLE_BUILDINGS,
|
||||
WW2_GERMANY_BUILDINGS,
|
||||
WW2_FREE,
|
||||
)
|
||||
from game.data.doctrine import (
|
||||
Doctrine,
|
||||
MODERN_DOCTRINE,
|
||||
COLDWAR_DOCTRINE,
|
||||
WWII_DOCTRINE,
|
||||
)
|
||||
from game.data.groundunitclass import GroundUnitClass
|
||||
from game.dcs.aircrafttype import AircraftType
|
||||
from game.dcs.groundunittype import GroundUnitType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from game.theater.start_generator import ModSettings
|
||||
|
||||
|
||||
@dataclass
|
||||
class Faction:
|
||||
#: List of locales to use when generating random names. If not set, Faker will
|
||||
#: choose the default locale.
|
||||
locales: Optional[List[str]]
|
||||
|
||||
# Country used by this faction
|
||||
country: str = field(default="")
|
||||
|
||||
# Nice name of the faction
|
||||
name: str = field(default="")
|
||||
|
||||
# List of faction file authors
|
||||
authors: str = field(default="")
|
||||
|
||||
# A description of the faction
|
||||
description: str = field(default="")
|
||||
|
||||
# Available aircraft
|
||||
aircrafts: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available awacs aircraft
|
||||
awacs: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available tanker aircraft
|
||||
tankers: List[AircraftType] = field(default_factory=list)
|
||||
|
||||
# Available frontline units
|
||||
frontline_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Available artillery units
|
||||
artillery_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Infantry units used
|
||||
infantry_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Logistics units used
|
||||
logistics_units: List[GroundUnitType] = field(default_factory=list)
|
||||
|
||||
# Possible SAMS site generators for this faction
|
||||
air_defenses: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible EWR generators for this faction.
|
||||
ewrs: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible Missile site generators for this faction
|
||||
missiles: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible costal site generators for this faction
|
||||
coastal_defenses: List[str] = field(default_factory=list)
|
||||
|
||||
# Required mods or asset packs
|
||||
requirements: Dict[str, str] = field(default_factory=dict)
|
||||
|
||||
# possible aircraft carrier units
|
||||
aircraft_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# possible helicopter carrier units
|
||||
helicopter_carrier: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Possible carrier names
|
||||
carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Possible helicopter carrier names
|
||||
helicopter_carrier_names: List[str] = field(default_factory=list)
|
||||
|
||||
# Navy group generators
|
||||
navy_generators: List[str] = field(default_factory=list)
|
||||
|
||||
# Available destroyers
|
||||
destroyers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# Available cruisers
|
||||
cruisers: List[Type[ShipType]] = field(default_factory=list)
|
||||
|
||||
# How many navy group should we try to generate per CP on startup for this faction
|
||||
navy_group_count: int = field(default=1)
|
||||
|
||||
# How many missiles group should we try to generate per CP on startup for this faction
|
||||
missiles_group_count: int = field(default=1)
|
||||
|
||||
# How many coastal group should we try to generate per CP on startup for this faction
|
||||
coastal_group_count: int = field(default=1)
|
||||
|
||||
# Whether this faction has JTAC access
|
||||
has_jtac: bool = field(default=False)
|
||||
|
||||
# Unit to use as JTAC for this faction
|
||||
jtac_unit: Optional[AircraftType] = field(default=None)
|
||||
|
||||
# doctrine
|
||||
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
|
||||
|
||||
# List of available buildings for this faction
|
||||
building_set: List[str] = field(default_factory=list)
|
||||
|
||||
# List of default livery overrides
|
||||
liveries_overrides: Dict[AircraftType, List[str]] = field(default_factory=dict)
|
||||
|
||||
#: Set to True if the faction should force the "Unrestricted satnav" option
|
||||
#: for the mission. This option enables GPS for capable aircraft regardless
|
||||
#: of the time period or operator. For example, the CJTF "countries" don't
|
||||
#: appear to have GPS capability, so they need this.
|
||||
#:
|
||||
#: Note that this option cannot be set per-side. If either faction needs it,
|
||||
#: both will use it.
|
||||
unrestricted_satnav: bool = False
|
||||
|
||||
def has_access_to_unittype(self, unit_class: GroundUnitClass) -> bool:
|
||||
for vehicle in itertools.chain(self.frontline_units, self.artillery_units):
|
||||
if vehicle.unit_class is unit_class:
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
|
||||
faction = Faction(locales=json.get("locales"))
|
||||
|
||||
faction.country = json.get("country", "/")
|
||||
if faction.country not in [c.name for c in country_dict.values()]:
|
||||
raise AssertionError(
|
||||
'Faction\'s country ("{}") is not a valid DCS country ID'.format(
|
||||
faction.country
|
||||
)
|
||||
)
|
||||
|
||||
faction.name = json.get("name", "")
|
||||
if not faction.name:
|
||||
raise AssertionError("Faction has no valid name")
|
||||
|
||||
faction.authors = json.get("authors", "")
|
||||
faction.description = json.get("description", "")
|
||||
|
||||
faction.aircrafts = [AircraftType.named(n) for n in json.get("aircrafts", [])]
|
||||
faction.awacs = [AircraftType.named(n) for n in json.get("awacs", [])]
|
||||
faction.tankers = [AircraftType.named(n) for n in json.get("tankers", [])]
|
||||
|
||||
faction.aircrafts = list(
|
||||
set(faction.aircrafts + faction.awacs + faction.tankers)
|
||||
)
|
||||
|
||||
faction.frontline_units = [
|
||||
GroundUnitType.named(n) for n in json.get("frontline_units", [])
|
||||
]
|
||||
faction.artillery_units = [
|
||||
GroundUnitType.named(n) for n in json.get("artillery_units", [])
|
||||
]
|
||||
faction.infantry_units = [
|
||||
GroundUnitType.named(n) for n in json.get("infantry_units", [])
|
||||
]
|
||||
faction.logistics_units = [
|
||||
GroundUnitType.named(n) for n in json.get("logistics_units", [])
|
||||
]
|
||||
|
||||
faction.ewrs = json.get("ewrs", [])
|
||||
|
||||
faction.air_defenses = json.get("air_defenses", [])
|
||||
# Compatibility for older factions. All air defenses now belong to a
|
||||
# single group and the generator decides what belongs where.
|
||||
faction.air_defenses.extend(json.get("sams", []))
|
||||
faction.air_defenses.extend(json.get("shorads", []))
|
||||
|
||||
faction.missiles = json.get("missiles", [])
|
||||
faction.coastal_defenses = json.get("coastal_defenses", [])
|
||||
faction.requirements = json.get("requirements", {})
|
||||
|
||||
faction.carrier_names = json.get("carrier_names", [])
|
||||
faction.helicopter_carrier_names = json.get("helicopter_carrier_names", [])
|
||||
faction.navy_generators = json.get("navy_generators", [])
|
||||
faction.aircraft_carrier = load_all_ships(json.get("aircraft_carrier", []))
|
||||
faction.helicopter_carrier = load_all_ships(json.get("helicopter_carrier", []))
|
||||
faction.destroyers = load_all_ships(json.get("destroyers", []))
|
||||
faction.cruisers = load_all_ships(json.get("cruisers", []))
|
||||
faction.has_jtac = json.get("has_jtac", False)
|
||||
jtac_name = json.get("jtac_unit", None)
|
||||
if jtac_name is not None:
|
||||
faction.jtac_unit = AircraftType.named(jtac_name)
|
||||
else:
|
||||
faction.jtac_unit = None
|
||||
faction.navy_group_count = int(json.get("navy_group_count", 1))
|
||||
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
|
||||
faction.coastal_group_count = int(json.get("coastal_group_count", 0))
|
||||
|
||||
# Load doctrine
|
||||
doctrine = json.get("doctrine", "modern")
|
||||
if doctrine == "modern":
|
||||
faction.doctrine = MODERN_DOCTRINE
|
||||
elif doctrine == "coldwar":
|
||||
faction.doctrine = COLDWAR_DOCTRINE
|
||||
elif doctrine == "ww2":
|
||||
faction.doctrine = WWII_DOCTRINE
|
||||
else:
|
||||
faction.doctrine = MODERN_DOCTRINE
|
||||
|
||||
# Load the building set
|
||||
building_set = json.get("building_set", "default")
|
||||
if building_set == "default":
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
elif building_set == "ww2free":
|
||||
faction.building_set = WW2_FREE
|
||||
elif building_set == "ww2ally":
|
||||
faction.building_set = WW2_ALLIES_BUILDINGS
|
||||
elif building_set == "ww2germany":
|
||||
faction.building_set = WW2_GERMANY_BUILDINGS
|
||||
else:
|
||||
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
|
||||
|
||||
# Load liveries override
|
||||
faction.liveries_overrides = {}
|
||||
liveries_overrides = json.get("liveries_overrides", {})
|
||||
for name, livery in liveries_overrides.items():
|
||||
aircraft = AircraftType.named(name)
|
||||
faction.liveries_overrides[aircraft] = [s.lower() for s in livery]
|
||||
|
||||
faction.unrestricted_satnav = json.get("unrestricted_satnav", False)
|
||||
|
||||
return faction
|
||||
|
||||
@property
|
||||
def ground_units(self) -> Iterator[GroundUnitType]:
|
||||
yield from self.artillery_units
|
||||
yield from self.frontline_units
|
||||
yield from self.logistics_units
|
||||
|
||||
def infantry_with_class(
|
||||
self, unit_class: GroundUnitClass
|
||||
) -> Iterator[GroundUnitType]:
|
||||
for unit in self.infantry_units:
|
||||
if unit.unit_class is unit_class:
|
||||
yield unit
|
||||
|
||||
def apply_mod_settings(self, mod_settings: ModSettings) -> Faction:
|
||||
# aircraft
|
||||
if not mod_settings.a4_skyhawk:
|
||||
self.remove_aircraft("A-4E-C")
|
||||
if not mod_settings.hercules:
|
||||
self.remove_aircraft("Hercules")
|
||||
if not mod_settings.uh_60l:
|
||||
self.remove_aircraft("UH-60L")
|
||||
self.remove_aircraft("KC130J")
|
||||
if not mod_settings.f22_raptor:
|
||||
self.remove_aircraft("F-22A")
|
||||
if not mod_settings.f104_starfighter:
|
||||
self.remove_aircraft("VSN_F104G")
|
||||
self.remove_aircraft("VSN_F104S")
|
||||
self.remove_aircraft("VSN_F104S_AG")
|
||||
if not mod_settings.jas39_gripen:
|
||||
self.remove_aircraft("JAS39Gripen")
|
||||
self.remove_aircraft("JAS39Gripen_AG")
|
||||
if not mod_settings.su57_felon:
|
||||
self.remove_aircraft("Su-57")
|
||||
# frenchpack
|
||||
if not mod_settings.frenchpack:
|
||||
self.remove_vehicle("AMX10RCR")
|
||||
self.remove_vehicle("SEPAR")
|
||||
self.remove_vehicle("ERC")
|
||||
self.remove_vehicle("M120")
|
||||
self.remove_vehicle("AA20")
|
||||
self.remove_vehicle("TRM2000")
|
||||
self.remove_vehicle("TRM2000_Citerne")
|
||||
self.remove_vehicle("TRM2000_AA20")
|
||||
self.remove_vehicle("TRMMISTRAL")
|
||||
self.remove_vehicle("VABH")
|
||||
self.remove_vehicle("VAB_RADIO")
|
||||
self.remove_vehicle("VAB_50")
|
||||
self.remove_vehicle("VIB_VBR")
|
||||
self.remove_vehicle("VAB_HOT")
|
||||
self.remove_vehicle("VAB_MORTIER")
|
||||
self.remove_vehicle("VBL50")
|
||||
self.remove_vehicle("VBLANF1")
|
||||
self.remove_vehicle("VBL-radio")
|
||||
self.remove_vehicle("VBAE")
|
||||
self.remove_vehicle("VBAE_MMP")
|
||||
self.remove_vehicle("AMX-30B2")
|
||||
self.remove_vehicle("Tracma")
|
||||
self.remove_vehicle("JTACFP")
|
||||
self.remove_vehicle("SHERIDAN")
|
||||
self.remove_vehicle("Leclerc_XXI")
|
||||
self.remove_vehicle("Toyota_bleu")
|
||||
self.remove_vehicle("Toyota_vert")
|
||||
self.remove_vehicle("Toyota_desert")
|
||||
self.remove_vehicle("Kamikaze")
|
||||
self.remove_vehicle("AMX1375")
|
||||
self.remove_vehicle("AMX1390")
|
||||
self.remove_vehicle("VBCI")
|
||||
self.remove_vehicle("T62")
|
||||
self.remove_vehicle("T64BV")
|
||||
self.remove_vehicle("T72M")
|
||||
self.remove_vehicle("KORNET")
|
||||
# high digit sams
|
||||
if not mod_settings.high_digit_sams:
|
||||
self.remove_air_defenses("SA10BGenerator")
|
||||
self.remove_air_defenses("SA12Generator")
|
||||
self.remove_air_defenses("SA20Generator")
|
||||
self.remove_air_defenses("SA20BGenerator")
|
||||
self.remove_air_defenses("SA23Generator")
|
||||
self.remove_air_defenses("SA17Generator")
|
||||
self.remove_air_defenses("KS19Generator")
|
||||
return self
|
||||
|
||||
def remove_aircraft(self, name: str) -> None:
|
||||
for i in self.aircrafts:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.aircrafts.remove(i)
|
||||
|
||||
def remove_air_defenses(self, name: str) -> None:
|
||||
for i in self.air_defenses:
|
||||
if i == name:
|
||||
self.air_defenses.remove(i)
|
||||
|
||||
def remove_vehicle(self, name: str) -> None:
|
||||
for i in self.frontline_units:
|
||||
if i.dcs_unit_type.id == name:
|
||||
self.frontline_units.remove(i)
|
||||
|
||||
|
||||
def load_ship(name: str) -> Optional[Type[ShipType]]:
|
||||
if (ship := getattr(dcs.ships, name, None)) is not None:
|
||||
return ship
|
||||
logging.error(f"FACTION ERROR : Unable to find {name} in dcs.ships")
|
||||
return None
|
||||
|
||||
|
||||
def load_all_ships(data: list[str]) -> List[Type[ShipType]]:
|
||||
items = []
|
||||
for name in data:
|
||||
item = load_ship(name)
|
||||
if item is not None:
|
||||
items.append(item)
|
||||
return items
|
||||
@@ -1,54 +0,0 @@
|
||||
from __future__ import annotations
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterator, List, Optional, Type
|
||||
|
||||
from game import persistency
|
||||
from game.factions.faction import Faction
|
||||
|
||||
FACTION_DIRECTORY = Path("./resources/factions/")
|
||||
|
||||
|
||||
class FactionLoader:
|
||||
def __init__(self) -> None:
|
||||
self._factions: Optional[Dict[str, Faction]] = None
|
||||
|
||||
@property
|
||||
def factions(self) -> Dict[str, Faction]:
|
||||
self.initialize()
|
||||
assert self._factions is not None
|
||||
return self._factions
|
||||
|
||||
def initialize(self) -> None:
|
||||
if self._factions is None:
|
||||
self._factions = self.load_factions()
|
||||
|
||||
@staticmethod
|
||||
def find_faction_files_in(path: Path) -> List[Path]:
|
||||
return [f for f in path.glob("*.json") if f.is_file()]
|
||||
|
||||
@classmethod
|
||||
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
|
||||
user_faction_path = Path(persistency.base_path()) / "Liberation/Factions"
|
||||
files = cls.find_faction_files_in(
|
||||
FACTION_DIRECTORY
|
||||
) + cls.find_faction_files_in(user_faction_path)
|
||||
factions = {}
|
||||
|
||||
for f in files:
|
||||
try:
|
||||
with f.open("r", encoding="utf-8") as fdata:
|
||||
data = json.load(fdata)
|
||||
factions[data["name"]] = Faction.from_json(data)
|
||||
logging.info("Loaded faction : " + str(f))
|
||||
except Exception:
|
||||
logging.exception(f"Unable to load faction : {f}")
|
||||
|
||||
return factions
|
||||
|
||||
def __getitem__(self, name: str) -> Faction:
|
||||
return self.factions[name]
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
return iter(self.factions.keys())
|
||||
47
game/factions/france_1995.py
Normal file
47
game/factions/france_1995.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
France_1995 = {
|
||||
"country": "France",
|
||||
"side": "blue",
|
||||
"units": [
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
SA342Mistral,
|
||||
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.TPz_Fuchs, # Standing as VAB
|
||||
Armor.APC_Cobra, # Standing as VBL
|
||||
Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
|
||||
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
|
||||
Artillery.MLRS_M270,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
|
||||
], "shorad": [
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_Roland_ADS
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
62
game/factions/france_2005.py
Normal file
62
game/factions/france_2005.py
Normal file
@@ -0,0 +1,62 @@
|
||||
from dcs.helicopters import *
|
||||
from dcs.planes import *
|
||||
from dcs.ships import *
|
||||
from dcs.vehicles import *
|
||||
|
||||
France_2005 = {
|
||||
"country": "France",
|
||||
"side": "blue",
|
||||
"units":[
|
||||
M_2000C,
|
||||
Mirage_2000_5,
|
||||
FA_18C_hornet, # Standing as Rafale M
|
||||
|
||||
KC_135,
|
||||
KC130,
|
||||
C_130,
|
||||
E_3A,
|
||||
|
||||
SA342M,
|
||||
SA342L,
|
||||
SA342Mistral,
|
||||
|
||||
Armor.MBT_Leclerc,
|
||||
Armor.TPz_Fuchs, # Standing as VAB
|
||||
Armor.APC_Cobra, # Standing as VBL
|
||||
Armor.ATGM_M1134_Stryker, # Standing as VAB Mephisto
|
||||
Artillery.SPH_M109_Paladin, # Standing as AMX30 AuF1
|
||||
Artillery.MLRS_M270,
|
||||
|
||||
Unarmed.Transport_M818,
|
||||
Infantry.Infantry_M4,
|
||||
Infantry.Soldier_M249,
|
||||
|
||||
AirDefence.SAM_Roland_ADS,
|
||||
AirDefence.SAM_Hawk_PCP,
|
||||
AirDefence.HQ_7_Self_Propelled_LN, # Standing as Crotale
|
||||
|
||||
CVN_74_John_C__Stennis,
|
||||
LHA_1_Tarawa,
|
||||
Armed_speedboat,
|
||||
|
||||
], "shorad":[
|
||||
AirDefence.HQ_7_Self_Propelled_LN,
|
||||
AirDefence.SAM_Roland_ADS
|
||||
], "aircraft_carrier": [
|
||||
CVN_74_John_C__Stennis, # Standing as CDG Aircraft Carrier
|
||||
], "helicopter_carrier": [
|
||||
LHA_1_Tarawa, # Standing as Mistral Class
|
||||
], "destroyer": [
|
||||
Oliver_Hazzard_Perry_class,
|
||||
], "cruiser": [
|
||||
Ticonderoga_class,
|
||||
], "carrier_names": [
|
||||
"PA Charles de Gaulle",
|
||||
], "lhanames": [
|
||||
"L9013 Mistral",
|
||||
"L9014 Tonerre",
|
||||
"L9015 Dixmude"
|
||||
], "boat":[
|
||||
"ArleighBurkeGroupGenerator", "OliverHazardPerryGroupGenerator"
|
||||
], "has_jtac": True
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user