Compare commits

..

102 Commits

Author SHA1 Message Date
Khopa
8b0f877041 Verrsion string updated to 2.1.2 2020-09-28 00:42:27 +02:00
Khopa
bf7ad4cad2 Merge remote-tracking branch 'khopa/master' into develop 2020-09-28 00:27:25 +02:00
C. Perreau
66b659c0af Merge pull request #152 from VEAF/introduced-scripts-plugins
Introduced LUA scripts plugins
2020-09-28 00:26:26 +02:00
Khopa
8709ea948f Merge remote-tracking branch 'khopa/master' into develop 2020-09-28 00:21:33 +02:00
Khopa
7236c10403 Changelog update 2020-09-28 00:21:12 +02:00
C. Perreau
dde703ec41 Merge pull request #154 from VEAF/add-tanker-type-to-tanker-name
add tanker type to tanker name
2020-09-28 00:06:49 +02:00
Khopa
aa2e9b123c Fix : AI is not planning flights for Tornado. 2020-09-28 00:03:01 +02:00
Khopa
737e04d09e Merge branch 'master' into develop 2020-09-27 19:16:34 +02:00
C. Perreau
0fe59efd72 Merge pull request #157 from DanAlbert/fix-none
Fix None dereference.
2020-09-27 19:10:31 +02:00
C. Perreau
ddb50e6254 Merge pull request #151 from VEAF/UHF-intraflight-frequency-for-Player-and-Clients-A-10C
UHF Intraflight Frequency for Player and Clients A-10C
2020-09-27 19:09:33 +02:00
Dan Albert
72e6ae4186 Fix None dereference. 2020-09-26 16:32:02 -07:00
David Pierron
4d510f643a add tanker type to tanker name 2020-09-25 17:11:17 +02:00
David Pierron
66f607b5e6 added a comment that links to my forum post 2020-09-25 11:33:07 +02:00
David Pierron
1a125c62e7 added sample __plugins.lst file 2020-09-25 11:21:23 +02:00
David Pierron
84da44a27b Introduced LUA scripts plugins
In order to be able to customize the scripts that can be injected in the
mission, a __plugin.lst file is read and the scripts mentionned in this
file are injected (through DoScriptFile and not DoScript).

A mechanism checks if a standard script (Mist, JTACAutolase) has
already been loaded, to avoid loading them twice.
2020-09-25 11:06:25 +02:00
David Pierron
5e35efcaef UHF Intraflight Frequency for Player and Clients A-10C
In the mission editor, using a VHF frequency for a Player or
Client A-10C results in an error. Changed the radio definitions
to use AN/ARC-164 for intraflight comms.
2020-09-25 10:36:18 +02:00
Khopa
83f221c5e3 Use latest data-export 2020-09-25 01:23:05 +02:00
Khopa
e86a10e9ba JF-17 radio frequency fix. 2020-09-25 01:13:59 +02:00
C. Perreau
7eea328706 Merge pull request #149 from Khopa/develop
2.1.1
2020-09-25 01:06:09 +02:00
Khopa
aa9dcec0ad Version number 2020-09-25 00:58:11 +02:00
Khopa
e9bad2c7eb Version number update and changelog update 2020-09-25 00:56:12 +02:00
Khopa
ce257a31bb Fixed UI bugs when buying new units in base defense menu. 2020-09-25 00:36:13 +02:00
Khopa
c96b5cf4d7 Fixed bug when buying armor at base 2020-09-25 00:25:20 +02:00
Khopa
fb40e9273d Added newest contributors to about dialog. 2020-09-24 20:00:57 +02:00
Khopa
42c9af102b Added KJ 2000 to China as AWACS 2020-09-24 19:56:56 +02:00
Khopa
b7dff59542 P_47 variants added to db 2020-09-24 18:03:10 +02:00
Khopa
266927aa9a Added new payloads for F-16C for SEAD and CAS, added defaults payload for new P-47 variants. 2020-09-24 18:01:20 +02:00
C. Perreau
dcaa8d4e96 Merge pull request #142 from DanAlbert/update-pydcs
Update pydcs.
2020-09-23 00:39:17 +02:00
Khopa
0e2a449553 Fixes to Buy/Sell sam UI 2020-09-23 00:39:01 +02:00
Dan Albert
a70e035e02 Update pydcs.
This fixes the issue where delayed AI flights would not start if the
mission was not first saved via the DCS mission editor.
2020-09-20 15:07:32 -07:00
Khopa
4019da8ba9 Base defense armor unit icon style fix. 2020-09-20 17:15:43 +02:00
Khopa
5042ac1789 P-51/P-47 radios support. 2020-09-20 17:07:09 +02:00
Khopa
4031c9b978 Possible to sell/buy units at SAM location and in airports. 2020-09-19 17:11:43 +02:00
Khopa
65dd9bc286 Fix typo. 2020-09-19 17:10:56 +02:00
Khopa
e210dcb4df Added disband menu in ground object menu. 2020-09-19 14:02:53 +02:00
C. Perreau
52a379229e Merge pull request #134 from pedromagueija/master
Fix typo in CombatStance enumeration
2020-09-17 10:12:27 +02:00
Pedro Magueija
93b2e91c10 Fix usage of CombatStance typo 2020-09-15 15:03:41 +02:00
Pedro Magueija
b8afb01c46 Fix typo in CombatStance enumeration 2020-09-15 14:58:17 +02:00
C. Perreau
59e7665b65 Merge pull request #133 from DanAlbert/radio-naming
Name radios appropriately for each aircraft.
2020-09-12 22:35:49 +02:00
C. Perreau
8a8a835a32 Merge pull request #132 from DanAlbert/fix-radio-reservation
Ensure that allocated channels are reserved.
2020-09-12 22:35:00 +02:00
Dan Albert
6ceab69656 Name radios appropriately for each aircraft. 2020-09-12 12:30:52 -07:00
Dan Albert
9e98f05be0 Ensure that allocated channels are reserved.
This was previously mostly working because the allocator itself was
moving forward, but since each radio has its own allocator, aircraft
with different radios would often get overlapping intra-flight
frequencies.
2020-09-12 12:28:59 -07:00
Khopa
5da1db91fd Normandy airfields data export. 2020-09-12 15:49:30 +02:00
C. Perreau
f0d58acd62 Merge pull request #131 from DanAlbert/more-aircraft-radios
Add radio information for more aircraft.
2020-09-12 11:36:43 +02:00
Dan Albert
722ec00076 Add radio information for more aircraft.
Adds the following:

* AJS37
* AV-8B
* JF-17

This does move the preset channel allocation logic into its own class,
since we need to customize that behavior for the AJS37 since it has a
rather unique preset channel layout (see the comments in
`ViggenRadioChannelAllocator` for details).
2020-09-11 18:45:19 -07:00
Khopa
2db740e1ad Finished airfield export for PG map. 2020-09-11 22:00:22 +02:00
C. Perreau
70cd0e8c31 Merge pull request #130 from DanAlbert/callsigns
Handle callsigns for flights.
2020-09-11 20:53:44 +02:00
C. Perreau
848c92ec25 Merge pull request #129 from DanAlbert/fix-kneeboard-tacan
Fix TACAN/ILS info for airfields.
2020-09-11 20:52:15 +02:00
C. Perreau
98946d0a63 Merge pull request #128 from DanAlbert/coalesce-target-points
Improve target waypoint behavior in the kneeboard.
2020-09-11 20:51:52 +02:00
C. Perreau
e0a39104b1 Merge pull request #127 from DanAlbert/first-waypoint
Add first waypoint to FlightData.
2020-09-11 20:48:04 +02:00
C. Perreau
a4f66298a4 Merge pull request #126 from DanAlbert/syria-map-data
Add map data for Syria.
2020-09-11 20:47:32 +02:00
Dan Albert
993bf50012 Handle callsigns for flights.
We don't configure the callsigns that pydcs uses, so instead read
those from pydcs and use them where appropriate instead of just
guessing.

Fixes https://github.com/Khopa/dcs_liberation/issues/113.
2020-09-11 01:47:13 -07:00
Dan Albert
51bfc9a59b Update pydcs. 2020-09-11 01:47:13 -07:00
Dan Albert
837795e87a Fix TACAN/ILS info for airfields. 2020-09-10 17:05:32 -07:00
Dan Albert
d4820b2435 Coalesce large runs of target waypoints.
Since we create a target waypoint for every target in a
strike/SEAD/DEAD objective area (including every ground vehicle), the
kneeboard can quickly be overrun with target waypoints. When there are
many target waypoints, collapse them all into a single row for
brevity.
2020-09-10 01:38:27 -07:00
Dan Albert
180537cd48 Fix SEAD/DEAD reversal. 2020-09-10 01:38:27 -07:00
Dan Albert
8bc77bbf18 Make waypoint types less error prone.
Make the type of the waypoint a non-optional part of the constructor.
Every waypoint needs a type, and there's no good default (the previous
default, `TAKEOFF`, is actually unused). All of the target waypoints
were mistakenly being set as `TAKEOFF`, so I've fixed that in the
process.

Also, fix the bug where only the last custom target of a SEAD
objective was being added to the waypoint list because the append was
scoped incorrectly.
2020-09-10 01:38:25 -07:00
Dan Albert
474b606524 Add map data for Syria. 2020-09-08 16:45:08 -07:00
Dan Albert
8a7e43ef42 Also dedup ATC frequencies.
Some airports on the Syria map share ATC frequencies.
2020-09-08 16:45:08 -07:00
C. Perreau
7b5b486f0e Merge pull request #123 from DanAlbert/fix-radios
Fix radio information.
2020-09-07 23:50:36 +02:00
Dan Albert
ebedc02a0a Add first waypoint to FlightData.
The first waypoint is automatically added by pydcs, so it's not
actually in our waypoint list from the flight planner. Import is from
the group so it shows up in the kneeboard.
2020-09-06 22:07:10 -07:00
Dan Albert
ad42a3d956 Fix radio information.
Not every aircraft has a pydcs radio index, so we can't use that to
index into a list. Any mission with an A-10C crashes, since it would
try to use `None - 1` to index into the list of radios to find the
intra-flight radio.

Also fix the radio ranges for the newly added radios. The current
implementation can't model gaps, so extending the radio ranges across
those gaps means that we might allocate channels that aren't tunable
by those radios. Additionally, the end frequency is exclusive rather
than inclusive, so fix the ranges to include that last tunable
frequency.
2020-09-06 16:44:46 -07:00
Khopa
98d75bc721 Tomcat and M2000 radios support 2020-09-06 15:43:44 +02:00
Khopa
14fe68eb54 Added Mirage 2000 radios 2020-09-06 15:10:10 +02:00
Khopa
b6938c14ca Added southern airfields of PG map 2020-09-06 15:09:40 +02:00
Khopa
3c96c1d5b1 Removed incorrect imports causing pydcs being imported twice; 2020-09-06 12:33:40 +02:00
Khopa
2969ce2d56 Removed unused file 2020-09-06 12:11:03 +02:00
C. Perreau
bca92f5ee7 Merge pull request #122 from DanAlbert/fix-pydcs-path
Fix incorrect pydcs import paths.
2020-09-06 11:49:58 +02:00
C. Perreau
1c1982952e Merge pull request #121 from DanAlbert/preferred-runway
Pick ILS runways if possible.
2020-09-06 11:48:47 +02:00
Dan Albert
4b74b5a13d Fix incorrect pydcs import paths.
I've been wrongly importing these from `pydcs.dcs` instead of just
`dcs`, because that was what PyCharm thought they were. These will all
be broken when we get back to using a real pydcs instead of relying on
its directory being in our tree.

This page in the wiki should be updated:
https://github.com/Khopa/dcs_liberation/wiki/Developer's-Guide

Instead of recommending that `PYTHONPATH` be updated in the run
configuration, it should instead recommend that Settings -> Project:
dcs_liberation -> Project Structure be set to exclude the pydcs
directory from the dcs_liberation content root, and add the pydcs
directory as a *separate* content root.

Alternatively, we could recommend that configure a virtualenv (good
advice anyway, and pycharm knows how to set them up) that have people
run `pip install -e pydcs`.

I think even easier would be switching from the virtualenv-style
requirements.txt to pipenv, which can actually encode the `-e` style
pip install into its equivalent of requirements.txt.
2020-09-06 01:16:57 -07:00
Dan Albert
0fc00fac38 Pick ILS runways if possible. 2020-09-04 15:58:43 -07:00
C. Perreau
4446a7f060 Merge pull request #119 from DanAlbert/radio-setup
Allocate per-flight radio channels, set up preset channels.
2020-09-04 12:50:44 +02:00
Dan Albert
9d31c478d3 Fix briefing generation.
I removed the nav target info from the briefing because that doesn't
seem to have been doing what it was intended to do. It didn't give any
actual target information, all it would show was (example is a  JF-17
strike mission):

    PP1
    PP2
    PP3
    PP4

Without any additional context that doesn't seem too helpful to me.
I'll be following up (hopefully) shortly by adding target information
(type, coordinates, STPT/PP, etc) to both the briefing and the
kneeboard that will cover that.

Refactor a bunch to share some code with the kneeboard generator as
well.
2020-09-04 01:06:31 -07:00
Dan Albert
b31e186d1d Fix inconsistent runway numbering.
pydcs gives us a 3-digit runway, but most of our data is 2-digit
runway numbers, so we weren't finding any runways for those airfields.
2020-09-04 00:56:26 -07:00
Dan Albert
d02a3a0d3f Add carrier support to kneeboards. 2020-09-04 00:56:26 -07:00
Dan Albert
a9e65cc83d Setup default radio channels for player flights. 2020-09-03 15:02:59 -07:00
Dan Albert
010d505f04 Reserve frequencies used by beacons. 2020-09-03 15:02:59 -07:00
Dan Albert
95b9a3e1aa Check in beacon lists for most maps. 2020-09-03 15:02:59 -07:00
Dan Albert
d051859371 Add beacon list importer. 2020-09-03 15:02:59 -07:00
Dan Albert
af596c58c3 Control radio/TACAN allocation, set flight radios.
Add central registries for allocating TACAN/radio channels to the
Operation. These ensure that each channel is allocated uniquely, and
removes the caller's need to think about which frequency to use.

The registry allocates frequencies based on the radio it is given,
which ensures that the allocated frequency will be compatible with the
radio that needs it. A mapping from aircraft to the radio used by that
aircraft for intra-flight comms (i.e. the F-16 uses the AN/ARC-222)
exists for creating infra-flight channels appropriate for the
aircraft. Inter-flight channels are allocated by a generic UHF radio.

I've moved the inter-flight radio channels from the VHF to UHF range,
since that's the most easily allocated band, and inter-flight will be
in the highest demand.

Intra-flight radios are now generally not shared. For aircraft where
the radio type is not known we will still fall back to the shared
channel, but that will stop being the case as we gain more data.

Tankers have been moved to the Y TACAN band. Not completely needed,
but seems typical for most missions and deconflicts the tankers from
any unknown airfields (which always use the X band in DCS).
2020-09-03 15:02:59 -07:00
Dan Albert
b4e3067718 Update pydcs. 2020-09-03 15:02:59 -07:00
C. Perreau
40f8ae1cde Merge pull request #120 from DanAlbert/sort-objectives
Sort objective name combo boxes.
2020-09-03 21:36:06 +02:00
Dan Albert
f5f45a098e Sort objective name combo boxes. 2020-09-03 00:42:20 -07:00
Khopa
d429804c1f Airfields data for Nevada 2020-09-01 20:58:27 +02:00
Khopa
f5f770f401 Added airfields data for The Channel map. 2020-09-01 20:32:32 +02:00
Khopa
91d6ed0ee7 Completed Caucasus airfield data. 2020-09-01 20:23:52 +02:00
Khopa
06858b26c7 Added additional informations to the AirfieldData class 2020-09-01 13:47:41 +02:00
C. Perreau
7e60a43f53 Merge pull request #114 from DanAlbert/kneeboard
Generate kneeboards for player flights.
2020-09-01 12:51:04 +02:00
Dan Albert
e7e82dcd0b Build mission kneeboards.
This includes most of the briefing information in the kneeboard:

* Airfield info
* Waypoint info
* Comm info
* AWACS
* Tankers
* JTAC

There's more that could be done:

* Restrict tankers to the type compatible with the current aircraft
* Support for carriers
* Merge all relevant comm info (tankers, AWACS, JTAC, other flights)
  into the comm ladder

This gives us a good start and a framework to build on. Very likely
that we'll want to split part of this (probably the comm ladder) off
onto a separate page once we start adding more to this, since it's a
pretty full page currently.

Also missing is any checking that the contents do not go beyond the
bounds of the page. We could add this if needed. For now the page has
enough room for about a dozen waypoints, which is quite a bit more
than most missions need.
2020-08-31 13:01:05 -07:00
Dan Albert
66af6be063 Update pydcs. 2020-08-31 13:01:05 -07:00
C. Perreau
69e1d2779d Merge pull request #112 from DanAlbert/gitignore
Update gitignore.
2020-08-31 12:04:37 +02:00
Dan Albert
001752a81e Update gitignore. 2020-08-29 19:58:03 -07:00
C. Perreau
1000041bce Merge pull request #109 from DanAlbert/non-modal-mission-planning
Make the mission planning window non-modal.
2020-08-29 18:06:57 +02:00
Dan Albert
d50e791c30 Make the mission planning window non-modal.
Doesn't appear to be any need for this to be modal. Making it
non-modal allows interacting with the map during planning.
2020-08-28 13:42:38 -07:00
Khopa
7817d59989 Fixed campaign sometimes not starting when the user does not explicitly re-select a campaign and just kee p the default one.. 2020-08-27 23:47:00 +02:00
Khopa
139c4c1dd8 Fixed crash on mission generation when clearing slots. 2020-08-27 23:46:10 +02:00
Khopa
75bb6941d3 Added version string in the window title 2020-08-24 22:49:55 +02:00
Khopa
e92fb38271 Fixed Sweden 1990 faction not working 2020-08-24 22:32:37 +02:00
C. Perreau
e4eeef8f99 Merge pull request #102 from parithon/menukbsupport
Add keyboard support to the menu system
2020-08-24 21:42:27 +02:00
Khopa
21c355bc9f Fix small issue with ground object menu when the ground object is empty. 2020-08-24 21:34:24 +02:00
Anthony Conrad
04c878f57c Added keyboard support to the menu system 2020-08-23 22:25:16 -07:00
Anthony Conrad
ef23ce58d1 Added keyboard support for the menu system 2020-08-23 21:54:12 -07:00
99 changed files with 7234 additions and 510 deletions

5
.gitignore vendored
View File

@@ -12,10 +12,13 @@ tests/**
# User-specific stuff
.idea/
liberation_preferences.json
/kneeboards
/liberation_preferences.json
/state.json
logs/liberation.log
qt_ui/logs/liberation.log
*.psd
resources/scripts/plugins/*

4
.gitmodules vendored
View File

@@ -1,4 +1,4 @@
[submodule "pydcs"]
path = pydcs
url = https://github.com/pydcs/dcs
branch = master
url = https://github.com/khopa/dcs
branch = dataexport

19
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,19 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Main",
"type": "python",
"request": "launch",
"program": "qt_ui\\main.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": ".;./pydcs"
},
"preLaunchTask": "Prepare Environment"
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"python.pythonPath": "g:\\python\\dcs_liberation\\venv\\Scripts\\python.exe",
"vsintellicode.python.completionsEnabled": true
}

35
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Prepare Environment",
"type": "shell",
"isBackground": false,
"problemMatcher": [],
"command": "powershell",
"args": [
"-Command",
"& {if (-not (Test-Path ${workspaceFolder}\\venv)) { python -m venv ${workspaceFolder}\\venv; . ${workspaceFolder}\\venv\\scripts\\activate.ps1; pip install -r requirements.txt; Copy-Item ${workspaceFolder}\\venv\\Lib\\site-packages\\shiboken2\\shiboken2.abi3.dll ${workspaceFolder}\\venv\\Lib\\site-packages\\PySide2 } }",
],
"group": "build",
"options": {
"env": {
"PYTHONPATH": ".;./pydcs"
}
},
"presentation": {
"echo": true,
"reveal": "never",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"runOptions": {
"runOn": "folderOpen"
}
}
]
}

View File

@@ -1,22 +1,44 @@
# 2.1.2
## Fixes :
* **[Mission Generator]** Fix mission generation issues with radio frequencies (Thanks to contributors davidp57 and danalbert)
* **[Mission Generator]** AI should now properly plan flights for Tornados
# 2.1.1
## Features/Improvements :
* **[Other]** Added an installer option (thanks to contributor parithon)
* **[Cheat Menu]** Added possibility to replace destroyed SAM and base defenses units for the player (Click on a SAM site to fix it)
* **[Cheat Menu]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations
* **[Kneeboards]** Generate mission kneeboards for player flights. Kneeboards include
airfield/carrier information (ATC frequencies, ILS, TACAN, and runway
assignments), assigned radio channels, waypoint lists, and AWACS/JTAC/tanker
information. (Thanks to contributor danalbert)
* **[Radios]** Allocate separate intra-flight channels for most aircraft to reduce global
chatter. (Thanks to contributor danalbert)
* **[Radios]** Configure radio channel presets for most aircraft. Currently supported are:
* AJS37
* AV-8B
* F-14B
* F-16C
* F/A-18C
* JF-17
* M-2000C (Thanks to contributor danalbert)
* **[Base Menu]** Added possibility to repair destroyed SAM and base defenses units for the player (Click on a SAM site to fix it)
* **[Base Menu]** Added possibility to buy/sell/replace SAM units
* **[Map]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations
* **[Units/Factions]** Added F-16C to USA 1990
* **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005
* **[Units/Factions]** Added Mig-21, Mig-23, SA-342L to Syria 2011
* **[Cheat Menu]** Added buttons to remove money
## Fixed issues :
* **[UI/UX]** Spelling issues (Thanks to Github contributor steveveepee)
* **[UI/UX]** Spelling issues (Thanks to contributor steveveepee)
* **[Campaign Generator]** LHA was placed on land in Syrian Civil War campaign
* **[Campaign Generator]** Fixed inverted configuration for Syria full map
* **[Campaign Generator]** Syria "Inherent Resolve" campaign, added Incirlik Air Base
* **[Mission Generator]** AH-1W was not used by AI to generate CAS mission by default
* **[Mission Generator]** Fixed F-16C targeting pod not being added to payload
* **[Mission Generator]** AH-64A and AH-64D payloads fix.
* **[Units/Factions]** China will use KJ-2000 as awacs instead of A-50
# 2.1.0

View File

@@ -1,5 +1,5 @@
import inspect
from pydcs import dcs
import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa']

View File

@@ -223,13 +223,16 @@ PRICES = {
KC130: 25,
A_50: 50,
KJ_2000: 50,
E_3A: 50,
C_130: 25,
# WW2
P_51D_30_NA: 18,
P_51D: 16,
P_47D_30: 18,
P_47D_30: 17,
P_47D_30bl1: 16,
P_47D_40: 18,
B_17G: 30,
# Drones
@@ -337,15 +340,15 @@ PRICES = {
AirDefence.SAM_SA_11_Buk_LN_9A310M1: 30,
AirDefence.SAM_SA_8_Osa_9A33: 28,
AirDefence.SAM_SA_15_Tor_9A331: 40,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 24,
AirDefence.SAM_SA_9_Strela_1_9P31: 16,
AirDefence.SAM_SA_13_Strela_10M3_9A35M3: 16,
AirDefence.SAM_SA_9_Strela_1_9P31: 12,
AirDefence.SAM_SA_11_Buk_CC_9S470M1: 25,
AirDefence.SAM_SA_8_Osa_LD_9T217: 22,
AirDefence.SAM_Patriot_AMG_AN_MRC_137: 35,
AirDefence.SAM_Patriot_ECS_AN_MSQ_104: 30,
AirDefence.SPAAA_Gepard: 24,
AirDefence.SAM_Hawk_PCP: 14,
AirDefence.AAA_Vulcan_M163: 12,
AirDefence.AAA_Vulcan_M163: 10,
AirDefence.SAM_Hawk_LN_M192: 8,
AirDefence.SAM_Chaparral_M48: 16,
AirDefence.SAM_Linebacker_M6: 18,
@@ -358,7 +361,7 @@ PRICES = {
AirDefence.Stinger_MANPADS: 6,
AirDefence.SAM_Stinger_comm_dsr: 4,
AirDefence.SAM_Stinger_comm: 4,
AirDefence.SPAAA_ZSU_23_4_Shilka: 12,
AirDefence.SPAAA_ZSU_23_4_Shilka: 10,
AirDefence.AAA_ZU_23_Closed: 6,
AirDefence.AAA_ZU_23_Emplacement: 6,
AirDefence.AAA_ZU_23_on_Ural_375: 8,
@@ -387,19 +390,19 @@ PRICES = {
AirDefence.SAM_SA_2_LN_SM_90: 8,
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song: 12,
AirDefence.Rapier_FSA_Launcher: 6,
AirDefence.Rapier_FSA_Optical_Tracker: 12,
AirDefence.Rapier_FSA_Blindfire_Tracker: 16,
AirDefence.Rapier_FSA_Optical_Tracker: 6,
AirDefence.Rapier_FSA_Blindfire_Tracker: 8,
AirDefence.HQ_7_Self_Propelled_LN: 20,
AirDefence.HQ_7_Self_Propelled_STR: 24,
AirDefence.AAA_8_8cm_Flak_18: 6,
AirDefence.AAA_Flak_38: 6,
AirDefence.AAA_8_8cm_Flak_36: 8,
AirDefence.AAA_8_8cm_Flak_37: 10,
AirDefence.AAA_8_8cm_Flak_37: 9,
AirDefence.AAA_Flak_Vierling_38:6,
AirDefence.AAA_Kdo_G_40: 8,
AirDefence.Flak_Searchlight_37: 4,
AirDefence.Maschinensatz_33: 10,
AirDefence.AAA_8_8cm_Flak_41: 12,
AirDefence.AAA_8_8cm_Flak_41: 10,
AirDefence.AAA_Bofors_40mm: 8,
# FRENCH PACK MOD
@@ -519,6 +522,8 @@ UNIT_BY_TASK = {
MiG_27K,
A_20G,
P_47D_30,
P_47D_30bl1,
P_47D_40,
Ju_88A4,
B_17G,
MB_339PAN,
@@ -542,7 +547,7 @@ UNIT_BY_TASK = {
KC130,
S_3B_Tanker,
],
AWACS: [E_3A, A_50, ],
AWACS: [E_3A, A_50, KJ_2000],
PinpointStrike: [
Armor.APC_MTLB,
Armor.APC_MTLB,
@@ -993,6 +998,8 @@ PLANE_PAYLOAD_OVERRIDES = {
Su_17M4: COMMON_OVERRIDE,
F_4E: COMMON_OVERRIDE,
P_47D_30:COMMON_OVERRIDE,
P_47D_30bl1:COMMON_OVERRIDE,
P_47D_40:COMMON_OVERRIDE,
B_17G: COMMON_OVERRIDE,
P_51D: COMMON_OVERRIDE,
P_51D_30_NA: COMMON_OVERRIDE,

View File

@@ -267,7 +267,7 @@ class Event:
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESIVE, 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

View File

@@ -20,7 +20,7 @@ China_2010 = {
An_30M,
Yak_40,
A_50,
KJ_2000,
Mi_8MT,
Mi_28N,

View File

@@ -11,7 +11,7 @@ Sweden_1990 = {
UH_1H,
AirDefence.SAM_Hawk_LN_M192,
AirDefence.SAM_Hawk_PCP,
Armor.IFV_MCV_80, # Standing as Strf 90
Armor.MBT_Leopard_2,

View File

@@ -36,6 +36,4 @@ class FrontlineAttackOperation(Operation):
def generate(self):
self.briefinggen.title = "Frontline CAS"
self.briefinggen.description = "Provide CAS for the ground forces attacking enemy lines. Operation will be considered successful if total number of enemy units will be lower than your own by a factor of 1.5 (i.e. with 12 units from both sides, enemy forces need to be reduced to at least 8), meaning that you (and, probably, your wingmans) should concentrate on destroying the enemy units. Target base strength will be lowered as a result. Be advised that your flight will not attack anything until you explicitly tell them so by comms menu."
self.briefinggen.append_waypoint("CAS AREA IP")
self.briefinggen.append_waypoint("CAS AREA EGRESS")
super(FrontlineAttackOperation, self).generate()

View File

@@ -1,12 +1,15 @@
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.terrain import Terrain
from typing import Set
from gen import *
from gen.airfields import AIRFIELD_DATA
from gen.beacons import load_beacons_for_terrain
from gen.radios import RadioRegistry
from gen.tacan import TacanRegistry
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.terrain.terrain import Terrain
from userdata.debriefing import *
TANKER_CALLSIGNS = ["Texaco", "Arco", "Shell"]
class Operation:
attackers_starting_position = None # type: db.StartingPosition
@@ -25,6 +28,8 @@ class Operation:
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None
environment_settings = None
trigger_radius = TRIGGER_RADIUS_MEDIUM
@@ -63,13 +68,25 @@ class Operation:
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings, self.game)
self.airsupportgen = AirSupportConflictGenerator(mission, conflict, self.game)
self.radio_registry = RadioRegistry()
self.tacan_registry = TacanRegistry()
self.airgen = AircraftConflictGenerator(
mission, conflict, self.game.settings, self.game,
self.radio_registry)
self.airsupportgen = AirSupportConflictGenerator(
mission, conflict, self.game, self.radio_registry,
self.tacan_registry)
self.triggersgen = TriggersGenerator(mission, conflict, self.game)
self.visualgen = VisualGenerator(mission, conflict, self.game)
self.envgen = EnviromentGenerator(mission, conflict, self.game)
self.forcedoptionsgen = ForcedOptionsGenerator(mission, conflict, self.game)
self.groundobjectgen = GroundObjectsGenerator(mission, conflict, self.game)
self.groundobjectgen = GroundObjectsGenerator(
mission,
conflict,
self.game,
self.radio_registry,
self.tacan_registry
)
self.briefinggen = BriefingGenerator(mission, conflict, self.game)
def prepare(self, terrain: Terrain, is_quick: bool):
@@ -110,6 +127,30 @@ class Operation:
self.defenders_starting_position = self.to_cp.at
def generate(self):
# Dedup beacon/radio frequencies, since some maps have some frequencies
# used multiple times.
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
unique_map_frequencies: Set[RadioFrequency] = set()
for beacon in beacons:
unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan:
if beacon.channel is None:
logging.error(
f"TACAN beacon has no channel: {beacon.callsign}")
else:
self.tacan_registry.reserve(beacon.tacan_channel)
for airfield, data in AIRFIELD_DATA.items():
if data.theater == self.game.theater.terrain.name:
unique_map_frequencies.add(data.atc.hf)
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
for frequency in unique_map_frequencies:
self.radio_registry.reserve(frequency)
# Generate meteo
if self.environment_settings is None:
@@ -151,10 +192,15 @@ class Operation:
else:
country = self.current_mission.country(self.game.enemy_country)
if cp.id in self.game.planners.keys():
self.airgen.generate_flights(cp, country, self.game.planners[cp.id])
self.airgen.generate_flights(
cp,
country,
self.game.planners[cp.id],
self.groundobjectgen.runways
)
# Generate ground units on frontline everywhere
self.game.jtacs = []
jtacs: List[JtacInfo] = []
for player_cp, enemy_cp in self.game.theater.conflicts(True):
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
self.current_mission.country(self.attacker_country),
@@ -165,6 +211,7 @@ class Operation:
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id])
groundConflictGen.generate()
jtacs.extend(groundConflictGen.jtacs)
# Setup combined arms parameters
self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0
@@ -187,30 +234,29 @@ class Operation:
if self.game.settings.perf_smoke_gen:
self.visualgen.generate()
# Inject Lua Scripts
load_mist = TriggerStart(comment="Load Mist Lua Framework")
with open("./resources/scripts/mist_4_3_74.lua") as f:
load_mist.add_action(DoScript(String(f.read())))
self.current_mission.triggerrules.triggers.append(load_mist)
# Inject Plugins Lua Scripts
listOfPluginsScripts = []
try:
with open("./resources/scripts/plugins/__plugins.lst", "r") as a_file:
for line in a_file:
name = line.strip()
if not name.startswith( '#' ):
trigger = TriggerStart(comment="Load " + name)
listOfPluginsScripts.append(name)
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/plugins/" + name)
trigger.add_action(DoScriptFile(fileref))
self.current_mission.triggerrules.triggers.append(trigger)
except Exception as e:
print(e)
# Load Ciribob's JTACAutoLase script
load_autolase = TriggerStart(comment="Load JTAC script")
with open("./resources/scripts/JTACAutoLase.lua") as f:
script = f.read()
script = script + "\n"
smoke = "true"
if hasattr(self.game.settings, "jtac_smoke_on"):
if not self.game.settings.jtac_smoke_on:
smoke = "false"
for jtac in self.game.jtacs:
script = script + "\n" + "JTACAutoLase('" + str(jtac[2]) + "', " + str(jtac[1]) + ", " + smoke + ", \"vehicle\")" + "\n"
load_autolase.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_autolase)
# Inject Mist Script if not done already in the plugins
if not "mist.lua" in listOfPluginsScripts and not "mist_4_3_74.lua" in listOfPluginsScripts: # don't load mist twice
trigger = TriggerStart(comment="Load Mist Lua Framework")
fileref = self.current_mission.map_resource.add_resource_file("./resources/scripts/mist_4_3_74.lua")
trigger.add_action(DoScriptFile(fileref))
self.current_mission.triggerrules.triggers.append(trigger)
# Inject Liberation script
load_dcs_libe = TriggerStart(comment="Load DCS Liberation Script")
with open("./resources/scripts/dcs_liberation.lua") as f:
script = f.read()
@@ -221,17 +267,68 @@ class Operation:
load_dcs_libe.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_dcs_libe)
# Briefing Generation
for i, tanker_type in enumerate(self.airsupportgen.generated_tankers):
self.briefinggen.append_frequency("Tanker {} ({})".format(TANKER_CALLSIGNS[i], tanker_type), "{}X/{} MHz AM".format(60+i, 130+i))
# Load Ciribob's JTACAutoLase script if not done already in the plugins
if not "JTACAutoLase.lua" in listOfPluginsScripts: # don't load JTACAutoLase twice
load_autolase = TriggerStart(comment="Load JTAC script")
with open("./resources/scripts/JTACAutoLase.lua") as f:
script = f.read()
script = script + "\n"
smoke = "true"
if hasattr(self.game.settings, "jtac_smoke_on"):
if not self.game.settings.jtac_smoke_on:
smoke = "false"
for jtac in jtacs:
script += f"\nJTACAutoLase('{jtac.unit_name}', {jtac.code}, {smoke}, 'vehicle')\n"
load_autolase.add_action(DoScript(String(script)))
self.current_mission.triggerrules.triggers.append(load_autolase)
self.assign_channels_to_flights()
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in self.groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in self.airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
if self.is_awacs_enabled:
self.briefinggen.append_frequency("AWACS", "233 MHz AM")
for awacs in self.airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
self.briefinggen.append_frequency("Flight", "251 MHz AM")
for jtac in jtacs:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
for flight in self.airgen.flights:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
# Generate the briefing
self.briefinggen.generate()
kneeboard_generator.generate()
def assign_channels_to_flights(self) -> None:
"""Assigns preset radio channels for client flights."""
for flight in self.airgen.flights:
if not flight.client_units:
continue
self.assign_channels_to_flight(flight)
def assign_channels_to_flight(self, flight: FlightData) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, self.airsupportgen.air_support)

View File

@@ -9,6 +9,7 @@ from .environmentgen import *
from .groundobjectsgen import *
from .briefinggen import *
from .forcedoptionsgen import *
from .kneeboard import *
from . import naming

View File

@@ -1,14 +1,28 @@
from dcs.action import ActivateGroup, AITaskPush, MessageToCoalition, MessageToAll
from dataclasses import dataclass
from typing import Type
from dcs import helicopters
from dcs.action import ActivateGroup, AITaskPush, MessageToAll
from dcs.condition import TimeAfter, CoalitionHasAirdrome, PartOfCoalitionInZone
from dcs.helicopters import UH_1H
from dcs.terrain.terrain import NoParkingSlotError
from dcs.flyingunit import FlyingUnit
from dcs.helicopters import helicopter_map, UH_1H
from dcs.terrain.terrain import Airport, NoParkingSlotError
from dcs.triggers import TriggerOnce, Event
from game.data.cap_capabilities_db import GUNFIGHTERS
from game.settings import Settings
from game.utils import nm_to_meter
from gen.airfields import RunwayData
from gen.airsupportgen import AirSupport
from gen.callsigns import create_group_callsign_from_unit
from gen.flights.ai_flight_planner import FlightPlanner
from gen.flights.flight import Flight, FlightType, FlightWaypointType
from gen.flights.flight import (
Flight,
FlightType,
FlightWaypoint,
FlightWaypointType,
)
from gen.radios import get_radio, MHz, Radio, RadioFrequency, RadioRegistry
from .conflictgen import *
from .naming import *
@@ -23,22 +37,471 @@ RTB_ALTITUDE = 800
RTB_DISTANCE = 5000
HELI_ALT = 500
# Note that fallback radio channels will *not* be reserved. It's possible that
# flights using these will overlap with other channels. This is because we would
# need to make sure we fell back to a frequency that is not used by any beacon
# or ATC, which we don't have the information to predict. Deal with the minor
# annoyance for now since we'll be fleshing out radio info soon enough.
ALLIES_WW2_CHANNEL = MHz(124)
GERMAN_WW2_CHANNEL = MHz(40)
HELICOPTER_CHANNEL = MHz(127)
UHF_FALLBACK_CHANNEL = MHz(251)
# TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
if unit_type in helicopter_map.values() and unit_type != UH_1H:
return HELICOPTER_CHANNEL
german_ww2_aircraft = [
Bf_109K_4,
FW_190A8,
FW_190D9,
Ju_88A4,
]
if unit_type in german_ww2_aircraft:
return GERMAN_WW2_CHANNEL
allied_ww2_aircraft = [
I_16,
P_47D_30,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
]
if unit_type in allied_ww2_aircraft:
return ALLIES_WW2_CHANNEL
return UHF_FALLBACK_CHANNEL
class ChannelNamer:
"""Base class allowing channel name customization per-aircraft.
Most aircraft will want to customize this behavior, but the default is
reasonable for any aircraft with numbered radios.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
"""Returns the name of the channel for the given radio and channel."""
return f"COMM{radio_id} Ch {channel_id}"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["V/UHF", "UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
class TomcatChannelNamer(ChannelNamer):
"""Channel namer for the F-14."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
radio_name = ["UHF", "VHF/UHF"][radio_id - 1]
return f"{radio_name} Ch {channel_id}"
class ViggenChannelNamer(ChannelNamer):
"""Channel namer for the AJS37."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id >= 4:
channel_letter = "EFGH"[channel_id - 4]
return f"FR 24 {channel_letter}"
return f"FR 22 Special {channel_id}"
class ViperChannelNamer(ChannelNamer):
"""Channel namer for the F-16."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM{radio_id} Ch {channel_id}"
class SCR522ChannelNamer(ChannelNamer):
"""
Channel namer for P-51 & P-47D
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
if channel_id > 3:
return "?"
else:
return f"Button " + "ABCD"[channel_id - 1]
@dataclass(frozen=True)
class ChannelAssignment:
radio_id: int
channel: int
@dataclass
class FlightData:
"""Details of a planned flight."""
flight_type: FlightType
#: All units in the flight.
units: List[FlyingUnit]
#: Total number of aircraft in the flight.
size: int
#: True if this flight belongs to the player's coalition.
friendly: bool
#: Number of minutes after mission start the flight is set to depart.
departure_delay: int
#: Arrival airport.
arrival: RunwayData
#: Departure airport.
departure: RunwayData
#: Diver airport.
divert: Optional[RunwayData]
#: Waypoints of the flight plan.
waypoints: List[FlightWaypoint]
#: Radio frequency for intra-flight communications.
intra_flight_channel: RadioFrequency
#: Map of radio frequencies to their assigned radio and channel, if any.
frequency_to_channel_map: Dict[RadioFrequency, ChannelAssignment]
def __init__(self, flight_type: FlightType, units: List[FlyingUnit],
size: int, friendly: bool, departure_delay: int,
departure: RunwayData, arrival: RunwayData,
divert: Optional[RunwayData], waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency) -> None:
self.flight_type = flight_type
self.units = units
self.size = size
self.friendly = friendly
self.departure_delay = departure_delay
self.departure = departure
self.arrival = arrival
self.divert = divert
self.waypoints = waypoints
self.intra_flight_channel = intra_flight_channel
self.frequency_to_channel_map = {}
self.callsign = create_group_callsign_from_unit(self.units[0])
@property
def client_units(self) -> List[FlyingUnit]:
"""List of playable units in the flight."""
return [u for u in self.units if u.is_human()]
@property
def aircraft_type(self) -> FlyingType:
"""Returns the type of aircraft in this flight."""
return self.units[0].unit_type
def num_radio_channels(self, radio_id: int) -> int:
"""Returns the number of preset channels for the given radio."""
# Note: pydcs only initializes the radio presets for client slots.
return self.client_units[0].num_radio_channels(radio_id)
def channel_for(
self, frequency: RadioFrequency) -> Optional[ChannelAssignment]:
"""Returns the radio and channel number for the given frequency."""
return self.frequency_to_channel_map.get(frequency, None)
def assign_channel(self, radio_id: int, channel_id: int,
frequency: RadioFrequency) -> None:
"""Assigns a preset radio channel to the given frequency."""
for unit in self.client_units:
unit.set_radio_channel_preset(radio_id, channel_id, frequency.mhz)
# One frequency could be bound to multiple channels. Prefer the first,
# since with the current implementation it will be the lowest numbered
# channel.
if frequency not in self.frequency_to_channel_map:
self.frequency_to_channel_map[frequency] = ChannelAssignment(
radio_id, channel_id
)
class RadioChannelAllocator:
"""Base class for radio channel allocators."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
"""Assigns mission frequencies to preset channels for the flight."""
raise NotImplementedError
@dataclass(frozen=True)
class CommonRadioChannelAllocator(RadioChannelAllocator):
"""Radio channel allocator suitable for most aircraft.
Most of the aircraft with preset channels available have one or more radios
with 20 or more channels available (typically per-radio, but this is not the
case for the JF-17).
"""
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
inter_flight_radio_index: Optional[int]
#: Index of the radio used for intra-flight communications. Matches the
#: index of the panel_radio field of the pydcs.dcs.planes object.
intra_flight_radio_index: Optional[int]
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
flight.assign_channel(
self.intra_flight_radio_index, 1, flight.intra_flight_channel)
# For cases where the inter-flight and intra-flight radios share presets
# (the JF-17 only has one set of channels, even though it can use two
# channels simultaneously), start assigning inter-flight channels at 2.
radio_id = self.inter_flight_radio_index
if self.intra_flight_radio_index == radio_id:
first_channel = 2
else:
first_channel = 1
last_channel = flight.num_radio_channels(radio_id)
channel_alloc = iter(range(first_channel, last_channel + 1))
flight.assign_channel(radio_id, next(channel_alloc), flight.departure.atc)
# TODO: If there ever are multiple AWACS, limit to mission relevant.
for awacs in air_support.awacs:
flight.assign_channel(radio_id, next(channel_alloc), awacs.freq)
if flight.arrival != flight.departure:
flight.assign_channel(radio_id, next(channel_alloc),
flight.arrival.atc)
try:
# TODO: Skip incompatible tankers.
for tanker in air_support.tankers:
flight.assign_channel(
radio_id, next(channel_alloc), tanker.freq)
if flight.divert is not None:
flight.assign_channel(radio_id, next(channel_alloc),
flight.divert.atc)
except StopIteration:
# Any remaining channels are nice-to-haves, but not necessary for
# the few aircraft with a small number of channels available.
pass
@dataclass(frozen=True)
class WarthogRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the A-10C."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
# The A-10's radio works differently than most aircraft. Doesn't seem to
# be a way to set these from the mission editor, let alone pydcs.
pass
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
# The Viggen's preset channels are handled differently from other
# aircraft. The aircraft automatically configures channels for every
# allied flight in the game (including AWACS) and for every airfield. As
# such, we don't need to allocate any of those. There are seven presets
# we can modify, however: three channels for the main radio intended for
# communication with wingmen, and four emergency channels for the backup
# radio. We'll set the first channel of the main radio to the
# intra-flight channel, and the first three emergency channels to each
# of the flight plan's airfields. The fourth emergency channel is always
# the guard channel.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
flight.assign_channel(radio_id, 4, flight.departure.atc)
flight.assign_channel(radio_id, 5, flight.arrival.atc)
# TODO: Assign divert to 6 when we support divert airfields.
@dataclass(frozen=True)
class SCR522RadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the SCR522 WW2 radios. (4 channels)"""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
flight.assign_channel(radio_id, 2, flight.departure.atc)
flight.assign_channel(radio_id, 3, flight.arrival.atc)
# TODO : Some GCI on Channel 4 ?
@dataclass(frozen=True)
class AircraftData:
"""Additional aircraft data not exposed by pydcs."""
#: The type of radio used for inter-flight communications.
inter_flight_radio: Radio
#: The type of radio used for intra-flight communications.
intra_flight_radio: Radio
#: The radio preset channel allocator, if the aircraft supports channel
#: presets. If the aircraft does not support preset channels, this will be
#: None.
channel_allocator: Optional[RadioChannelAllocator]
#: Defines how channels should be named when printed in the kneeboard.
channel_namer: Type[ChannelNamer] = ChannelNamer
# Indexed by the id field of the pydcs PlaneType.
AIRCRAFT_DATA: Dict[str, AircraftData] = {
"A-10C": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
intra_flight_radio=get_radio("AN/ARC-164"), # VHF for intraflight is not accepted anymore by DCS (see https://forums.eagle.ru/showthread.php?p=4499738)
channel_allocator=WarthogRadioChannelAllocator()
),
"AJS37": AircraftData(
# The AJS37 has somewhat unique radio configuration. Two backup radio
# (FR 24) can only operate simultaneously with the main radio in guard
# mode. As such, we only use the main radio for both inter- and intra-
# flight communication.
inter_flight_radio=get_radio("FR 22"),
intra_flight_radio=get_radio("FR 22"),
channel_allocator=ViggenRadioChannelAllocator(),
channel_namer=ViggenChannelNamer
),
"AV8BNA": AircraftData(
inter_flight_radio=get_radio("AN/ARC-210"),
intra_flight_radio=get_radio("AN/ARC-210"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=2,
intra_flight_radio_index=1
)
),
"F-14B": AircraftData(
inter_flight_radio=get_radio("AN/ARC-159"),
intra_flight_radio=get_radio("AN/ARC-182"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
channel_namer=TomcatChannelNamer
),
"F-16C_50": AircraftData(
inter_flight_radio=get_radio("AN/ARC-164"),
intra_flight_radio=get_radio("AN/ARC-222"),
# COM2 is the AN/ARC-222, which is the VHF radio we want to use for
# intra-flight communication to leave COM1 open for UHF inter-flight.
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
channel_namer=ViperChannelNamer
),
"FA-18C_hornet": AircraftData(
inter_flight_radio=get_radio("AN/ARC-210"),
intra_flight_radio=get_radio("AN/ARC-210"),
# DCS will clobber channel 1 of the first radio compatible with the
# flight's assigned frequency. Since the F/A-18's two radios are both
# AN/ARC-210s, radio 1 will be compatible regardless of which frequency
# is assigned, so we must use radio 1 for the intra-flight radio.
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=2,
intra_flight_radio_index=1
)
),
"JF-17": AircraftData(
inter_flight_radio=get_radio("R&S M3AR UHF"),
intra_flight_radio=get_radio("R&S M3AR VHF"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=1
),
# Same naming pattern as the Viper, so just reuse that.
channel_namer=ViperChannelNamer
),
"M-2000C": AircraftData(
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=2
),
channel_namer=MirageChannelNamer
),
"P-51D": AircraftData(
inter_flight_radio=get_radio("SCR522"),
intra_flight_radio=get_radio("SCR522"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=1
),
channel_namer=SCR522ChannelNamer
),
}
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator:
escort_targets = [] # type: typing.List[typing.Tuple[FlyingGroup, int]]
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings, game):
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game, radio_registry: RadioRegistry):
self.m = mission
self.game = game
self.settings = settings
self.conflict = conflict
self.radio_registry = radio_registry
self.escort_targets = []
self.flights: List[FlightData] = []
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
"""Allocates an intra-flight channel to a group.
Args:
airframe: The type of aircraft a channel should be allocated for.
Returns:
The frequency of the intra-flight channel.
"""
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
return self.radio_registry.alloc_for_radio(
aircraft_data.intra_flight_radio)
except KeyError:
return get_fallback_channel(airframe)
def _start_type(self) -> StartType:
return self.settings.cold_start and StartType.Cold or StartType.Warm
def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task], flight: Flight):
def _setup_group(self, group: FlyingGroup, for_task: typing.Type[Task],
flight: Flight, dynamic_runways: Dict[str, RunwayData]):
did_load_loadout = False
unit_type = group.units[0].unit_type
@@ -74,10 +537,11 @@ class AircraftConflictGenerator:
single_client = flight.client_count == 1
for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
if single_client:
group.units[idx].set_player()
unit.set_player()
else:
group.units[idx].set_client()
unit.set_client()
# Do not generate player group with late activation.
if group.late_activation:
@@ -85,24 +549,42 @@ class AircraftConflictGenerator:
# Set up F-14 Client to have pre-stored alignement
if unit_type is F_14B:
group.units[idx].set_property(F_14B.Properties.INSAlignmentStored.id, True)
unit.set_property(F_14B.Properties.INSAlignmentStored.id, True)
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
# TODO : refactor this following bad specific special case code :(
channel = self.get_intra_flight_channel(unit_type)
group.set_frequency(channel.mhz)
if unit_type in helicopters.helicopter_map.values() and unit_type not in [UH_1H]:
group.set_frequency(127.5)
# TODO: Support for different departure/arrival airfields.
cp = flight.from_cp
fallback_runway = RunwayData(cp.full_name, runway_name="")
if cp.cptype == ControlPointType.AIRBASE:
departure_runway = self.get_preferred_runway(flight.from_cp.airport)
elif cp.is_fleet:
departure_runway = dynamic_runways.get(cp.name, fallback_runway)
else:
if unit_type not in [P_51D_30_NA, P_51D, SpitfireLFMkIX, SpitfireLFMkIXCW, P_47D_30, I_16, FW_190A8, FW_190D9, Bf_109K_4]:
group.set_frequency(251.0)
else:
# WW2
if unit_type in [FW_190A8, FW_190D9, Bf_109K_4, Ju_88A4]:
group.set_frequency(40)
else:
group.set_frequency(124.0)
logging.warning(f"Unhandled departure control point: {cp.cptype}")
departure_runway = fallback_runway
# The first waypoint is set automatically by pydcs, so it's not in our
# list. Convert the pydcs MovingPoint to a FlightWaypoint so it shows up
# in our FlightData.
first_point = FlightWaypoint.from_pydcs(group.points[0], flight.from_cp)
self.flights.append(FlightData(
flight_type=flight.flight_type,
units=group.units,
size=len(group.units),
friendly=flight.from_cp.captured,
departure_delay=flight.scheduled_in,
departure=departure_runway,
arrival=departure_runway,
# TODO: Support for divert airfields.
divert=None,
waypoints=[first_point] + flight.points,
intra_flight_channel=channel
))
# Special case so Su 33 carrier take off
if unit_type is Su_33:
@@ -113,6 +595,21 @@ class AircraftConflictGenerator:
for unit in group.units:
unit.fuel = Su_33.fuel_max * 0.8
def get_preferred_runway(self, airport: Airport) -> RunwayData:
"""Returns the preferred runway for the given airport.
Right now we're only selecting runways based on whether or not they have
ILS, but we could also choose based on wind conditions, or which
direction flight plans should follow.
"""
runways = list(RunwayData.for_pydcs_airport(airport))
for runway in runways:
# Prefer any runway with ILS.
if runway.ils is not None:
return runway
# Otherwise we lack the mission information to pick more usefully,
# so just use the first runway.
return runways[0]
def _generate_at_airport(self, name: str, side: Country, unit_type: FlyingType, count: int, client_count: int, airport: Airport = None, start_type = None) -> FlyingGroup:
assert count > 0
@@ -197,6 +694,7 @@ class AircraftConflictGenerator:
try:
return self._generate_at_airport(name, side, unit_type, count, client_count, at)
except NoParkingSlotError:
logging.info("No parking slot found at " + at.name + ", switching to air start.")
pass
return self._generate_inflight(name, side, unit_type, count, client_count, at.position)
else:
@@ -252,21 +750,28 @@ class AircraftConflictGenerator:
logging.warning("Pylon not found ! => Pylon" + key + " on " + str(flight.unit_type))
def generate_flights(self, cp, country, flight_planner:FlightPlanner):
def generate_flights(self, cp, country, flight_planner: FlightPlanner,
dynamic_runways: Dict[str, RunwayData]):
# Clear pydcs parking slots
if cp.airport is not None:
for ps in cp.airport.parking_slots:
ps.unit_id = None
logging.info("CLEARING SLOTS @ " + cp.airport.name)
logging.info("===============")
if cp.airport is not None:
for ps in cp.airport.parking_slots:
logging.info("SLOT : " + str(ps.unit_id))
ps.unit_id = None
logging.info("----------------")
logging.info("===============")
for flight in flight_planner.flights:
if flight.client_count == 0 and self.game.position_culled(flight.from_cp.position):
logging.info("Flight not generated : culled")
continue
logging.info("Generating flight : " + str(flight.unit_type))
group = self.generate_planned_flight(cp, country, flight)
self.setup_flight_group(group, flight, flight.flight_type)
self.setup_flight_group(group, flight, flight.flight_type,
dynamic_runways)
self.setup_group_activation_trigger(flight, group)
@@ -324,7 +829,7 @@ class AircraftConflictGenerator:
def generate_planned_flight(self, cp, country, flight:Flight):
try:
if flight.client_count == 0 and self.game.settings.perf_ai_parking_start:
flight.start_type = "Warm"
flight.start_type = "Cold"
if flight.start_type == "In Flight":
group = self._generate_group(
@@ -360,8 +865,10 @@ class AircraftConflictGenerator:
client_count=0,
airport=cp.airport,
start_type=st)
except Exception:
except Exception as e:
# Generated when there is no place on Runway or on Parking Slots
logging.error(e)
logging.warning("No room on runway or parking slots. Starting from the air.")
flight.start_type = "In Flight"
group = self._generate_group(
name=namegen.next_unit_name(country, cp.id, flight.unit_type),
@@ -375,19 +882,13 @@ class AircraftConflictGenerator:
flight.group = group
return group
def setup_group_as_intercept_flight(self, group, flight):
group.points[0].ETA = 0
group.late_activation = True
self._setup_group(group, Intercept, flight)
for point in flight.points:
group.add_waypoint(Point(point.x,point.y), point.alt)
def setup_flight_group(self, group, flight, flight_type):
def setup_flight_group(self, group, flight, flight_type,
dynamic_runways: Dict[str, RunwayData]):
if flight_type in [FlightType.CAP, FlightType.BARCAP, FlightType.TARCAP, FlightType.INTERCEPTION]:
group.task = CAP.name
self._setup_group(group, CAP, flight)
self._setup_group(group, CAP, flight, dynamic_runways)
# group.points[0].tasks.clear()
group.points[0].tasks.clear()
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(50), targets=[Targets.All.Air]))
@@ -399,7 +900,7 @@ class AircraftConflictGenerator:
elif flight_type in [FlightType.CAS, FlightType.BAI]:
group.task = CAS.name
self._setup_group(group, CAS, flight)
self._setup_group(group, CAS, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(EngageTargets(max_distance=nm_to_meter(10), targets=[Targets.All.GroundUnits.GroundVehicles]))
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
@@ -408,7 +909,7 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
group.task = SEAD.name
self._setup_group(group, SEAD, flight)
self._setup_group(group, SEAD, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(NoTask())
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
@@ -417,14 +918,14 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(OptRTBOnOutOfAmmo.Values.ASM))
elif flight_type in [FlightType.STRIKE]:
group.task = PinpointStrike.name
self._setup_group(group, GroundAttack, flight)
self._setup_group(group, GroundAttack, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
group.points[0].tasks.append(OptRestrictJettison(True))
elif flight_type in [FlightType.ANTISHIP]:
group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight)
self._setup_group(group, AntishipStrike, flight, dynamic_runways)
group.points[0].tasks.clear()
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFire))
@@ -503,23 +1004,3 @@ class AircraftConflictGenerator:
pt.name = String(point.name)
self._setup_custom_payload(flight, group)
def setup_group_as_antiship_flight(self, group, flight):
group.task = AntishipStrike.name
self._setup_group(group, AntishipStrike, flight)
group.points[0].tasks.clear()
group.points[0].tasks.append(AntishipStrikeTaskAction())
group.points[0].tasks.append(OptReactOnThreat(OptReactOnThreat.Values.EvadeFire))
group.points[0].tasks.append(OptROE(OptROE.Values.OpenFireWeaponFree))
group.points[0].tasks.append(OptRestrictJettison(True))
for point in flight.points:
group.add_waypoint(Point(point.x, point.y), point.alt)
def setup_radio_preset(self, flight, group):
pass

1561
gen/airfields.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,10 @@
from game import db
from dataclasses import dataclass, field
from .callsigns import callsign_for_support_unit
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.task import *
from dcs.terrain.terrain import NoParkingSlotError
from .radios import RadioFrequency, RadioRegistry
from .tacan import TacanBand, TacanChannel, TacanRegistry
TANKER_DISTANCE = 15000
TANKER_ALT = 4572
@@ -16,14 +14,39 @@ AWACS_DISTANCE = 150000
AWACS_ALT = 13000
class AirSupportConflictGenerator:
generated_tankers = None # type: typing.List[str]
@dataclass
class AwacsInfo:
"""AWACS information for the kneeboard."""
callsign: str
freq: RadioFrequency
def __init__(self, mission: Mission, conflict: Conflict, game):
@dataclass
class TankerInfo:
"""Tanker information for the kneeboard."""
callsign: str
variant: str
freq: RadioFrequency
tacan: TacanChannel
@dataclass
class AirSupport:
awacs: List[AwacsInfo] = field(default_factory=list)
tankers: List[TankerInfo] = field(default_factory=list)
class AirSupportConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game,
radio_registry: RadioRegistry,
tacan_registry: TacanRegistry) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
self.generated_tankers = []
self.air_support = AirSupport()
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
@classmethod
def support_tasks(cls) -> typing.Collection[typing.Type[MainTask]]:
@@ -32,34 +55,58 @@ class AirSupportConflictGenerator:
def generate(self, is_awacs_enabled):
player_cp = self.conflict.from_cp if self.conflict.from_cp.captured else self.conflict.to_cp
CALLSIGNS = ["TKR", "TEX", "FUL", "FUE", ""]
fallback_tanker_number = 0
for i, tanker_unit_type in enumerate(db.find_unittype(Refueling, self.conflict.attackers_side)):
self.generated_tankers.append(db.unit_type_name(tanker_unit_type))
variant = db.unit_type_name(tanker_unit_type)
freq = self.radio_registry.alloc_uhf()
tacan = self.tacan_registry.alloc_for_band(TacanBand.Y)
tanker_heading = self.conflict.to_cp.position.heading_between_point(self.conflict.from_cp.position) + TANKER_HEADING_OFFSET * i
tanker_position = player_cp.position.point_from_heading(tanker_heading, TANKER_DISTANCE)
tanker_group = self.mission.refuel_flight(
country=self.mission.country(self.game.player_country),
name=namegen.next_tanker_name(self.mission.country(self.game.player_country)),
name=namegen.next_tanker_name(self.mission.country(self.game.player_country), tanker_unit_type),
airport=None,
plane_type=tanker_unit_type,
position=tanker_position,
altitude=TANKER_ALT,
race_distance=58000,
frequency=130 + i,
frequency=freq.mhz,
start_type=StartType.Warm,
speed=574,
tacanchannel="{}X".format(60 + i),
tacanchannel=str(tacan),
)
callsign = callsign_for_support_unit(tanker_group)
tacan_callsign = {
"Texaco": "TEX",
"Arco": "ARC",
"Shell": "SHL",
}.get(callsign)
if tacan_callsign is None:
# The dict above is all the callsigns currently in the game, but
# non-Western countries don't use the callsigns and instead just
# use numbers. It's possible that none of those nations have
# TACAN compatible refueling aircraft, but fallback just in
# case.
tacan_callsign = f"TK{fallback_tanker_number}"
fallback_tanker_number += 1
if tanker_unit_type != IL_78M:
tanker_group.points[0].tasks.pop() # Override PyDCS tacan channel
tanker_group.points[0].tasks.append(ActivateBeaconCommand(60 + i, "X", CALLSIGNS[i], True, tanker_group.units[0].id, True))
# Override PyDCS tacan channel.
tanker_group.points[0].tasks.pop()
tanker_group.points[0].tasks.append(ActivateBeaconCommand(
tacan.number, tacan.band.value, tacan_callsign, True,
tanker_group.units[0].id, True))
tanker_group.points[0].tasks.append(SetInvisibleCommand(True))
tanker_group.points[0].tasks.append(SetImmortalCommand(True))
self.air_support.tankers.append(TankerInfo(callsign, variant, freq, tacan))
if is_awacs_enabled:
try:
freq = self.radio_registry.alloc_uhf()
awacs_unit = db.find_unittype(AWACS, self.conflict.attackers_side)[0]
awacs_flight = self.mission.awacs_flight(
country=self.mission.country(self.game.player_country),
@@ -68,11 +115,13 @@ class AirSupportConflictGenerator:
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE),
frequency=233,
frequency=freq.mhz,
start_type=StartType.Warm,
)
awacs_flight.points[0].tasks.append(SetInvisibleCommand(True))
awacs_flight.points[0].tasks.append(SetImmortalCommand(True))
except:
print("No AWACS for faction")
self.air_support.awacs.append(AwacsInfo(
callsign_for_support_unit(awacs_flight), freq))
except:
print("No AWACS for faction")

View File

@@ -1,10 +1,12 @@
from dcs.action import AITaskPush, AITaskSet
from dataclasses import dataclass
from dcs.action import AITaskPush
from dcs.condition import TimeAfter, UnitDamaged, Or, GroupLifeLess
from dcs.task import *
from dcs.triggers import TriggerOnce, Event
from gen import namegen
from gen.ground_forces.ai_ground_planner import CombatGroupRole, DISTANCE_FROM_FRONTLINE
from .callsigns import callsign_for_support_unit
from .conflictgen import *
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
@@ -22,6 +24,17 @@ FIGHT_DISTANCE = 3500
RANDOM_OFFSET_ATTACK = 250
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
unit_name: str
callsign: str
region: str
code: str
# TODO: Radio info? Type?
class GroundConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
@@ -30,8 +43,9 @@ class GroundConflictGenerator:
self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance)
self.enemy_stance = random.choice([CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.AGGRESIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESIVE])
self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE])
self.game = game
self.jtacs: List[JtacInfo] = []
def _group_point(self, point) -> Point:
distance = randint(
@@ -100,7 +114,7 @@ class GroundConflictGenerator:
# Add JTAC
if "has_jtac" in self.game.player_faction and self.game.player_faction["has_jtac"] and self.game.settings.include_jtac_if_available:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.game.jtacs)
code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper
if "jtac_unit" in self.game.player_faction:
@@ -115,7 +129,10 @@ class GroundConflictGenerator:
jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
self.game.jtacs.append(("Frontline " + self.conflict.from_cp.name + "/" + self.conflict.to_cp.name, code, n))
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(n, callsign, frontline, str(code)))
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):
@@ -222,7 +239,7 @@ class GroundConflictGenerator:
u.heading = forward_heading + random.randint(-5,5)
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
if stance == CombatStance.AGGRESIVE:
if stance == CombatStance.AGGRESSIVE:
# Attack nearest enemy if any
# Then move forward OR Attack enemy base if it is not too far away
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
@@ -263,7 +280,7 @@ class GroundConflictGenerator:
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
if stance in [CombatStance.AGGRESIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
# APC & ATGM will never move too much forward, but will follow along any offensive
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)

74
gen/beacons.py Normal file
View File

@@ -0,0 +1,74 @@
from dataclasses import dataclass
from enum import auto, IntEnum
import json
from pathlib import Path
from typing import Iterable, Optional
from gen.radios import RadioFrequency
from gen.tacan import TacanBand, TacanChannel
BEACONS_RESOURCE_PATH = Path("resources/dcs/beacons")
class BeaconType(IntEnum):
BEACON_TYPE_NULL = auto()
BEACON_TYPE_VOR = auto()
BEACON_TYPE_DME = auto()
BEACON_TYPE_VOR_DME = auto()
BEACON_TYPE_TACAN = auto()
BEACON_TYPE_VORTAC = auto()
BEACON_TYPE_RSBN = auto()
BEACON_TYPE_BROADCAST_STATION = auto()
BEACON_TYPE_HOMER = auto()
BEACON_TYPE_AIRPORT_HOMER = auto()
BEACON_TYPE_AIRPORT_HOMER_WITH_MARKER = auto()
BEACON_TYPE_ILS_FAR_HOMER = auto()
BEACON_TYPE_ILS_NEAR_HOMER = auto()
BEACON_TYPE_ILS_LOCALIZER = auto()
BEACON_TYPE_ILS_GLIDESLOPE = auto()
BEACON_TYPE_PRMG_LOCALIZER = auto()
BEACON_TYPE_PRMG_GLIDESLOPE = auto()
BEACON_TYPE_ICLS_LOCALIZER = auto()
BEACON_TYPE_ICLS_GLIDESLOPE = auto()
BEACON_TYPE_NAUTICAL_HOMER = auto()
@dataclass(frozen=True)
class Beacon:
name: str
callsign: str
beacon_type: BeaconType
hertz: int
channel: Optional[int]
@property
def frequency(self) -> RadioFrequency:
return RadioFrequency(self.hertz)
@property
def is_tacan(self) -> bool:
return self.beacon_type in (
BeaconType.BEACON_TYPE_VORTAC,
BeaconType.BEACON_TYPE_TACAN,
)
@property
def tacan_channel(self) -> TacanChannel:
assert self.is_tacan
assert self.channel is not None
return TacanChannel(self.channel, TacanBand.X)
def load_beacons_for_terrain(name: str) -> Iterable[Beacon]:
beacons_file = BEACONS_RESOURCE_PATH / f"{name.lower()}.json"
if not beacons_file.exists():
raise RuntimeError(f"Beacon file {beacons_file.resolve()} is missing")
for beacon in json.loads(beacons_file.read_text()):
yield Beacon(**beacon)

View File

@@ -1,68 +1,127 @@
import logging
import os
from collections import defaultdict
from dataclasses import dataclass
import random
from typing import List
from game import db
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.mission import Mission
from .aircraft import FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency
class BriefingGenerator:
freqs = None # type: typing.List[typing.Tuple[str, str]]
title = "" # type: str
description = "" # type: str
targets = None # type: typing.List[typing.Tuple[str, str]]
waypoints = None # type: typing.List[str]
@dataclass
class CommInfo:
"""Communications information for the kneeboard."""
name: str
freq: RadioFrequency
class MissionInfoGenerator:
"""Base type for generators of mission information for the player.
Examples of subtypes include briefing generators, kneeboard generators, etc.
"""
def __init__(self, mission: Mission) -> None:
self.mission = mission
self.awacs: List[AwacsInfo] = []
self.comms: List[CommInfo] = []
self.flights: List[FlightData] = []
self.jtacs: List[JtacInfo] = []
self.tankers: List[TankerInfo] = []
def add_awacs(self, awacs: AwacsInfo) -> None:
"""Adds an AWACS/GCI to the mission.
Args:
awacs: AWACS information.
"""
self.awacs.append(awacs)
def add_comm(self, name: str, freq: RadioFrequency) -> None:
"""Adds communications info to the mission.
Args:
name: Name of the radio channel.
freq: Frequency of the radio channel.
"""
self.comms.append(CommInfo(name, freq))
def add_flight(self, flight: FlightData) -> None:
"""Adds flight info to the mission.
Args:
flight: Flight information.
"""
self.flights.append(flight)
def add_jtac(self, jtac: JtacInfo) -> None:
"""Adds a JTAC to the mission.
Args:
jtac: JTAC information.
"""
self.jtacs.append(jtac)
def add_tanker(self, tanker: TankerInfo) -> None:
"""Adds a tanker to the mission.
Args:
tanker: Tanker information.
"""
self.tankers.append(tanker)
def generate(self) -> None:
"""Generates the mission information."""
raise NotImplementedError
class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, conflict: Conflict, game):
self.m = mission
super().__init__(mission)
self.conflict = conflict
self.game = game
self.title = ""
self.description = ""
self.dynamic_runways: List[RunwayData] = []
self.freqs = []
self.targets = []
self.waypoints = []
def add_dynamic_runway(self, runway: RunwayData) -> None:
"""Adds a dynamically generated runway to the briefing.
self.jtacs = []
Dynamic runways are any valid landing point that is a unit rather than a
map feature. These include carriers, ships with a helipad, and FARPs.
"""
self.dynamic_runways.append(runway)
def append_frequency(self, name: str, frequency: str):
self.freqs.append((name, frequency))
def add_flight_description(self, flight: FlightData):
assert flight.client_units
def append_target(self, description: str, markpoint: str = None):
self.targets.append((description, markpoint))
def append_waypoint(self, description: str):
self.waypoints.append(description)
def add_flight_description(self, flight):
if flight.client_count <= 0:
return
flight_unit_name = db.unit_type_name(flight.unit_type)
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += "-" * 50 + "\n"
self.description += flight_unit_name + " x " + str(flight.count) + 2 * "\n"
self.description += f"{flight_unit_name} x {flight.size + 2}\n\n"
self.description += "#0 -- TAKEOFF : Take off from " + flight.from_cp.name + "\n"
for i, wpt in enumerate(flight.points):
self.description += "#" + str(1+i) + " -- " + wpt.name + " : " + wpt.description + "\n"
self.description += "#" + str(len(flight.points) + 1) + " -- RTB\n\n"
for i, wpt in enumerate(flight.waypoints):
self.description += f"#{i + 1} -- {wpt.name} : {wpt.description}\n"
self.description += f"#{len(flight.waypoints) + 1} -- RTB\n\n"
group = flight.group
if group is not None:
for i, nav_target in enumerate(group.nav_target_points):
self.description += nav_target.text_comment + "\n"
self.description += "\n"
self.description += "-" * 50 + "\n"
def add_ally_flight_description(self, flight):
if flight.client_count == 0:
flight_unit_name = db.unit_type_name(flight.unit_type)
self.description += flight.flight_type.name + " " + flight_unit_name + " x " + str(flight.count) + ", departing in " + str(flight.scheduled_in) + " minutes \n"
def add_ally_flight_description(self, flight: FlightData):
assert not flight.client_units
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += (
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
f"departing in {flight.departure_delay} minutes\n"
)
def generate(self):
self.description = ""
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
@@ -74,52 +133,50 @@ class BriefingGenerator:
self.description += "Your flights:" + "\n"
self.description += "=" * 15 + "\n\n"
for planner in self.game.planners.values():
for flight in planner.flights:
for flight in self.flights:
if flight.client_units:
self.add_flight_description(flight)
self.description += "\n"*2
self.description += "Planned ally flights:" + "\n"
self.description += "=" * 15 + "\n"
for planner in self.game.planners.values():
if planner.from_cp.captured and len(planner.flights) > 0:
self.description += "\nFrom " + planner.from_cp.full_name + " \n"
self.description += "-" * 50 + "\n\n"
for flight in planner.flights:
self.add_ally_flight_description(flight)
allied_flights_by_departure = defaultdict(list)
for flight in self.flights:
if not flight.client_units and flight.friendly:
name = flight.departure.airfield_name
allied_flights_by_departure[name].append(flight)
for departure, flights in allied_flights_by_departure.items():
self.description += f"\nFrom {departure}\n"
self.description += "-" * 50 + "\n\n"
for flight in flights:
self.add_ally_flight_description(flight)
if self.freqs:
if self.comms:
self.description += "\n\nComms Frequencies:\n"
self.description += "=" * 15 + "\n"
for name, freq in self.freqs:
self.description += "{}: {}\n".format(name, freq)
for comm_info in self.comms:
self.description += f"{comm_info.name}: {comm_info.freq}\n"
self.description += ("-" * 50) + "\n"
for cp in self.game.theater.controlpoints:
if cp.captured and cp.cptype in [ControlPointType.LHA_GROUP, ControlPointType.AIRCRAFT_CARRIER_GROUP]:
self.description += cp.name + "\n"
self.description += "RADIO : 127.5 Mhz AM\n"
self.description += "TACAN : "
self.description += str(cp.tacanN)
if cp.tacanY:
self.description += "Y"
else:
self.description += "X"
self.description += " " + str(cp.tacanI) + "\n"
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP and hasattr(cp, "icls"):
self.description += "ICLS Channel : " + str(cp.icls) + "\n"
self.description += "-" * 50 + "\n"
for runway in self.dynamic_runways:
self.description += f"{runway.airfield_name}\n"
self.description += f"RADIO : {runway.atc}\n"
if runway.tacan is not None:
self.description += f"TACAN : {runway.tacan} {runway.tacan_callsign}\n"
if runway.icls is not None:
self.description += f"ICLS Channel : {runway.icls}\n"
self.description += "-" * 50 + "\n"
self.description += "JTACS [F-10 Menu] : \n"
self.description += "===================\n\n"
for jtac in self.game.jtacs:
self.description += str(jtac[0]) + " -- Code : " + str(jtac[1]) + "\n"
for jtac in self.jtacs:
self.description += f"{jtac.region} -- Code : {jtac.code}\n"
self.m.set_description_text(self.description)
self.mission.set_description_text(self.description)
self.m.add_picture_blue(os.path.abspath("./resources/ui/splash_screen.png"))
self.mission.add_picture_blue(os.path.abspath(
"./resources/ui/splash_screen.png"))
def generate_ongoing_war_text(self):
@@ -148,7 +205,7 @@ class BriefingGenerator:
self.description += "We do not have a single vehicle available to hold our position, the situation is critical, and we will lose ground inevitably.\n"
elif enemy_base.base.total_armor == 0:
self.description += "The enemy forces have been crushed, we will be able to make significant progress toward " + enemy_base.name + ". \n"
if stance == CombatStance.AGGRESIVE:
if stance == CombatStance.AGGRESSIVE:
if has_numerical_superiority:
self.description += "On this location, our ground forces will try to make progress against the enemy"
self.description += ". As the enemy is outnumbered, our forces should have no issue making progress.\n"
@@ -180,7 +237,7 @@ class BriefingGenerator:
def __random_frontline_sentence(self, player_base_name, enemy_base_name):
templates = [
"There are combats between {} and {}. ",
"The war on the ground is still going on between {} an {}. ",
"The war on the ground is still going on between {} and {}. ",
"Our ground forces in {} are opposed to enemy forces based in {}. ",
"Our forces from {} are fighting enemies based in {}. ",
"There is an active frontline between {} and {}. ",

34
gen/callsigns.py Normal file
View File

@@ -0,0 +1,34 @@
"""Support for working with DCS group callsigns."""
import logging
import re
from dcs.unitgroup import FlyingGroup
from dcs.flyingunit import FlyingUnit
def callsign_for_support_unit(group: FlyingGroup) -> str:
# Either something like Overlord11 for Western AWACS, or else just a number.
# Convert to either "Overlord" or "Flight 123".
lead = group.units[0]
raw_callsign = lead.callsign_as_str()
try:
return f"Flight {int(raw_callsign)}"
except ValueError:
return raw_callsign.rstrip("1234567890")
def create_group_callsign_from_unit(lead: FlyingUnit) -> str:
raw_callsign = lead.callsign_as_str()
if not lead.callsign_is_western:
# Callsigns for non-Western countries are just a number per flight,
# similar to tail numbers.
return f"Flight {raw_callsign}"
# Callsign from pydcs is in the format `<name><group ID><unit ID>`,
# where unit ID is guaranteed to be a single digit but the group ID may
# be more.
match = re.search(r"^(\D+)(\d+)(\d)$", raw_callsign)
if match is None:
logging.error(f"Could not parse unit callsign: {raw_callsign}")
return f"Flight {raw_callsign}"
return f"{match.group(1)} {match.group(2)}"

View File

@@ -1,7 +1,7 @@
import logging
import typing
import pdb
from pydcs import dcs
import dcs
from random import randint
from dcs import Mission

View File

@@ -3,22 +3,38 @@ import random
from dcs.vehicles import Armor
from game import db
from gen.defenses.armored_group_generator import ArmoredGroupGenerator
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
def generate_armor_group(faction:str, game, ground_object):
"""
This generate a group of ground units
:param parentCp: The parent control point
:param ground_object: The ground object which will own the group
:param country: Owner country
:return: Generated group
"""
possible_unit = [u for u in db.FACTIONS[faction]["units"] if u in Armor.__dict__.values()]
if len(possible_unit) > 0:
unit_type = random.choice(possible_unit)
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
generator.generate()
return generator.get_generated_group()
return generate_armor_group_of_type(game, ground_object, unit_type)
return None
def generate_armor_group_of_type(game, ground_object, unit_type):
"""
This generate a group of ground units of given type
:return: Generated group
"""
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
generator.generate()
return generator.get_generated_group()
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
"""
This generate a group of ground units of given type and size
:return: Generated group
"""
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
generator.generate()
return generator.get_generated_group()

View File

@@ -25,3 +25,20 @@ class ArmoredGroupGenerator(GroupGenerator):
self.position.y + spacing * j, self.heading)
class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size):
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type
self.size = size
def generate(self):
spacing = random.randint(20, 70)
index = 0
for i in range(self.size):
index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y, self.heading)

View File

@@ -372,17 +372,26 @@ class FlightPlanner:
egress_heading = heading - 180 - 25
ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"])
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_STRIKE,
ingress_pos.x,
ingress_pos.y,
self.doctrine["INGRESS_ALT"]
)
ingress_point.pretty_name = "INGRESS on " + location.obj_name
ingress_point.description = "INGRESS on " + location.obj_name
ingress_point.name = "INGRESS"
ingress_point.waypoint_type = FlightWaypointType.INGRESS_STRIKE
flight.points.append(ingress_point)
if len(location.groups) > 0 and location.dcs_identifier == "AA":
for g in location.groups:
for j, u in enumerate(g.units):
point = FlightWaypoint(u.position.x, u.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
u.position.x,
u.position.y,
0
)
point.description = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j)
point.pretty_name = "STRIKE " + "[" + str(location.obj_name) + "] : " + u.type + " #" + str(j)
point.name = location.obj_name + "#" + str(j)
@@ -398,7 +407,12 @@ class FlightPlanner:
if building.is_dead:
continue
point = FlightWaypoint(building.position.x, building.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
building.position.x,
building.position.y,
0
)
point.description = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]"
point.pretty_name = "STRIKE on " + building.obj_name + " " + building.category + " [" + str(building.dcs_identifier) + " ]"
point.name = building.obj_name
@@ -406,7 +420,12 @@ class FlightPlanner:
ingress_point.targets.append(building)
flight.points.append(point)
else:
point = FlightWaypoint(location.position.x, location.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
point.description = "STRIKE on " + location.obj_name
point.pretty_name = "STRIKE on " + location.obj_name
point.name = location.obj_name
@@ -415,11 +434,15 @@ class FlightPlanner:
flight.points.append(point)
egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"])
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress_pos.x,
egress_pos.y,
self.doctrine["EGRESS_ALT"]
)
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS from " + location.obj_name
egress_point.description = "EGRESS from " + location.obj_name
egress_point.waypoint_type = FlightWaypointType.EGRESS
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
@@ -454,18 +477,26 @@ class FlightPlanner:
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt)
orbit0 = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
orbit0p.x,
orbit0p.y,
patrol_alt
)
orbit0.name = "ORBIT 0"
orbit0.description = "Standby between this point and the next one"
orbit0.pretty_name = "Race-track start"
orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK
flight.points.append(orbit0)
orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt)
orbit1 = FlightWaypoint(
FlightWaypointType.PATROL,
orbit1p.x,
orbit1p.y,
patrol_alt
)
orbit1.name = "ORBIT 1"
orbit1.description = "Standby between this point and the previous one"
orbit1.pretty_name = "Race-track end"
orbit1.waypoint_type = FlightWaypointType.PATROL
flight.points.append(orbit1)
orbit0.targets.append(for_cp)
@@ -512,18 +543,26 @@ class FlightPlanner:
ascend = self.generate_ascend_point(flight.from_cp)
flight.points.append(ascend)
orbit0 = FlightWaypoint(orbit0p.x, orbit0p.y, patrol_alt)
orbit0 = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
orbit0p.x,
orbit0p.y,
patrol_alt
)
orbit0.name = "ORBIT 0"
orbit0.description = "Standby between this point and the next one"
orbit0.pretty_name = "Race-track start"
orbit0.waypoint_type = FlightWaypointType.PATROL_TRACK
flight.points.append(orbit0)
orbit1 = FlightWaypoint(orbit1p.x, orbit1p.y, patrol_alt)
orbit1 = FlightWaypoint(
FlightWaypointType.PATROL,
orbit1p.x,
orbit1p.y,
patrol_alt
)
orbit1.name = "ORBIT 1"
orbit1.description = "Standby between this point and the previous one"
orbit1.pretty_name = "Race-track end"
orbit1.waypoint_type = FlightWaypointType.PATROL
flight.points.append(orbit1)
# Note : Targets of a PATROL TRACK waypoints are the points to be defended
@@ -555,49 +594,67 @@ class FlightPlanner:
egress_heading = heading - 180 - 25
ingress_pos = location.position.point_from_heading(ingress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
ingress_point = FlightWaypoint(ingress_pos.x, ingress_pos.y, self.doctrine["INGRESS_ALT"])
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_SEAD,
ingress_pos.x,
ingress_pos.y,
self.doctrine["INGRESS_ALT"]
)
ingress_point.name = "INGRESS"
ingress_point.pretty_name = "INGRESS on " + location.obj_name
ingress_point.description = "INGRESS on " + location.obj_name
ingress_point.waypoint_type = FlightWaypointType.INGRESS_SEAD
flight.points.append(ingress_point)
if len(custom_targets) > 0:
for target in custom_targets:
point = FlightWaypoint(target.position.x, target.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.position.x,
target.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
point.description = "SEAD on " + target.type
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "DEAD on " + location.obj_name
point.description = "DEAD on " + target.type
point.pretty_name = "DEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "SEAD on " + location.obj_name
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
flight.points.append(point)
ingress_point.targets.append(location)
ingress_point.targetGroup = location
flight.points.append(point)
else:
point = FlightWaypoint(location.position.x, location.position.y, 0)
point = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
location.position.y,
0
)
point.alt_type = "RADIO"
if flight.flight_type == FlightType.DEAD:
point.description = "SEAD on " + location.obj_name
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "DEAD on " + location.obj_name
point.pretty_name = "DEAD on " + location.obj_name
point.only_for_player = True
else:
point.description = "SEAD on " + location.obj_name
point.pretty_name = "SEAD on " + location.obj_name
point.only_for_player = True
ingress_point.targets.append(location)
ingress_point.targetGroup = location
flight.points.append(point)
egress_pos = location.position.point_from_heading(egress_heading, self.doctrine["INGRESS_EGRESS_DISTANCE"])
egress_point = FlightWaypoint(egress_pos.x, egress_pos.y, self.doctrine["EGRESS_ALT"])
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress_pos.x,
egress_pos.y,
self.doctrine["EGRESS_ALT"]
)
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS from " + location.obj_name
egress_point.description = "EGRESS from " + location.obj_name
egress_point.waypoint_type = FlightWaypointType.EGRESS
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
@@ -628,28 +685,40 @@ class FlightPlanner:
ascend.alt = 500
flight.points.append(ascend)
ingress_point = FlightWaypoint(ingress.x, ingress.y, cap_alt)
ingress_point = FlightWaypoint(
FlightWaypointType.INGRESS_CAS,
ingress.x,
ingress.y,
cap_alt
)
ingress_point.alt_type = "RADIO"
ingress_point.name = "INGRESS"
ingress_point.pretty_name = "INGRESS"
ingress_point.description = "Ingress into CAS area"
ingress_point.waypoint_type = FlightWaypointType.INGRESS_CAS
flight.points.append(ingress_point)
center_point = FlightWaypoint(center.x, center.y, cap_alt)
center_point = FlightWaypoint(
FlightWaypointType.CAS,
center.x,
center.y,
cap_alt
)
center_point.alt_type = "RADIO"
center_point.description = "Provide CAS"
center_point.name = "CAS"
center_point.pretty_name = "CAS"
center_point.waypoint_type = FlightWaypointType.CAS
flight.points.append(center_point)
egress_point = FlightWaypoint(egress.x, egress.y, cap_alt)
egress_point = FlightWaypoint(
FlightWaypointType.EGRESS,
egress.x,
egress.y,
cap_alt
)
egress_point.alt_type = "RADIO"
egress_point.description = "Egress from CAS area"
egress_point.name = "EGRESS"
egress_point.pretty_name = "EGRESS"
egress_point.waypoint_type = FlightWaypointType.EGRESS
flight.points.append(egress_point)
descend = self.generate_descend_point(flight.from_cp)
@@ -660,7 +729,6 @@ class FlightPlanner:
rtb = self.generate_rtb_waypoint(flight.from_cp)
flight.points.append(rtb)
def generate_ascend_point(self, from_cp):
"""
Generate ascend point
@@ -669,15 +737,18 @@ class FlightPlanner:
"""
ascend_heading = from_cp.heading
pos_ascend = from_cp.position.point_from_heading(ascend_heading, 10000)
ascend = FlightWaypoint(pos_ascend.x, pos_ascend.y, self.doctrine["PATTERN_ALTITUDE"])
ascend = FlightWaypoint(
FlightWaypointType.ASCEND_POINT,
pos_ascend.x,
pos_ascend.y,
self.doctrine["PATTERN_ALTITUDE"]
)
ascend.name = "ASCEND"
ascend.alt_type = "RADIO"
ascend.description = "Ascend"
ascend.pretty_name = "Ascend"
ascend.waypoint_type = FlightWaypointType.ASCEND_POINT
return ascend
def generate_descend_point(self, from_cp):
"""
Generate approach/descend point
@@ -686,15 +757,18 @@ class FlightPlanner:
"""
ascend_heading = from_cp.heading
descend = from_cp.position.point_from_heading(ascend_heading - 180, 10000)
descend = FlightWaypoint(descend.x, descend.y, self.doctrine["PATTERN_ALTITUDE"])
descend = FlightWaypoint(
FlightWaypointType.DESCENT_POINT,
descend.x,
descend.y,
self.doctrine["PATTERN_ALTITUDE"]
)
descend.name = "DESCEND"
descend.alt_type = "RADIO"
descend.description = "Descend to pattern alt"
descend.pretty_name = "Descend to pattern alt"
descend.waypoint_type = FlightWaypointType.DESCENT_POINT
return descend
def generate_rtb_waypoint(self, from_cp):
"""
Generate RTB landing point
@@ -702,10 +776,14 @@ class FlightPlanner:
:return:
"""
rtb = from_cp.position
rtb = FlightWaypoint(rtb.x, rtb.y, 0)
rtb = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
rtb.x,
rtb.y,
0
)
rtb.name = "LANDING"
rtb.alt_type = "RADIO"
rtb.description = "RTB"
rtb.pretty_name = "RTB"
rtb.waypoint_type = FlightWaypointType.LANDING_POINT
return rtb
return rtb

View File

@@ -27,6 +27,7 @@ INTERCEPT_CAPABLE = [
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
CAP_CAPABLE = [
MiG_15bis,
MiG_19P,
MiG_21Bis,
@@ -62,6 +63,8 @@ CAP_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
@@ -106,6 +109,9 @@ CAS_CAPABLE = [
F_16C_50,
FA_18C_hornet,
Tornado_IDS,
Tornado_GR4,
C_101CC,
MB_339PAN,
L_39ZA,
@@ -119,7 +125,6 @@ CAS_CAPABLE = [
AH_64D,
AH_1W,
UH_1H,
Mi_8MT,
@@ -130,6 +135,8 @@ CAS_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
SpitfireLFMkIXCW,
@@ -164,6 +171,9 @@ SEAD_CAPABLE = [
Su_34,
MiG_27K,
Tornado_IDS,
Tornado_GR4,
A_4E_C,
Rafale_A_S
]
@@ -197,6 +207,9 @@ STRIKE_CAPABLE = [
F_16C_50,
FA_18C_hornet,
Tornado_IDS,
Tornado_GR4,
C_101CC,
L_39ZA,
AJS37,
@@ -204,6 +217,8 @@ STRIKE_CAPABLE = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
B_17G,
@@ -231,6 +246,9 @@ ANTISHIP_CAPABLE = [
A_10C,
A_10A,
Tornado_IDS,
Tornado_GR4,
Ju_88A4,
Rafale_A_S
]

View File

@@ -1,10 +1,10 @@
from enum import Enum
from typing import List
from dcs.mission import StartType
from dcs.unittype import UnitType
from game import db
from dcs.unittype import UnitType
from dcs.point import MovingPoint, PointAction
from theater.controlpoint import ControlPoint
class FlightType(Enum):
@@ -62,7 +62,9 @@ class PredefinedWaypointCategory(Enum):
class FlightWaypoint:
def __init__(self, x: float, y: float, alt=0):
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: int = 0) -> None:
self.waypoint_type = waypoint_type
self.x = x
self.y = y
self.alt = alt
@@ -73,12 +75,38 @@ class FlightWaypoint:
self.targetGroup = None
self.obj_name = ""
self.pretty_name = ""
self.waypoint_type = FlightWaypointType.TAKEOFF # type: FlightWaypointType
self.category = PredefinedWaypointCategory.NOT_PREDEFINED# type: PredefinedWaypointCategory
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
self.only_for_player = False
self.data = None
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(point.position.x, point.position.y,
point.alt)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
waypoint.waypoint_type = {
PointAction.TurningPoint: FlightWaypointType.NAV,
PointAction.FlyOverPoint: FlightWaypointType.NAV,
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
}[point.action]
if waypoint.waypoint_type == FlightWaypointType.NAV:
waypoint.name = "NAV"
waypoint.pretty_name = "Nav"
waypoint.description = "Nav"
else:
waypoint.name = "TAKEOFF"
waypoint.pretty_name = "Takeoff"
waypoint.description = "Takeoff"
waypoint.description = f"Takeoff from {from_cp.name}"
return waypoint
class Flight:
unit_type: UnitType = None
from_cp = None
@@ -113,10 +141,10 @@ class Flight:
# Test
if __name__ == '__main__':
from dcs.planes import A_10C
from pydcs.dcs.planes import A_10C
from theater import ControlPoint, Point, List
from_cp = ControlPoint(0, "AA", Point(0, 0), None, [], 0, 0)
f = Flight(A_10C, 4, from_cp, FlightType.CAS)
f = Flight(A_10C(), 4, from_cp, FlightType.CAS)
f.scheduled_in = 50
print(f)

View File

@@ -1,4 +0,0 @@
from dcs.unitgroup import FlyingGroup

View File

@@ -197,7 +197,7 @@ DISTANCE_FROM_FRONTLINE = {
GROUP_SIZES_BY_COMBAT_STANCE = {
CombatStance.DEFENSIVE: [2, 4, 6],
CombatStance.AGGRESIVE: [2, 4, 6],
CombatStance.AGGRESSIVE: [2, 4, 6],
CombatStance.RETREAT: [2, 4, 6, 8],
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],

View File

@@ -3,7 +3,7 @@ from enum import Enum
class CombatStance(Enum):
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
AGGRESIVE = 1 # Unit will attempt to make progress with medium sized group of units
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units
RETREAT = 2 # Unit will retreat
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force

View File

@@ -1,13 +1,13 @@
import logging
from dcs.statics import *
from dcs.unit import Ship, Vehicle
from game import db
from game.data.building_data import FORTIFICATION_UNITS_ID, FORTIFICATION_UNITS
from game.db import unit_type_from_name
from .airfields import RunwayData
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.statics import *
from .radios import RadioRegistry
from .tacan import TacanBand, TacanRegistry
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
@@ -16,10 +16,15 @@ AA_CP_MIN_DISTANCE = 40000
class GroundObjectsGenerator:
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game):
def __init__(self, mission: Mission, conflict: Conflict, game,
radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
self.m = mission
self.conflict = conflict
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
def generate_farps(self, number_of_units=1) -> typing.Collection[StaticGroup]:
if self.conflict.is_vector:
@@ -103,6 +108,8 @@ class GroundObjectsGenerator:
utype = db.upgrade_to_supercarrier(utype, cp.name)
sg = self.m.ship_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
atc_channel = self.radio_registry.alloc_uhf()
sg.set_frequency(atc_channel.hertz)
sg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
@@ -111,6 +118,8 @@ class GroundObjectsGenerator:
ship.position.x = u.position.x
ship.position.y = u.position.y
ship.heading = u.heading
# TODO: Verify.
ship.set_frequency(atc_channel.hertz)
sg.add_unit(ship)
# Find carrier direction (In the wind)
@@ -125,10 +134,58 @@ class GroundObjectsGenerator:
attempt = attempt + 1
# Set UP TACAN and ICLS
modeChannel = "X" if not cp.tacanY else "Y"
sg.points[0].tasks.append(ActivateBeaconCommand(channel=cp.tacanN, modechannel=modeChannel, callsign=cp.tacanI, unit_id=sg.units[0].id, aa=False))
if ground_object.dcs_identifier == "CARRIER" and hasattr(cp, "icls"):
sg.points[0].tasks.append(ActivateICLSCommand(cp.icls, unit_id=sg.units[0].id))
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
icls_channel = next(self.icls_alloc)
# TODO: Assign these properly.
if ground_object.dcs_identifier == "CARRIER":
tacan_callsign = random.choice([
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
])
else:
tacan_callsign = random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
sg.points[0].tasks.append(ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=tacan_callsign,
unit_id=sg.units[0].id,
aa=False
))
sg.points[0].tasks.append(ActivateICLSCommand(
icls_channel,
unit_id=sg.units[0].id
))
# TODO: Make unit name usable.
# This relies on one control point mapping exactly
# to one LHA, carrier, or other usable "runway".
# This isn't wholly true, since the DD escorts of
# the carrier group are valid for helicopters, but
# they aren't exposed as such to the game. Should
# clean this up so that's possible. We can't use the
# unit name since it's an arbitrary ID.
self.runways[cp.name] = RunwayData(
cp.name,
"N/A",
atc=atc_channel,
tacan=tacan,
tacan_callsign=tacan_callsign,
icls=icls_channel,
)
else:

290
gen/kneeboard.py Normal file
View File

@@ -0,0 +1,290 @@
"""Generates kneeboard pages relevant to the player's mission.
The player kneeboard includes the following information:
* Airfield (departure, arrival, divert) info.
* Flight plan (waypoint numbers, names, altitudes).
* Comm channels.
* AWACS info.
* Tanker info.
* JTAC info.
Things we should add:
* Flight plan ToT and fuel ladder (current have neither available).
* Support for planning an arrival/divert airfield separate from departure.
* Mission package infrastructure to include information about the larger
mission, i.e. information about the escort flight for a strike package.
* Target information. Steerpoints, preplanned objectives, ToT, etc.
For multiplayer missions, a kneeboard will be generated per flight.
https://forums.eagle.ru/showthread.php?t=206360 claims that kneeboard pages can
only be added per airframe, so PvP missions where each side have the same
aircraft will be able to see the enemy's kneeboard for the same airframe.
"""
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission
from dcs.unittype import FlyingType
from tabulate import tabulate
from . import units
from .aircraft import AIRCRAFT_DATA, FlightData
from .airfields import RunwayData
from .airsupportgen import AwacsInfo, TankerInfo
from .briefinggen import CommInfo, JtacInfo, MissionInfoGenerator
from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency
class KneeboardPageWriter:
"""Creates kneeboard images."""
def __init__(self, page_margin: int = 24, line_spacing: int = 12) -> None:
self.image = Image.new('RGB', (768, 1024), (0xff, 0xff, 0xff))
# These font sizes create a relatively full page for current sorties. If
# we start generating more complicated flight plans, or start including
# more information in the comm ladder (the latter of which we should
# probably do), we'll need to split some of this information off into a
# second page.
self.title_font = ImageFont.truetype("arial.ttf", 32)
self.heading_font = ImageFont.truetype("arial.ttf", 24)
self.content_font = ImageFont.truetype("arial.ttf", 20)
self.table_font = ImageFont.truetype(
"resources/fonts/Inconsolata.otf", 20)
self.draw = ImageDraw.Draw(self.image)
self.x = page_margin
self.y = page_margin
self.line_spacing = line_spacing
@property
def position(self) -> Tuple[int, int]:
return self.x, self.y
def text(self, text: str, font=None,
fill: Tuple[int, int, int] = (0, 0, 0)) -> None:
if font is None:
font = self.content_font
self.draw.text(self.position, text, font=font, fill=fill)
width, height = self.draw.textsize(text, font=font)
self.y += height + self.line_spacing
def title(self, title: str) -> None:
self.text(title, font=self.title_font)
def heading(self, text: str) -> None:
self.text(text, font=self.heading_font)
def table(self, cells: List[List[str]],
headers: Optional[List[str]] = None) -> None:
table = tabulate(cells, headers=headers, numalign="right")
self.text(table, font=self.table_font)
def write(self, path: Path) -> None:
self.image.save(path)
class KneeboardPage:
"""Base class for all kneeboard pages."""
def write(self, path: Path) -> None:
"""Writes the kneeboard page to the given path."""
raise NotImplementedError
@dataclass(frozen=True)
class NumberedWaypoint:
number: int
waypoint: FlightWaypoint
class FlightPlanBuilder:
def __init__(self) -> None:
self.rows: List[List[str]] = []
self.target_points: List[NumberedWaypoint] = []
def add_waypoint(self, waypoint_num: int, waypoint: FlightWaypoint) -> None:
if waypoint.waypoint_type == FlightWaypointType.TARGET_POINT:
self.target_points.append(NumberedWaypoint(waypoint_num, waypoint))
return
if self.target_points:
self.coalesce_target_points()
self.target_points = []
self.add_waypoint_row(NumberedWaypoint(waypoint_num, waypoint))
def coalesce_target_points(self) -> None:
if len(self.target_points) <= 4:
for steerpoint in self.target_points:
self.add_waypoint_row(steerpoint)
return
first_waypoint_num = self.target_points[0].number
last_waypoint_num = self.target_points[-1].number
self.rows.append([
f"{first_waypoint_num}-{last_waypoint_num}",
"Target points",
"0"
])
def add_waypoint_row(self, waypoint: NumberedWaypoint) -> None:
self.rows.append([
waypoint.number,
waypoint.waypoint.pretty_name,
str(int(units.meters_to_feet(waypoint.waypoint.alt)))
])
def build(self) -> List[List[str]]:
return self.rows
class BriefingPage(KneeboardPage):
"""A kneeboard page containing briefing information."""
def __init__(self, flight: FlightData, comms: List[CommInfo],
awacs: List[AwacsInfo], tankers: List[TankerInfo],
jtacs: List[JtacInfo]) -> None:
self.flight = flight
self.comms = list(comms)
self.awacs = awacs
self.tankers = tankers
self.jtacs = jtacs
self.comms.append(CommInfo("Flight", self.flight.intra_flight_channel))
def write(self, path: Path) -> None:
writer = KneeboardPageWriter()
writer.title(f"{self.flight.callsign} Mission Info")
# TODO: Handle carriers.
writer.heading("Airfield Info")
writer.table([
self.airfield_info_row("Departure", self.flight.departure),
self.airfield_info_row("Arrival", self.flight.arrival),
self.airfield_info_row("Divert", self.flight.divert),
], headers=["", "Airbase", "ATC", "TCN", "I(C)LS", "RWY"])
writer.heading("Flight Plan")
flight_plan_builder = FlightPlanBuilder()
for num, waypoint in enumerate(self.flight.waypoints):
flight_plan_builder.add_waypoint(num, waypoint)
writer.table(flight_plan_builder.build(),
headers=["STPT", "Action", "Alt"])
writer.heading("Comm Ladder")
comms = []
for comm in self.comms:
comms.append([comm.name, self.format_frequency(comm.freq)])
writer.table(comms, headers=["Name", "UHF"])
writer.heading("AWACS")
awacs = []
for a in self.awacs:
awacs.append([a.callsign, self.format_frequency(a.freq)])
writer.table(awacs, headers=["Callsign", "UHF"])
writer.heading("Tankers")
tankers = []
for tanker in self.tankers:
tankers.append([
tanker.callsign,
tanker.variant,
tanker.tacan,
self.format_frequency(tanker.freq),
])
writer.table(tankers, headers=["Callsign", "Type", "TACAN", "UHF"])
writer.heading("JTAC")
jtacs = []
for jtac in self.jtacs:
jtacs.append([jtac.callsign, jtac.region, jtac.code])
writer.table(jtacs, headers=["Callsign", "Region", "Laser Code"])
writer.write(path)
def airfield_info_row(self, row_title: str,
runway: Optional[RunwayData]) -> List[str]:
"""Creates a table row for a given airfield.
Args:
row_title: Purpose of the airfield. e.g. "Departure", "Arrival" or
"Divert".
runway: The runway described by this row.
Returns:
A list of strings to be used as a row of the airfield table.
"""
if runway is None:
return [row_title, "", "", "", "", ""]
atc = ""
if runway.atc is not None:
atc = self.format_frequency(runway.atc)
return [
row_title,
runway.airfield_name,
atc,
runway.tacan or "",
runway.ils or runway.icls or "",
runway.runway_name,
]
def format_frequency(self, frequency: RadioFrequency) -> str:
channel = self.flight.channel_for(frequency)
if channel is None:
return str(frequency)
namer = AIRCRAFT_DATA[self.flight.aircraft_type.id].channel_namer
channel_name = namer.channel_name(channel.radio_id, channel.channel)
return f"{channel_name} {frequency}"
class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission) -> None:
super().__init__(mission)
def generate(self) -> None:
"""Generates a kneeboard per client flight."""
temp_dir = Path("kneeboards")
temp_dir.mkdir(exist_ok=True)
for aircraft, pages in self.pages_by_airframe().items():
aircraft_dir = temp_dir / aircraft.id
aircraft_dir.mkdir(exist_ok=True)
for idx, page in enumerate(pages):
page_path = aircraft_dir / f"page{idx:02}.png"
page.write(page_path)
self.mission.add_aircraft_kneeboard(aircraft, page_path)
def pages_by_airframe(self) -> Dict[FlyingType, List[KneeboardPage]]:
"""Returns a list of kneeboard pages per airframe in the mission.
Only client flights will be included, but because DCS does not support
group-specific kneeboard pages, flights (possibly from opposing sides)
will be able to see the kneeboards of all aircraft of the same type.
Returns:
A dict mapping aircraft types to the list of kneeboard pages for
that aircraft.
"""
all_flights: Dict[FlyingType, List[KneeboardPage]] = defaultdict(list)
for flight in self.flights:
if not flight.client_units:
continue
all_flights[flight.aircraft_type].extend(
self.generate_flight_kneeboard(flight))
return all_flights
def generate_flight_kneeboard(self, flight: FlightData) -> List[KneeboardPage]:
"""Returns a list of kneeboard pages for the given flight."""
return [
BriefingPage(
flight, self.comms, self.awacs, self.tankers, self.jtacs
),
]

View File

@@ -61,9 +61,9 @@ class NameGenerator:
self.number += 1
return "awacs|{}|{}|0|".format(country.id, self.number)
def next_tanker_name(self, country):
def next_tanker_name(self, country, unit_type):
self.number += 1
return "tanker|{}|{}|0|".format(country.id, self.number)
return "tanker|{}|{}|0|{}".format(country.id, self.number, db.unit_type_name(unit_type))
def next_carrier_name(self, country):
self.number += 1

226
gen/radios.py Normal file
View File

@@ -0,0 +1,226 @@
"""Radio frequency types and allocators."""
import itertools
from dataclasses import dataclass
from typing import Dict, Iterator, List, Set
@dataclass(frozen=True)
class RadioFrequency:
"""A radio frequency.
Not currently concerned with tracking modulation, just the frequency.
"""
#: The frequency in kilohertz.
hertz: int
def __str__(self):
if self.hertz >= 1000000:
return self.format("MHz", 1000000)
return self.format("kHz", 1000)
def format(self, units: str, divisor: int) -> str:
converted = self.hertz / divisor
if converted.is_integer():
return f"{int(converted)} {units}"
return f"{converted:0.3f} {units}"
@property
def mhz(self) -> float:
"""Returns the frequency in megahertz.
Returns:
The frequency in megahertz.
"""
return self.hertz / 1000000
def MHz(num: int, khz: int = 0) -> RadioFrequency:
return RadioFrequency(num * 1000000 + khz * 1000)
def kHz(num: int) -> RadioFrequency:
return RadioFrequency(num * 1000)
@dataclass(frozen=True)
class Radio:
"""A radio.
Defines the minimum (inclusive) and maximum (exclusive) range of the radio.
"""
#: The name of the radio.
name: str
#: The minimum (inclusive) frequency tunable by this radio.
minimum: RadioFrequency
#: The maximum (exclusive) frequency tunable by this radio.
maximum: RadioFrequency
#: The spacing between adjacent frequencies.
step: RadioFrequency
def __str__(self) -> str:
return self.name
def range(self) -> Iterator[RadioFrequency]:
"""Returns an iterator over the usable frequencies of this radio."""
return (RadioFrequency(x) for x in range(
self.minimum.hertz, self.maximum.hertz, self.step.hertz
))
class OutOfChannelsError(RuntimeError):
"""Raised when all channels usable by this radio have been allocated."""
def __init__(self, radio: Radio) -> None:
super().__init__(f"No available channels for {radio}")
class ChannelInUseError(RuntimeError):
"""Raised when attempting to reserve an in-use frequency."""
def __init__(self, frequency: RadioFrequency) -> None:
super().__init__(f"{frequency} is already in use")
# TODO: Figure out appropriate steps for each radio. These are just guesses.
#: List of all known radios used by aircraft in the game.
RADIOS: List[Radio] = [
Radio("AN/ARC-164", MHz(225), MHz(400), step=MHz(1)),
Radio("AN/ARC-186(V) AM", MHz(116), MHz(152), step=MHz(1)),
Radio("AN/ARC-186(V) FM", MHz(30), MHz(76), step=MHz(1)),
# The AN/ARC-210 can also use [30, 88) and [108, 118), but the current
# implementation can't implement the gap and the radio can't transmit on the
# latter. There's still plenty of channels between 118 MHz and 400 MHz, so
# not worth worrying about.
Radio("AN/ARC-210", MHz(118), MHz(400), step=MHz(1)),
Radio("AN/ARC-222", MHz(116), MHz(174), step=MHz(1)),
Radio("SCR-522", MHz(100), MHz(156), step=MHz(1)),
Radio("A.R.I. 1063", MHz(100), MHz(156), step=MHz(1)),
Radio("BC-1206", kHz(200), kHz(400), step=kHz(10)),
# Note: The M2000C V/UHF can operate in both ranges, but has a gap between
# 150 MHz and 225 MHz. We can't allocate in that gap, and the current
# system doesn't model gaps, so just pretend it ends at 150 MHz for now. We
# can model gaps later if needed.
Radio("TRT ERA 7000 V/UHF", MHz(118), MHz(150), step=MHz(1)),
Radio("TRT ERA 7200 UHF", MHz(225), MHz(400), step=MHz(1)),
# Tomcat radios
# # https://www.heatblur.se/F-14Manual/general.html#an-arc-159-uhf-1-radio
Radio("AN/ARC-159", MHz(225), MHz(400), step=MHz(1)),
# AN/ARC-182 can also operate from 30 MHz to 88 MHz, as well as from 225 MHz
# to 400 MHz range, but we can't model gaps with the current implementation.
# https://www.heatblur.se/F-14Manual/general.html#an-arc-182-v-uhf-2-radio
Radio("AN/ARC-182", MHz(108), MHz(174), step=MHz(1)),
# Also capable of [103, 156) at 25 kHz intervals, but we can't do gaps.
Radio("FR 22", MHz(225), MHz(400), step=kHz(50)),
# P-51 / P-47 Radio
# 4 preset channels (A/B/C/D)
Radio("SCR522", MHz(100), MHz(156), step=kHz(25)),
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
]
def get_radio(name: str) -> Radio:
"""Returns the radio with the given name.
Args:
name: Name of the radio to return.
Returns:
The radio matching name.
Raises:
KeyError: No matching radio was found.
"""
for radio in RADIOS:
if radio.name == name:
return radio
raise KeyError
class RadioRegistry:
"""Manages allocation of radio channels.
There's some room for improvement here. We could prefer to allocate
frequencies that are available to the fewest number of radios first, so
radios with wide bands like the AN/ARC-210 don't exhaust all the channels
available to narrower radios like the AN/ARC-186(V). In practice there are
probably plenty of channels, so we can deal with that later if we need to.
We could also allocate using a larger increment, returning to smaller
increments each time the range is exhausted. This would help with the
previous problem, as the AN/ARC-186(V) would still have plenty of 25 kHz
increment channels left after the AN/ARC-210 moved on to the higher
frequencies. This would also look a little nicer than having every flight
allocated in the 30 MHz range.
"""
# Not a real radio, but useful for allocating a channel usable for
# inter-flight communications.
BLUFOR_UHF = Radio("BLUFOR UHF", MHz(225), MHz(400), step=MHz(1))
def __init__(self) -> None:
self.allocated_channels: Set[RadioFrequency] = set()
self.radio_allocators: Dict[Radio, Iterator[RadioFrequency]] = {}
radios = itertools.chain(RADIOS, [self.BLUFOR_UHF])
for radio in radios:
self.radio_allocators[radio] = radio.range()
def alloc_for_radio(self, radio: Radio) -> RadioFrequency:
"""Allocates a radio channel tunable by the given radio.
Args:
radio: The radio to allocate a channel for.
Returns:
A radio channel compatible with the given radio.
Raises:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
allocator = self.radio_allocators[radio]
try:
while (channel := next(allocator)) in self.allocated_channels:
pass
self.reserve(channel)
return channel
except StopIteration:
raise OutOfChannelsError(radio)
def alloc_uhf(self) -> RadioFrequency:
"""Allocates a UHF radio channel suitable for inter-flight comms.
Returns:
A UHF radio channel suitable for inter-flight comms.
Raises:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
return self.alloc_for_radio(self.BLUFOR_UHF)
def reserve(self, frequency: RadioFrequency) -> None:
"""Reserves the given channel.
Reserving a channel ensures that it will not be allocated in the future.
Args:
frequency: The channel to reserve.
Raises:
ChannelInUseError: The given frequency is already in use.
"""
if frequency in self.allocated_channels:
raise ChannelInUseError(frequency)
self.allocated_channels.add(frequency)

View File

@@ -10,9 +10,12 @@ class BoforsGenerator(GroupGenerator):
This generate a Bofors flak artillery group
"""
name = "Bofors AAA"
price = 75
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10,40)

View File

@@ -11,11 +11,14 @@ class FlakGenerator(GroupGenerator):
This generate a German flak artillery group
"""
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
name = "Flak Site"
price = 135
spacing = random.randint(30,60)
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(30, 60)
index = 0
mixed = random.choice([True, False])
@@ -32,7 +35,7 @@ class FlakGenerator(GroupGenerator):
unit_type = random.choice(GFLAK)
# Search lights
search_pos = self.get_circular_position(random.randint(2,5), 90)
search_pos = self.get_circular_position(random.randint(2,3), 90)
for index, pos in enumerate(search_pos):
self.add_unit(AirDefence.Flak_Searchlight_37, "SearchLight#" + str(index), pos[0], pos[1], self.heading)

View File

@@ -10,9 +10,12 @@ class ZU23InsurgentGenerator(GroupGenerator):
This generate a ZU23 insurgent flak artillery group
"""
name = "Zu-23 Site"
price = 56
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10,40)

View File

@@ -10,6 +10,9 @@ class AvengerGenerator(GroupGenerator):
This generate an Avenger group
"""
name = "Avenger Group"
price = 62
def generate(self):
num_launchers = random.randint(2, 3)

View File

@@ -10,6 +10,9 @@ class ChaparralGenerator(GroupGenerator):
This generate a Chaparral group
"""
name = "Chaparral Group"
price = 66
def generate(self):
num_launchers = random.randint(2, 4)

View File

@@ -10,6 +10,9 @@ class GepardGenerator(GroupGenerator):
This generate a Gepard group
"""
name = "Gepard Group"
price = 50
def generate(self):
self.add_unit(AirDefence.SPAAA_Gepard, "SPAAA", self.position.x, self.position.y, self.heading)
if random.randint(0, 1) == 1:

View File

@@ -1,5 +1,7 @@
import random
from typing import List
from dcs.unittype import UnitType
from dcs.vehicles import AirDefence
from game import db
@@ -99,6 +101,23 @@ SAM_PRICES = {
AirDefence.HQ_7_Self_Propelled_LN: 35
}
def get_faction_possible_sams_units(faction: str) -> List[UnitType]:
"""
Return the list
:param faction: Faction to search units for
"""
return [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()]
def get_faction_possible_sams_generator(faction: str) -> List[UnitType]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction to search units for
"""
return [SAM_MAP[u] for u in get_faction_possible_sams_units(faction)]
def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
"""
This generate a SAM group
@@ -107,7 +126,7 @@ def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
"""
possible_sams = [u for u in db.FACTIONS[faction]["units"] if u in AirDefence.__dict__.values()]
possible_sams = get_faction_possible_sams_units(faction)
if len(possible_sams) > 0:
sam = random.choice(possible_sams)
generator = SAM_MAP[sam](game, ground_object)

View File

@@ -10,6 +10,9 @@ class HawkGenerator(GroupGenerator):
This generate an HAWK group
"""
name = "Hawk Site"
price = 115
def generate(self):
self.add_unit(AirDefence.SAM_Hawk_PCP, "PCP", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Hawk_SR_AN_MPQ_50, "SR", self.position.x + 20, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class HQ7Generator(GroupGenerator):
This generate an HQ7 group
"""
name = "HQ-7 Site"
price = 120
def generate(self):
self.add_unit(AirDefence.HQ_7_Self_Propelled_STR, "STR", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.HQ_7_Self_Propelled_LN, "LN", self.position.x + 20, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class LinebackerGenerator(GroupGenerator):
This generate an m6 linebacker group
"""
name = "Linebacker Group"
price = 75
def generate(self):
num_launchers = random.randint(2, 4)

View File

@@ -10,6 +10,9 @@ class PatriotGenerator(GroupGenerator):
This generate a Patriot group
"""
name = "Patriot Battery"
price = 240
def generate(self):
# Command Post
self.add_unit(AirDefence.SAM_Patriot_AMG_AN_MRC_137, "MRC", self.position.x, self.position.y, self.heading)
@@ -18,13 +21,13 @@ class PatriotGenerator(GroupGenerator):
self.add_unit(AirDefence.SAM_Patriot_EPP_III, "EPP", self.position.x, self.position.y + 30, self.heading)
self.add_unit(AirDefence.SAM_Patriot_STR_AN_MPQ_53, "ICC", self.position.x + 30, self.position.y + 30, self.heading)
num_launchers = random.randint(2, 4)
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_Patriot_LN_M901, "LN#" + str(i), position[0], position[1], position[2])
# Short range protection for high value site
num_launchers = random.randint(2, 4)
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])

View File

@@ -10,6 +10,9 @@ class RapierGenerator(GroupGenerator):
This generate a Rapier Group
"""
name = "Rapier AA Site"
price = 50
def generate(self):
self.add_unit(AirDefence.Rapier_FSA_Blindfire_Tracker, "BT", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.Rapier_FSA_Optical_Tracker, "OT", self.position.x + 20, self.position.y, self.heading)

View File

@@ -8,6 +8,9 @@ class RolandGenerator(GroupGenerator):
This generate a Roland group
"""
name = "Roland Site"
price = 40
def generate(self):
self.add_unit(AirDefence.SAM_Roland_ADS, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_Roland_EWR, "EWR", self.position.x + 40, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA10Generator(GroupGenerator):
This generate a SA-10 group
"""
name = "SA-10/S-300PS Battery"
price = 450
def generate(self):
# Command Post
self.add_unit(AirDefence.SAM_SA_10_S_300PS_CP_54K6, "CP", self.position.x, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA11Generator(GroupGenerator):
This generate a SA-11 group
"""
name = "SA-11 Buk Battery"
price = 180
def generate(self):
self.add_unit(AirDefence.SAM_SA_11_Buk_CC_9S470M1, "CC", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_11_Buk_SR_9S18M1, "SR", self.position.x+20, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA13Generator(GroupGenerator):
This generate a SA-13 group
"""
name = "SA-13 Strela Group"
price = 50
def generate(self):
self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading)

View File

@@ -8,6 +8,9 @@ class SA15Generator(GroupGenerator):
This generate a SA-15 group
"""
name = "SA-15 Tor Group"
price = 55
def generate(self):
self.add_unit(AirDefence.SAM_SA_15_Tor_9A331, "ADS", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_UAZ_469, "EWR", self.position.x + 40, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA19Generator(GroupGenerator):
This generate a SA-19 group
"""
name = "SA-19 Tunguska Group"
price = 90
def generate(self):
num_launchers = random.randint(1, 3)

View File

@@ -10,6 +10,9 @@ class SA2Generator(GroupGenerator):
This generate a SA-2 group
"""
name = "SA-2/S-75 Site"
price = 74
def generate(self):
self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song, "TR", self.position.x + 20, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA3Generator(GroupGenerator):
This generate a SA-3 group
"""
name = "SA-3/S-125 Site"
price = 80
def generate(self):
self.add_unit(AirDefence.SAM_SR_P_19, "SR", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_3_S_125_TR_SNR, "TR", self.position.x + 20, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA6Generator(GroupGenerator):
This generate a SA-6 group
"""
name = "SA-6 Kub Site"
price = 102
def generate(self):
self.add_unit(AirDefence.SAM_SA_6_Kub_STR_9S91, "STR", self.position.x, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA8Generator(GroupGenerator):
This generate a SA-8 group
"""
name = "SA-8 OSA Site"
price = 55
def generate(self):
self.add_unit(AirDefence.SAM_SA_8_Osa_9A33, "OSA", self.position.x, self.position.y, self.heading)
self.add_unit(AirDefence.SAM_SA_8_Osa_LD_9T217, "LD", self.position.x + 20, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class SA9Generator(GroupGenerator):
This generate a SA-9 group
"""
name = "SA-9 Group"
price = 40
def generate(self):
self.add_unit(Unarmed.Transport_UAZ_469, "UAZ", self.position.x, self.position.y, self.heading)
self.add_unit(Unarmed.Transport_KAMAZ_43101, "TRUCK", self.position.x+40, self.position.y, self.heading)

View File

@@ -10,6 +10,9 @@ class VulcanGenerator(GroupGenerator):
This generate a Vulcan group
"""
name = "Vulcan Group"
price = 25
def generate(self):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA", self.position.x, self.position.y, self.heading)
if random.randint(0, 1) == 1:

View File

@@ -10,8 +10,11 @@ class ZSU23Generator(GroupGenerator):
This generate a ZSU 23 group
"""
name = "ZSU-23 Group"
price = 50
def generate(self):
num_launchers = random.randint(2, 5)
num_launchers = random.randint(4, 5)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=180)
for i, position in enumerate(positions):

View File

@@ -10,9 +10,12 @@ class ZU23Generator(GroupGenerator):
This generate a ZU23 flak artillery group
"""
name = "ZU-23 Group"
price = 54
def generate(self):
grid_x = random.randint(2, 4)
grid_y = random.randint(2, 4)
grid_x = random.randint(2, 3)
grid_y = random.randint(2, 3)
spacing = random.randint(10,40)

View File

@@ -10,6 +10,9 @@ class ZU23UralGenerator(GroupGenerator):
This generate a Zu23 Ural group
"""
name = "ZU-23 Ural Group"
price = 64
def generate(self):
num_launchers = random.randint(2, 8)

View File

@@ -10,6 +10,9 @@ class ZU23UralInsurgentGenerator(GroupGenerator):
This generate a Zu23 Ural group
"""
name = "ZU-23 Ural Insurgent Group"
price = 64
def generate(self):
num_launchers = random.randint(2, 8)

83
gen/tacan.py Normal file
View File

@@ -0,0 +1,83 @@
"""TACAN channel handling."""
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Iterator, Set
class TacanBand(Enum):
X = "X"
Y = "Y"
def range(self) -> Iterator["TacanChannel"]:
"""Returns an iterator over the channels in this band."""
return (TacanChannel(x, self) for x in range(1, 100))
@dataclass(frozen=True)
class TacanChannel:
number: int
band: TacanBand
def __str__(self) -> str:
return f"{self.number}{self.band.value}"
class OutOfTacanChannelsError(RuntimeError):
"""Raised when all channels in this band have been allocated."""
def __init__(self, band: TacanBand) -> None:
super().__init__(f"No available channels in TACAN {band.value} band")
class TacanChannelInUseError(RuntimeError):
"""Raised when attempting to reserve an in-use channel."""
def __init__(self, channel: TacanChannel) -> None:
super().__init__(f"{channel} is already in use")
class TacanRegistry:
"""Manages allocation of TACAN channels."""
def __init__(self) -> None:
self.allocated_channels: Set[TacanChannel] = set()
self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {}
for band in TacanBand:
self.band_allocators[band] = band.range()
def alloc_for_band(self, band: TacanBand) -> TacanChannel:
"""Allocates a TACAN channel in the given band.
Args:
band: The TACAN band to allocate a channel for.
Returns:
A TACAN channel in the given band.
Raises:
OutOfChannelsError: All channels compatible with the given radio are
already allocated.
"""
allocator = self.band_allocators[band]
try:
while (channel := next(allocator)) in self.allocated_channels:
pass
return channel
except StopIteration:
raise OutOfTacanChannelsError(band)
def reserve(self, channel: TacanChannel) -> None:
"""Reserves the given channel.
Reserving a channel ensures that it will not be allocated in the future.
Args:
channel: The channel to reserve.
Raises:
ChannelInUseError: The given frequency is already in use.
"""
if channel in self.allocated_channels:
raise TacanChannelInUseError(channel)
self.allocated_channels.add(channel)

6
gen/units.py Normal file
View File

@@ -0,0 +1,6 @@
"""Unit conversions."""
def meters_to_feet(meters: float) -> float:
"""Convers meters to feet."""
return meters * 3.28084

2
pydcs

Submodule pydcs updated: dcc3d84631...f46781b854

View File

@@ -4,7 +4,7 @@ import logging
import os
import sys
from pydcs import dcs
import dcs
from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen

View File

@@ -8,7 +8,7 @@ from game.event import UnitsDeliveryEvent, FrontlineAttackEvent
from theater.theatergroundobject import CATEGORY_MAP
from userdata.liberation_theme import get_theme_icons
VERSION_STRING = "2.1.0"
VERSION_STRING = "2.1.2"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",

View File

@@ -21,6 +21,7 @@ class QTopPanel(QFrame):
self.setMaximumHeight(70)
self.init_ui()
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().budgetupdated.connect(self.budget_update)
def init_ui(self):
@@ -101,4 +102,7 @@ class QTopPanel(QFrame):
def proceed(self):
self.subwindow = QMissionPlanning(self.game)
self.subwindow.show()
self.subwindow.show()
def budget_update(self, game:Game):
self.budgetBox.setGame(game)

View File

@@ -35,6 +35,7 @@ class QFilteredComboBox(QComboBox):
super(QFilteredComboBox, self).setModel(model)
self.pFilterModel.setSourceModel(model)
self.completer.setModel(self.pFilterModel)
self.model().sort(0)
def setModelColumn(self, column):
self.completer.setCompletionColumn(column)

View File

@@ -57,13 +57,16 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
enemy_cp = [ecp for ecp in cp.connected_points if ecp.captured != cp.captured]
for ecp in enemy_cp:
pos = Conflict.frontline_position(self.game.theater, cp, ecp)[0]
wpt = FlightWaypoint(pos.x, pos.y, 800)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
pos.x,
pos.y,
800)
wpt.name = "Frontline " + cp.name + "/" + ecp.name + " [CAS]"
wpt.alt_type = "RADIO"
wpt.pretty_name = wpt.name
wpt.description = "Frontline"
wpt.data = [cp, ecp]
wpt.waypoint_type = FlightWaypointType.CUSTOM
wpt.category = PredefinedWaypointCategory.FRONTLINE
i = add_model_item(i, model, wpt.pretty_name, wpt)
@@ -72,14 +75,18 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
for ground_object in cp.ground_objects:
if not ground_object.is_dead and not ground_object.dcs_identifier == "AA":
wpt = FlightWaypoint(ground_object.position.x,ground_object.position.y, 0)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
ground_object.position.x,
ground_object.position.y,
0
)
wpt.alt_type = "RADIO"
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + ground_object.category + " #" + str(ground_object.object_id)
wpt.pretty_name = wpt.name
wpt.obj_name = ground_object.obj_name
wpt.targets.append(ground_object)
wpt.data = ground_object
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly Building"
wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
@@ -95,7 +102,12 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if not ground_object.is_dead and ground_object.dcs_identifier == "AA":
for g in ground_object.groups:
for j, u in enumerate(g.units):
wpt = FlightWaypoint(u.position.x, u.position.y, 0)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
u.position.x,
u.position.y,
0
)
wpt.alt_type = "RADIO"
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
wpt.pretty_name = wpt.name
@@ -114,11 +126,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
if self.include_airbases:
for cp in self.game.theater.controlpoints:
if (self.include_enemy and not cp.captured) or (self.include_friendly and cp.captured):
wpt = FlightWaypoint(cp.position.x, cp.position.y, 0)
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
cp.position.x,
cp.position.y,
0
)
wpt.alt_type = "RADIO"
wpt.name = cp.name
wpt.data = cp
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
wpt.category = PredefinedWaypointCategory.ALLY_CP
@@ -133,7 +149,6 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
else:
wpt.pretty_name = cp.name + " (Airbase)"
i = add_model_item(i, model, wpt.pretty_name, wpt)
self.setModel(model)

View File

@@ -16,6 +16,7 @@ class GameUpdateSignal(QObject):
instance = None
gameupdated = Signal(Game)
budgetupdated = Signal(Game)
debriefingReceived = Signal(DebriefingSignal)
def __init__(self):
@@ -25,6 +26,9 @@ class GameUpdateSignal(QObject):
def updateGame(self, game: Game):
self.gameupdated.emit(game)
def updateBudget(self, game: Game):
self.budgetupdated.emit(game)
def sendDebriefing(self, game: Game, gameEvent: Event, debriefing: Debriefing):
sig = DebriefingSignal(game, gameEvent, debriefing)
self.gameupdated.emit(game)

View File

@@ -29,7 +29,7 @@ class QLiberationWindow(QMainWindow):
self.setGame(persistency.restore_game())
self.setGeometry(300, 100, 270, 100)
self.setWindowTitle("DCS Liberation")
self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING)
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage('Ready')
@@ -69,27 +69,31 @@ class QLiberationWindow(QMainWindow):
GameUpdateSignal.get_instance().debriefingReceived.connect(self.onDebriefing)
def initActions(self):
self.newGameAction = QAction("New Game", self)
self.newGameAction = QAction("&New Game", self)
self.newGameAction.setIcon(QIcon(CONST.ICONS["New"]))
self.newGameAction.triggered.connect(self.newGame)
self.newGameAction.setShortcut('CTRL+N')
self.openAction = QAction("Open", self)
self.openAction = QAction("&Open", self)
self.openAction.setIcon(QIcon(CONST.ICONS["Open"]))
self.openAction.triggered.connect(self.openFile)
self.openAction.setShortcut('CTRL+O')
self.saveGameAction = QAction("Save", self)
self.saveGameAction = QAction("&Save", self)
self.saveGameAction.setIcon(QIcon(CONST.ICONS["Save"]))
self.saveGameAction.triggered.connect(self.saveGame)
self.saveGameAction.setShortcut('CTRL+S')
self.saveAsAction = QAction("Save As", self)
self.saveAsAction = QAction("Save &As", self)
self.saveAsAction.setIcon(QIcon(CONST.ICONS["Save"]))
self.saveAsAction.triggered.connect(self.saveGameAs)
self.saveAsAction.setShortcut('CTRL+A')
self.showAboutDialogAction = QAction("About DCS Liberation", self)
self.showAboutDialogAction = QAction("&About DCS Liberation", self)
self.showAboutDialogAction.setIcon(QIcon.fromTheme("help-about"))
self.showAboutDialogAction.triggered.connect(self.showAboutDialog)
self.showLiberationPrefDialogAction = QAction("Preferences", self)
self.showLiberationPrefDialogAction = QAction("&Preferences", self)
self.showLiberationPrefDialogAction.setIcon(QIcon.fromTheme("help-about"))
self.showLiberationPrefDialogAction.triggered.connect(self.showLiberationDialog)
@@ -102,57 +106,47 @@ class QLiberationWindow(QMainWindow):
def initMenuBar(self):
self.menu = self.menuBar()
file_menu = self.menu.addMenu("File")
file_menu = self.menu.addMenu("&File")
file_menu.addAction(self.newGameAction)
file_menu.addAction(self.openAction)
file_menu.addSeparator()
file_menu.addAction(self.saveGameAction)
file_menu.addAction(self.saveAsAction)
file_menu.addSeparator()
file_menu.addAction(self.showLiberationPrefDialogAction)
file_menu.addSeparator()
#file_menu.addAction("Close Current Game", lambda: self.closeGame()) # Not working
file_menu.addAction("Exit" , lambda: self.exit())
file_menu.addAction("E&xit" , lambda: self.exit())
help_menu = self.menu.addMenu("Help")
help_menu.addAction("Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
help_menu.addAction("Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation"))
help_menu.addAction("Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases"))
help_menu.addAction("Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"]))
help_menu.addAction("ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
help_menu.addAction("Report an issue", lambda: webbrowser.open_new_tab(URLS["Issues"]))
displayMenu = self.menu.addMenu("&Display")
help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction)
displayMenu = self.menu.addMenu("Display")
tg_cp_visibility = QAction('Control Point', displayMenu)
tg_cp_visibility = QAction('&Control Point', displayMenu)
tg_cp_visibility.setCheckable(True)
tg_cp_visibility.setChecked(True)
tg_cp_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("cp", tg_cp_visibility.isChecked()))
tg_go_visibility = QAction('Ground Objects', displayMenu)
tg_go_visibility = QAction('&Ground Objects', displayMenu)
tg_go_visibility.setCheckable(True)
tg_go_visibility.setChecked(True)
tg_go_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("go", tg_go_visibility.isChecked()))
tg_line_visibility = QAction('Lines', displayMenu)
tg_line_visibility = QAction('&Lines', displayMenu)
tg_line_visibility.setCheckable(True)
tg_line_visibility.setChecked(True)
tg_line_visibility.toggled.connect(
lambda: QLiberationMap.set_display_rule("lines", tg_line_visibility.isChecked()))
tg_event_visibility = QAction('Events', displayMenu)
tg_event_visibility = QAction('&Events', displayMenu)
tg_event_visibility.setCheckable(True)
tg_event_visibility.setChecked(True)
tg_event_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("events", tg_event_visibility.isChecked()))
tg_sam_visibility = QAction('SAM Range', displayMenu)
tg_sam_visibility = QAction('&SAM Range', displayMenu)
tg_sam_visibility.setCheckable(True)
tg_sam_visibility.setChecked(True)
tg_sam_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("sam", tg_sam_visibility.isChecked()))
tg_flight_path_visibility = QAction('Flight Paths', displayMenu)
tg_flight_path_visibility = QAction('&Flight Paths', displayMenu)
tg_flight_path_visibility.setCheckable(True)
tg_flight_path_visibility.setChecked(False)
tg_flight_path_visibility.toggled.connect(lambda: QLiberationMap.set_display_rule("flight_paths", tg_flight_path_visibility.isChecked()))
@@ -164,6 +158,17 @@ class QLiberationWindow(QMainWindow):
displayMenu.addAction(tg_sam_visibility)
displayMenu.addAction(tg_flight_path_visibility)
help_menu = self.menu.addMenu("&Help")
help_menu.addAction("&Discord Server", lambda: webbrowser.open_new_tab("https://" + "discord.gg" + "/" + "bKrt" + "rkJ"))
help_menu.addAction("&Github Repository", lambda: webbrowser.open_new_tab("https://github.com/khopa/dcs_liberation"))
help_menu.addAction("&Releases", lambda: webbrowser.open_new_tab("https://github.com/Khopa/dcs_liberation/releases"))
help_menu.addAction("&Online Manual", lambda: webbrowser.open_new_tab(URLS["Manual"]))
help_menu.addAction("&ED Forum Thread", lambda: webbrowser.open_new_tab(URLS["ForumThread"]))
help_menu.addAction("Report an &issue", lambda: webbrowser.open_new_tab(URLS["Issues"]))
help_menu.addSeparator()
help_menu.addAction(self.showAboutDialogAction)
def newGame(self):
wizard = NewGameWizard(self)
wizard.show()
@@ -216,7 +221,7 @@ class QLiberationWindow(QMainWindow):
"<h4>Authors</h4>" + \
"<p>DCS Liberation was originally developed by <b>shdwp</b>, DCS Liberation 2.0 is a partial rewrite based on this work by <b>Khopa</b>." \
"<h4>Contributors</h4>" + \
"shdwp, Khopa, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody" + \
"shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl" + \
"<h4>Special Thanks :</h4>" \
"<b>rp-</b> <i>for the pydcs framework</i><br/>"\
"<b>Grimes (mrSkortch)</b> & <b>Speed</b> <i>for the MIST framework</i><br/>"\

View File

@@ -4,8 +4,6 @@ from dcs.unittype import UnitType
from theater import db
class QRecruitBehaviour:
game = None

View File

@@ -1,4 +1,5 @@
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
@@ -7,19 +8,40 @@ from theater import ControlPoint, TheaterGroundObject
class QBaseDefenseGroupInfo(QGroupBox):
def __init__(self, cp:ControlPoint, ground_object: TheaterGroundObject, game):
def __init__(self, cp: ControlPoint, ground_object: TheaterGroundObject, game):
super(QBaseDefenseGroupInfo, self).__init__("Group : " + ground_object.obj_name)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.buildings = game.theater.find_ground_objects_by_obj_name(self.ground_object.obj_name)
self.main_layout = QVBoxLayout()
self.unit_layout = QGridLayout()
self.init_ui()
def init_ui(self):
self.buildLayout()
manage_button = QPushButton("Manage")
manage_button.setProperty("style", "btn-success")
manage_button.setMaximumWidth(180)
manage_button.clicked.connect(self.onManage)
self.main_layout.addLayout(self.unit_layout)
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
self.setLayout(self.main_layout)
def buildLayout(self):
unit_dict = {}
layout = QGridLayout()
for i in range(self.unit_layout.rowCount()):
for j in range(self.unit_layout.columnCount()):
item = self.unit_layout.itemAtPosition(i, j)
if item is not None and item.widget() is not None:
item.widget().setParent(None)
print("Remove " + str(i) + ", " + str(j))
for g in self.ground_object.groups:
for u in g.units:
if u.type in unit_dict.keys():
@@ -28,25 +50,27 @@ class QBaseDefenseGroupInfo(QGroupBox):
unit_dict[u.type] = 1
i = 0
for k, v in unit_dict.items():
#icon = QLabel()
#if k in VEHICLES_ICONS.keys():
# icon.setPixmap(VEHICLES_ICONS[k])
#else:
# icon.setText("<b>" + k[:6] + "</b>")
#icon.setProperty("style", "icon-plane")
#layout.addWidget(icon, i, 0)
layout.addWidget(QLabel(str(v) + " x " + "<strong>" + k + "</strong>"), i, 0)
icon = QLabel()
if k in VEHICLES_ICONS.keys():
icon.setPixmap(VEHICLES_ICONS[k])
else:
icon.setText("<b>" + k[:8] + "</b>")
icon.setProperty("style", "icon-armor")
self.unit_layout.addWidget(icon, i, 0)
self.unit_layout.addWidget(QLabel(str(v) + " x " + "<strong>" + k + "</strong>"), i, 1)
i = i + 1
manage_button = QPushButton("Manage")
manage_button.setProperty("style", "btn-success")
manage_button.setMaximumWidth(180)
manage_button.clicked.connect(self.onManage)
layout.addWidget(manage_button, i+1, 0)
self.setLayout(layout)
if len(unit_dict.items()) == 0:
self.unit_layout.addWidget(QLabel("/"), 0, 0)
self.setLayout(self.main_layout)
def onManage(self):
self.editionMenu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game)
self.editionMenu.show()
self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game)
self.edition_menu.show()
self.edition_menu.changed.connect(self.onEdition)
def onEdition(self):
self.buildLayout()

View File

@@ -23,7 +23,6 @@ class QBaseInformation(QFrame):
scroll_content = QWidget()
task_box_layout = QGridLayout()
scroll_content.setLayout(task_box_layout)
row = 0
for g in self.cp.ground_objects:
if g.airbase_group:

View File

@@ -1,12 +1,16 @@
import logging
from PySide2.QtGui import QCloseEvent
from PySide2.QtWidgets import QHBoxLayout, QWidget, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton
from PySide2 import QtCore
from PySide2.QtGui import Qt
from PySide2.QtWidgets import QHBoxLayout, QDialog, QGridLayout, QLabel, QGroupBox, QVBoxLayout, QPushButton, \
QComboBox, QSpinBox, QMessageBox
from dcs import Point
from game import Game
from game import Game, db
from game.data.building_data import FORTIFICATION_BUILDINGS
from game.db import PRICES, unit_type_of
from game.db import PRICES, unit_type_of, PinpointStrike
from gen.defenses.armor_group_generator import generate_armor_group_of_type_and_size
from gen.sam.sam_group_generator import get_faction_possible_sams_generator
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.QBudgetBox import QBudgetBox
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
@@ -16,6 +20,8 @@ from theater import ControlPoint, TheaterGroundObject
class QGroundObjectMenu(QDialog):
changed = QtCore.Signal()
def __init__(self, parent, ground_object: TheaterGroundObject, buildings:[], cp: ControlPoint, game: Game):
super(QGroundObjectMenu, self).__init__(parent)
self.setMinimumWidth(350)
@@ -29,6 +35,8 @@ class QGroundObjectMenu(QDialog):
self.buildingBox = QGroupBox("Buildings :")
self.intelLayout = QGridLayout()
self.buildingsLayout = QGridLayout()
self.sell_all_button = None
self.total_value = 0
self.init_ui()
def init_ui(self):
@@ -39,13 +47,32 @@ class QGroundObjectMenu(QDialog):
self.doLayout()
if len(self.ground_object.groups) > 0:
if self.ground_object.dcs_identifier == "AA":
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox)
self.actionLayout = QHBoxLayout()
self.sell_all_button = QPushButton("Disband (+" + str(self.total_value) + "M)")
self.sell_all_button.clicked.connect(self.sell_all)
self.sell_all_button.setProperty("style", "btn-danger")
self.buy_replace = QPushButton("Buy/Replace")
self.buy_replace.clicked.connect(self.buy_group)
self.buy_replace.setProperty("style", "btn-success")
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
self.mainLayout.addLayout(self.actionLayout)
self.setLayout(self.mainLayout)
def doLayout(self):
self.update_total_value()
self.intelBox = QGroupBox("Units :")
self.intelLayout = QGridLayout()
i = 0
@@ -71,6 +98,9 @@ class QGroundObjectMenu(QDialog):
repair.clicked.connect(lambda u=u, g=g, p=price: self.repair_unit(g, u, p))
self.intelLayout.addWidget(repair, i, 1)
i = i + 1
stretch = QVBoxLayout()
stretch.addStretch()
self.intelLayout.addLayout(stretch, i, 0)
self.buildingBox = QGroupBox("Buildings :")
self.buildingsLayout = QGridLayout()
@@ -86,14 +116,44 @@ class QGroundObjectMenu(QDialog):
def do_refresh_layout(self):
try:
for i in range(self.mainLayout.count()):
self.mainLayout.removeItem(self.mainLayout.itemAt(i))
item = self.mainLayout.itemAt(i)
if item is not None and item.widget() is not None:
item.widget().setParent(None)
self.sell_all_button.setParent(None)
self.buy_replace.setParent(None)
self.actionLayout.setParent(None)
self.doLayout()
if len(self.ground_object.groups) > 0:
if self.ground_object.dcs_identifier == "AA":
self.mainLayout.addWidget(self.intelBox)
else:
self.mainLayout.addWidget(self.buildingBox)
self.actionLayout = QHBoxLayout()
if self.total_value > 0:
self.actionLayout.addWidget(self.sell_all_button)
self.actionLayout.addWidget(self.buy_replace)
if self.cp.captured and self.ground_object.dcs_identifier == "AA":
self.mainLayout.addLayout(self.actionLayout)
except Exception as e:
print(e)
self.update_total_value()
self.changed.emit()
def update_total_value(self):
total_value = 0
for group in self.ground_object.groups:
for u in group.units:
utype = unit_type_of(u)
if utype in PRICES:
total_value = total_value + PRICES[utype]
else:
total_value = total_value + 1
if self.sell_all_button is not None:
self.sell_all_button.setText("Disband (+$" + str(self.total_value) + "M)")
self.total_value = total_value
def repair_unit(self, group, unit, price):
if self.game.budget > price:
@@ -112,6 +172,168 @@ class QGroundObjectMenu(QDialog):
logging.info("Repaired unit : " + str(unit.id) + " " + str(unit.type))
self.do_refresh_layout()
self.changed.emit()
def closeEvent(self, closeEvent: QCloseEvent):
GameUpdateSignal.get_instance().updateGame(self.game)
def sell_all(self):
self.update_total_value()
self.game.budget = self.game.budget + self.total_value
self.ground_object.groups = []
self.do_refresh_layout()
GameUpdateSignal.get_instance().updateBudget(self.game)
def buy_group(self):
self.subwindow = QBuyGroupForGroundObjectDialog(self, self.ground_object, self.cp, self.game, self.total_value)
self.subwindow.changed.connect(self.do_refresh_layout)
self.subwindow.show()
class QBuyGroupForGroundObjectDialog(QDialog):
changed = QtCore.Signal()
def __init__(self, parent, ground_object: TheaterGroundObject, cp: ControlPoint, game: Game, current_group_value: int):
super(QBuyGroupForGroundObjectDialog, self).__init__(parent)
self.setMinimumWidth(350)
self.ground_object = ground_object
self.cp = cp
self.game = game
self.current_group_value = current_group_value
self.setWindowTitle("Buy units @ " + self.ground_object.obj_name)
self.setWindowIcon(EVENT_ICONS["capture"])
self.buySamButton = QPushButton("Buy")
self.buyArmorButton = QPushButton("Buy")
self.buySamLayout = QGridLayout()
self.buyArmorLayout = QGridLayout()
self.amount = QSpinBox()
self.buyArmorCombo = QComboBox()
self.samCombo = QComboBox()
self.buySamBox = QGroupBox("Buy SAM site :")
self.buyArmorBox = QGroupBox("Buy defensive position :")
self.init_ui()
def init_ui(self):
faction = self.game.player_name
# Sams
possible_sams = get_faction_possible_sams_generator(faction)
for sam in possible_sams:
self.samCombo.addItem(sam.name + " [$" + str(sam.price) + "M]", userData=sam)
self.samCombo.currentIndexChanged.connect(self.samComboChanged)
self.buySamLayout.addWidget(QLabel("Site Type :"), 0, 0, Qt.AlignLeft)
self.buySamLayout.addWidget(self.samCombo, 0, 1, alignment=Qt.AlignRight)
self.buySamLayout.addWidget(self.buySamButton, 1, 1, alignment=Qt.AlignRight)
stretch = QVBoxLayout()
stretch.addStretch()
self.buySamLayout.addLayout(stretch, 2, 0)
self.buySamButton.clicked.connect(self.buySam)
# Armored units
armored_units = db.find_unittype(PinpointStrike, faction) # Todo : refactor this legacy nonsense
for unit in set(armored_units):
self.buyArmorCombo.addItem(db.unit_type_name_2(unit) + " [$" + str(db.PRICES[unit]) + "M]", userData=unit)
self.buyArmorCombo.currentIndexChanged.connect(self.armorComboChanged)
self.amount.setMinimum(2)
self.amount.setMaximum(8)
self.amount.setValue(2)
self.amount.valueChanged.connect(self.amountComboChanged)
self.buyArmorLayout.addWidget(QLabel("Unit type :"), 0, 0, Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.buyArmorCombo, 0, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(QLabel("Group size :"), 1, 0, alignment=Qt.AlignLeft)
self.buyArmorLayout.addWidget(self.amount, 1, 1, alignment=Qt.AlignRight)
self.buyArmorLayout.addWidget(self.buyArmorButton, 2, 1, alignment=Qt.AlignRight)
stretch2 = QVBoxLayout()
stretch2.addStretch()
self.buyArmorLayout.addLayout(stretch2, 3, 0)
self.buyArmorButton.clicked.connect(self.buyArmor)
# Do layout
self.buySamBox.setLayout(self.buySamLayout)
self.buyArmorBox.setLayout(self.buyArmorLayout)
self.mainLayout = QHBoxLayout()
self.mainLayout.addWidget(self.buySamBox)
if self.ground_object.airbase_group:
self.mainLayout.addWidget(self.buyArmorBox)
self.setLayout(self.mainLayout)
try:
self.samComboChanged(0)
self.armorComboChanged(0)
except:
pass
def samComboChanged(self, index):
self.buySamButton.setText("Buy [$" + str(self.samCombo.itemData(index).price) + "M] [-$" + str(self.current_group_value) + "M]")
def armorComboChanged(self, index):
self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(index)] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]")
def amountComboChanged(self):
self.buyArmorButton.setText("Buy [$" + str(db.PRICES[self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())] * self.amount.value()) + "M][-$" + str(self.current_group_value) + "M]")
def buyArmor(self):
print("Buy Armor ")
utype = self.buyArmorCombo.itemData(self.buyArmorCombo.currentIndex())
print(utype)
price = db.PRICES[utype] * self.amount.value() - self.current_group_value
if price > self.game.budget:
self.error_money()
self.close()
return
else:
self.game.budget -= price
# Generate Armor
group = generate_armor_group_of_type_and_size(self.game, self.ground_object, utype, int(self.amount.value()))
self.ground_object.groups = [group]
GameUpdateSignal.get_instance().updateBudget(self.game)
self.changed.emit()
self.close()
def buySam(self):
sam_generator = self.samCombo.itemData(self.samCombo.currentIndex())
price = sam_generator.price - self.current_group_value
if price > self.game.budget:
self.error_money()
return
else:
self.game.budget -= price
# Generate SAM
generator = sam_generator(self.game, self.ground_object)
generator.generate()
generated_group = generator.get_generated_group()
self.ground_object.groups = [generated_group]
GameUpdateSignal.get_instance().updateBudget(self.game)
self.changed.emit()
self.close()
def error_money(self):
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Not enough money to buy these units !")
msg.setWindowTitle("Not enough money")
msg.setStandardButtons(QMessageBox.Ok)
msg.setWindowFlags(Qt.WindowStaysOnTopHint)
msg.exec_()
self.close()

View File

@@ -17,7 +17,6 @@ class QMissionPlanning(QDialog):
self.game = game
self.setWindowFlags(Qt.WindowStaysOnTopHint)
self.setMinimumSize(1000, 440)
self.setModal(True)
self.setWindowTitle("Mission Preparation")
self.setWindowIcon(EVENT_ICONS["strike"])
self.init_ui()

View File

@@ -12,7 +12,7 @@ import qt_ui.uiconstants as CONST
from game import db, Game
from game.settings import Settings
from gen import namegen
from qt_ui.windows.newgame.QCampaignList import QCampaignList
from qt_ui.windows.newgame.QCampaignList import QCampaignList, CAMPAIGNS
from theater import start_generator, persiangulf, nevada, caucasus, ConflictTheater, normandy, thechannel
@@ -42,6 +42,8 @@ class NewGameWizard(QtWidgets.QWizard):
redFaction = [c for c in db.FACTIONS][self.field("redFaction")]
selectedCampaign = self.field("selectedCampaign")
if selectedCampaign is None:
selectedCampaign = CAMPAIGNS[0]
conflictTheater = selectedCampaign[1]()
timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]]

View File

@@ -2,3 +2,6 @@
Pyside2>=5.13.0
pyinstaller==3.6
pyproj==2.6.1.post1
Pillow~=7.2.0
tabulate~=0.8.7

View File

@@ -2,41 +2,84 @@ local unitPayloads = {
["name"] = "F-16C_50",
["payloads"] = {
[1] = {
["name"] = "ANTISHIP",
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "LAU3_HE5",
["num"] = 6,
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
[2] = {
["CLSID"] = "LAU3_HE5",
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 7,
},
[3] = {
["CLSID"] = "LAU3_HE5",
["num"] = 4,
},
[4] = {
["CLSID"] = "LAU3_HE5",
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 3,
},
[5] = {
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 2,
},
[6] = {
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[7] = {
[6] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 8,
},
[8] = {
[7] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[8] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
},
["tasks"] = {
},
},
[2] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 7,
},
[2] = {
["CLSID"] = "{DAC53A2F-79CA-42FF-A77A-F5649B601308}",
["num"] = 3,
},
[3] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 2,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 8,
},
[6] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
[8] = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
@@ -44,7 +87,7 @@ local unitPayloads = {
["tasks"] = {
},
},
[2] = {
[3] = {
["name"] = "CAP",
["pylons"] = {
[1] = {
@@ -87,53 +130,6 @@ local unitPayloads = {
["tasks"] = {
},
},
[3] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
[2] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["num"] = 7,
},
[3] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["num"] = 3,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 2,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[6] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 8,
},
[7] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 9,
},
[8] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["num"] = 6,
},
[11] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
},
["tasks"] = {
},
},
[4] = {
["name"] = "STRIKE",
["pylons"] = {
@@ -142,11 +138,11 @@ local unitPayloads = {
["num"] = 7,
},
[2] = {
["CLSID"] = "{TER_9A_2L*MK-82}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 4,
},
[3] = {
["CLSID"] = "{TER_9A_2R*MK-82}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 6,
},
[4] = {
@@ -173,7 +169,7 @@ local unitPayloads = {
["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}",
["num"] = 5,
},
[11] = {
[10] = {
["CLSID"] = "{A111396E-D3E8-4b9c-8AC9-2432489304D5}",
["num"] = 11,
},
@@ -185,19 +181,19 @@ local unitPayloads = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 6,
},
[2] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
["num"] = 7,
},
[3] = {
["CLSID"] = "{DB769D48-67D7-42ED-A2BE-108D566C8B1E}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
[4] = {
["CLSID"] = "{5335D97A-35A5-4643-9D9B-026C75961E52}",
["CLSID"] = "{B06DD79A-F21E-4EB9-BD9D-AB3844618C93}",
["num"] = 3,
},
[5] = {

View File

@@ -2,26 +2,6 @@ local unitPayloads = {
["name"] = "P-47D-30",
["payloads"] = {
[1] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[2] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
@@ -41,14 +21,34 @@ local unitPayloads = {
[1] = 11,
},
},
[3] = {
["name"] = "ANTISHIP",
[2] = {
["name"] = "ANTISTRIKE",
["pylons"] = {
},
["tasks"] = {
[1] = 11,
},
},
[3] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[4] = {
["name"] = "CAP",
["pylons"] = {
@@ -77,6 +77,25 @@ local unitPayloads = {
[1] = 11,
},
},
[6] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
},
},
},
["tasks"] = {
},

View File

@@ -0,0 +1,96 @@
local unitPayloads = {
["name"] = "P-47D-30bl1",
["payloads"] = {
[1] = {
["name"] = "CAP",
["pylons"] = {
},
["tasks"] = {
[1] = 11,
},
},
[2] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{AN_M57}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{AN_M57}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN_M57}",
["num"] = 3,
},
},
["tasks"] = {
[1] = 11,
},
},
[3] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[4] = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[5] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
},
},
},
["tasks"] = {
},
["unitType"] = "P-47D-30bl1",
}
return unitPayloads

View File

@@ -0,0 +1,88 @@
local unitPayloads = {
["name"] = "P-47D-40",
["payloads"] = {
[1] = {
["name"] = "CAP",
["pylons"] = {
},
["tasks"] = {
[1] = 11,
},
},
[2] = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}",
["num"] = 4,
},
[2] = {
["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 11,
},
},
[3] = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}",
["num"] = 4,
},
[2] = {
["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}",
["num"] = 5,
},
},
["tasks"] = {
[1] = 11,
},
},
[4] = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{AN-M64}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{AN-M64}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 11,
},
},
[5] = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{P47_5_HVARS_ON_RIGHT_WING_RAILS}",
["num"] = 5,
},
[2] = {
["CLSID"] = "{P47_5_HVARS_ON_LEFT_WING_RAILS}",
["num"] = 4,
},
[3] = {
["CLSID"] = "{AN-M64}",
["num"] = 1,
},
},
["tasks"] = {
},
},
},
["tasks"] = {
},
["unitType"] = "P-47D-40",
}
return unitPayloads

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
[
{
"name": "",
"callsign": "ICRR",
"beacon_type": 15,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "ICRR",
"beacon_type": 14,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "ICRS",
"beacon_type": 14,
"hertz": 108500000,
"channel": 22
},
{
"name": "",
"callsign": "ICRS",
"beacon_type": 15,
"hertz": 108500000,
"channel": 22
},
{
"name": "Indian Springs",
"callsign": "INS",
"beacon_type": 5,
"hertz": null,
"channel": 87
},
{
"name": "",
"callsign": "GLRI",
"beacon_type": 14,
"hertz": 109300000,
"channel": 30
},
{
"name": "",
"callsign": "GLRI",
"beacon_type": 15,
"hertz": 109300000,
"channel": 30
},
{
"name": "Groom Lake",
"callsign": "GRL",
"beacon_type": 5,
"hertz": null,
"channel": 18
},
{
"name": "",
"callsign": "I-RLE",
"beacon_type": 15,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "I-LAS",
"beacon_type": 15,
"hertz": 110300000,
"channel": 40
},
{
"name": "",
"callsign": "I-RLE",
"beacon_type": 14,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "I-LAS",
"beacon_type": 14,
"hertz": 110300000,
"channel": 40
},
{
"name": "Las Vegas",
"callsign": "LAS",
"beacon_type": 6,
"hertz": 116900000,
"channel": 116
},
{
"name": "",
"callsign": "IDIQ",
"beacon_type": 15,
"hertz": 109100000,
"channel": null
},
{
"name": "Nellis",
"callsign": "LSV",
"beacon_type": 5,
"hertz": null,
"channel": 12
},
{
"name": "",
"callsign": "IDIQ",
"beacon_type": 14,
"hertz": 109100000,
"channel": null
},
{
"name": "",
"callsign": "I-HWG",
"beacon_type": 14,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "I-HWG",
"beacon_type": 15,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "I-RVP",
"beacon_type": 14,
"hertz": 108300000,
"channel": null
},
{
"name": "",
"callsign": "I-UVV",
"beacon_type": 14,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "I-UVV",
"beacon_type": 15,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "I-RVP",
"beacon_type": 15,
"hertz": 108300000,
"channel": null
},
{
"name": "Silverbow",
"callsign": "TQQ",
"beacon_type": 6,
"hertz": 113000000,
"channel": 77
},
{
"name": "St George",
"callsign": "UTI",
"beacon_type": 4,
"hertz": 108600000,
"channel": 23
},
{
"name": "Grand Canyon",
"callsign": "GCN",
"beacon_type": 4,
"hertz": 113100000,
"channel": 78
},
{
"name": "Kingman",
"callsign": "IGM",
"beacon_type": 4,
"hertz": 108800000,
"channel": 25
},
{
"name": "Colorado City",
"callsign": "AZC",
"beacon_type": 10,
"hertz": 403000,
"channel": null
},
{
"name": "Meggi",
"callsign": "EC",
"beacon_type": 10,
"hertz": 217000,
"channel": null
},
{
"name": "Daggett",
"callsign": "DAG",
"beacon_type": 6,
"hertz": 113200000,
"channel": 79
},
{
"name": "Hector",
"callsign": "HEC",
"beacon_type": 6,
"hertz": 112700000,
"channel": 74
},
{
"name": "Needles",
"callsign": "EED",
"beacon_type": 6,
"hertz": 115200000,
"channel": 99
},
{
"name": "Milford",
"callsign": "MLF",
"beacon_type": 6,
"hertz": 112100000,
"channel": 58
},
{
"name": "GOFFS",
"callsign": "GFS",
"beacon_type": 6,
"hertz": 114400000,
"channel": 91
},
{
"name": "Tonopah",
"callsign": "TPH",
"beacon_type": 6,
"hertz": 117200000,
"channel": 119
},
{
"name": "Mina",
"callsign": "MVA",
"beacon_type": 6,
"hertz": 115100000,
"channel": 98
},
{
"name": "Wilson Creek",
"callsign": "ILC",
"beacon_type": 6,
"hertz": 116300000,
"channel": 110
},
{
"name": "Cedar City",
"callsign": "CDC",
"beacon_type": 6,
"hertz": 117300000,
"channel": 120
},
{
"name": "Bryce Canyon",
"callsign": "BCE",
"beacon_type": 6,
"hertz": 112800000,
"channel": 75
},
{
"name": "Mormon Mesa",
"callsign": "MMM",
"beacon_type": 6,
"hertz": 114300000,
"channel": 90
},
{
"name": "Beatty",
"callsign": "BTY",
"beacon_type": 6,
"hertz": 114700000,
"channel": 94
},
{
"name": "Bishop",
"callsign": "BIH",
"beacon_type": 6,
"hertz": 109600000,
"channel": 33
},
{
"name": "Coaldale",
"callsign": "OAL",
"beacon_type": 6,
"hertz": 117700000,
"channel": 124
},
{
"name": "Peach Springs",
"callsign": "PGS",
"beacon_type": 6,
"hertz": 112000000,
"channel": 57
},
{
"name": "Boulder City",
"callsign": "BLD",
"beacon_type": 6,
"hertz": 116700000,
"channel": 114
},
{
"name": "Mercury",
"callsign": "MCY",
"beacon_type": 10,
"hertz": 326000,
"channel": null
}
]

View File

@@ -0,0 +1 @@
[]

View File

@@ -0,0 +1,709 @@
[
{
"name": "ABUDHABI",
"callsign": "ADV",
"beacon_type": 2,
"hertz": 114250000,
"channel": null
},
{
"name": "AbuDhabiInt",
"callsign": "ADV",
"beacon_type": 3,
"hertz": 114250000,
"channel": 119
},
{
"name": "Abumusa",
"callsign": "ABM",
"beacon_type": 3,
"hertz": 285000,
"channel": 101
},
{
"name": "AlAinInt",
"callsign": "ALN",
"beacon_type": 4,
"hertz": 112600000,
"channel": 119
},
{
"name": "AlBateenInt",
"callsign": "ALB",
"beacon_type": 2,
"hertz": 114000000,
"channel": 119
},
{
"name": "BandarAbbas",
"callsign": "BND",
"beacon_type": 4,
"hertz": 117200000,
"channel": 119
},
{
"name": "BandarAbbas",
"callsign": "BND",
"beacon_type": 9,
"hertz": 250000,
"channel": null
},
{
"name": "",
"callsign": "IBND",
"beacon_type": 14,
"hertz": 333800000,
"channel": null
},
{
"name": "",
"callsign": "IBND",
"beacon_type": 15,
"hertz": 333800000,
"channel": null
},
{
"name": "BandarAbbas",
"callsign": "BND",
"beacon_type": 5,
"hertz": null,
"channel": 78
},
{
"name": "BandarEJask",
"callsign": "KHM",
"beacon_type": 4,
"hertz": 116300000,
"channel": null
},
{
"name": "BandarEJask",
"callsign": "JSK",
"beacon_type": 9,
"hertz": 349000000,
"channel": null
},
{
"name": "BandarLengeh",
"callsign": "LEN",
"beacon_type": 9,
"hertz": 408000,
"channel": null
},
{
"name": "BandarLengeh",
"callsign": "LEN",
"beacon_type": 4,
"hertz": 114800000,
"channel": 95
},
{
"name": "",
"callsign": "MMA",
"beacon_type": 15,
"hertz": 111100000,
"channel": 48
},
{
"name": "",
"callsign": "LMA",
"beacon_type": 15,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "IMA",
"beacon_type": 15,
"hertz": 109100000,
"channel": 28
},
{
"name": "",
"callsign": "RMA",
"beacon_type": 15,
"hertz": 114900000,
"channel": 24
},
{
"name": "",
"callsign": "MMA",
"beacon_type": 14,
"hertz": 111100000,
"channel": 48
},
{
"name": "",
"callsign": "RMA",
"beacon_type": 14,
"hertz": 114900000,
"channel": 24
},
{
"name": "",
"callsign": "LMA",
"beacon_type": 14,
"hertz": 108700000,
"channel": 24
},
{
"name": "",
"callsign": "IMA",
"beacon_type": 14,
"hertz": 109100000,
"channel": 28
},
{
"name": "AlDhafra",
"callsign": "MA",
"beacon_type": 6,
"hertz": 114900000,
"channel": 96
},
{
"name": "",
"callsign": "IDBW",
"beacon_type": 14,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IDBR",
"beacon_type": 14,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "IDBE",
"beacon_type": 14,
"hertz": 111300000,
"channel": null
},
{
"name": "",
"callsign": "IDBL",
"beacon_type": 14,
"hertz": 110900000,
"channel": null
},
{
"name": "",
"callsign": "IDBL",
"beacon_type": 15,
"hertz": 110900000,
"channel": null
},
{
"name": "",
"callsign": "IDBR",
"beacon_type": 15,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "IDBE",
"beacon_type": 15,
"hertz": 111300000,
"channel": null
},
{
"name": "",
"callsign": "IDBW",
"beacon_type": 15,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IJEA",
"beacon_type": 14,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "IJWA",
"beacon_type": 15,
"hertz": 109750000,
"channel": null
},
{
"name": "",
"callsign": "IJEA",
"beacon_type": 15,
"hertz": 111750000,
"channel": null
},
{
"name": "",
"callsign": "IJWA",
"beacon_type": 14,
"hertz": 109750000,
"channel": null
},
{
"name": "Fujairah",
"callsign": "FJV",
"beacon_type": 4,
"hertz": 113800000,
"channel": 85
},
{
"name": "",
"callsign": "IFJR",
"beacon_type": 15,
"hertz": 111500000,
"channel": null
},
{
"name": "",
"callsign": "IFJR",
"beacon_type": 14,
"hertz": 111500000,
"channel": null
},
{
"name": "Havadarya",
"callsign": "HDR",
"beacon_type": 5,
"hertz": 111000000,
"channel": 47
},
{
"name": "",
"callsign": "IBHD",
"beacon_type": 14,
"hertz": 108900000,
"channel": null
},
{
"name": "",
"callsign": "IBHD",
"beacon_type": 15,
"hertz": 108900000,
"channel": null
},
{
"name": "Jiroft",
"callsign": "JIR",
"beacon_type": 10,
"hertz": 276000,
"channel": null
},
{
"name": "KERMAN",
"callsign": "KER",
"beacon_type": 5,
"hertz": 122500000,
"channel": 97
},
{
"name": "KERMAN",
"callsign": "KER",
"beacon_type": 4,
"hertz": 112000000,
"channel": 57
},
{
"name": "KERMAN",
"callsign": "KER",
"beacon_type": 3,
"hertz": 290000000,
"channel": null
},
{
"name": "",
"callsign": "IBKS",
"beacon_type": 14,
"hertz": 110300000,
"channel": null
},
{
"name": "",
"callsign": "IBKS",
"beacon_type": 15,
"hertz": 110300000,
"channel": null
},
{
"name": "KishIsland",
"callsign": "KIH",
"beacon_type": 9,
"hertz": 201000000,
"channel": null
},
{
"name": "KishIsland",
"callsign": "KIH",
"beacon_type": 5,
"hertz": null,
"channel": 112
},
{
"name": "LAR",
"callsign": "LAR",
"beacon_type": 4,
"hertz": 117900000,
"channel": null
},
{
"name": "LAR",
"callsign": "OISL",
"beacon_type": 9,
"hertz": 224000,
"channel": null
},
{
"name": "LavanIsland",
"callsign": "LVA",
"beacon_type": 4,
"hertz": 116850000,
"channel": 115
},
{
"name": "LavanIsland",
"callsign": "LVA",
"beacon_type": 9,
"hertz": 310000000,
"channel": 0
},
{
"name": "LiwaAirbase",
"callsign": "\u00c4\u00bc",
"beacon_type": 7,
"hertz": null,
"channel": 121
},
{
"name": "Minhad",
"callsign": "MIN",
"beacon_type": 5,
"hertz": 115200000,
"channel": 99
},
{
"name": "",
"callsign": "IMNW",
"beacon_type": 14,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "IMNW",
"beacon_type": 15,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "IMNR",
"beacon_type": 14,
"hertz": 110750000,
"channel": null
},
{
"name": "",
"callsign": "IMNR",
"beacon_type": 15,
"hertz": 110750000,
"channel": null
},
{
"name": "GheshmIsland",
"callsign": "KHM",
"beacon_type": 9,
"hertz": 233000,
"channel": null
},
{
"name": "GheshmIsland",
"callsign": "KHM",
"beacon_type": 4,
"hertz": 117100000,
"channel": null
},
{
"name": "RasAlKhaimah",
"callsign": "OMRK",
"beacon_type": 4,
"hertz": 113600000,
"channel": 83
},
{
"name": "SasAlNakheelAirport",
"callsign": "SAS",
"beacon_type": 10,
"hertz": 128925,
"channel": null
},
{
"name": "SasAlNakheel",
"callsign": "SAS",
"beacon_type": 4,
"hertz": 128925000,
"channel": 119
},
{
"name": "",
"callsign": "ISRE",
"beacon_type": 14,
"hertz": 108550000,
"channel": null
},
{
"name": "",
"callsign": "ISHW",
"beacon_type": 14,
"hertz": 111950000,
"channel": null
},
{
"name": "",
"callsign": "ISHW",
"beacon_type": 15,
"hertz": 111950000,
"channel": null
},
{
"name": "",
"callsign": "ISRE",
"beacon_type": 15,
"hertz": 108550000,
"channel": null
},
{
"name": "SHIRAZ",
"callsign": "SYZ",
"beacon_type": 4,
"hertz": 117800000,
"channel": 125
},
{
"name": "SHIRAZ",
"callsign": "SYZ1",
"beacon_type": 5,
"hertz": 114700000,
"channel": 94
},
{
"name": "SHIRAZ",
"callsign": "SR",
"beacon_type": 9,
"hertz": 205000,
"channel": null
},
{
"name": "",
"callsign": "ISYZ",
"beacon_type": 15,
"hertz": 109900000,
"channel": null
},
{
"name": "",
"callsign": "ISYZ",
"beacon_type": 14,
"hertz": 109900000,
"channel": null
},
{
"name": "SirriIsland",
"callsign": "SIR",
"beacon_type": 9,
"hertz": 300000,
"channel": null
},
{
"name": "SirriIsland",
"callsign": "SIR",
"beacon_type": 4,
"hertz": 113750000,
"channel": null
},
{
"name": "Kochak",
"callsign": "KCK",
"beacon_type": 5,
"hertz": 114200000,
"channel": 89
},
{
"name": "Kish",
"callsign": "KIS",
"beacon_type": 4,
"hertz": 117400000,
"channel": 121
},
{
"name": "DohaAirport",
"callsign": "DIA",
"beacon_type": 4,
"hertz": 112400000,
"channel": 71
},
{
"name": "HamadInternationalAirport",
"callsign": "DOH",
"beacon_type": 4,
"hertz": 114400000,
"channel": 91
},
{
"name": "DezfulAirport",
"callsign": "DZF",
"beacon_type": 9,
"hertz": 293000000,
"channel": null
},
{
"name": "AbadanIntAirport",
"callsign": "ABD",
"beacon_type": 4,
"hertz": 115100000,
"channel": 98
},
{
"name": "AhvazIntAirport",
"callsign": "AWZ",
"beacon_type": 4,
"hertz": 114000000,
"channel": 87
},
{
"name": "AghajariAirport",
"callsign": "AJR",
"beacon_type": 4,
"hertz": 114900000,
"channel": 96
},
{
"name": "BirjandIntAirport",
"callsign": "BJD",
"beacon_type": 4,
"hertz": 113500000,
"channel": 82
},
{
"name": "BushehrIntAirport",
"callsign": "BUZ",
"beacon_type": 4,
"hertz": 117450000,
"channel": 121
},
{
"name": "KonarakAirport",
"callsign": "CBH",
"beacon_type": 4,
"hertz": 115600000,
"channel": 103
},
{
"name": "IsfahanIntAirport",
"callsign": "ISN",
"beacon_type": 4,
"hertz": 113200000,
"channel": 79
},
{
"name": "KhoramabadAirport",
"callsign": "KRD",
"beacon_type": 4,
"hertz": 113750000,
"channel": 84
},
{
"name": "PersianGulfIntAirport",
"callsign": "PRG",
"beacon_type": 4,
"hertz": 112100000,
"channel": 58
},
{
"name": "YasoujAirport",
"callsign": "YSJ",
"beacon_type": 4,
"hertz": 116550000,
"channel": 112
},
{
"name": "BamAirport",
"callsign": "BAM",
"beacon_type": 4,
"hertz": 114900000,
"channel": 96
},
{
"name": "MahshahrAirport",
"callsign": "MAH",
"beacon_type": 4,
"hertz": 115800000,
"channel": 105
},
{
"name": "IranShahrAirport",
"callsign": "ISR",
"beacon_type": 4,
"hertz": 117000000,
"channel": 117
},
{
"name": "LamerdAirport",
"callsign": "LAM",
"beacon_type": 4,
"hertz": 117000000,
"channel": 117
},
{
"name": "SirjanAirport",
"callsign": "SRJ",
"beacon_type": 4,
"hertz": 114600000,
"channel": 93
},
{
"name": "YazdIntAirport",
"callsign": "YZD",
"beacon_type": 4,
"hertz": 117700000,
"channel": 124
},
{
"name": "ZabolAirport",
"callsign": "ZAL",
"beacon_type": 4,
"hertz": 113100000,
"channel": 78
},
{
"name": "ZahedanIntAirport",
"callsign": "ZDN",
"beacon_type": 4,
"hertz": 116000000,
"channel": 107
},
{
"name": "RafsanjanAirport",
"callsign": "RAF",
"beacon_type": 4,
"hertz": 112300000,
"channel": 70
},
{
"name": "SaravanAirport",
"callsign": "SRN",
"beacon_type": 4,
"hertz": 114100000,
"channel": 88
},
{
"name": "BuHasa",
"callsign": "BH",
"beacon_type": 3,
"hertz": 309000000,
"channel": null
}
]

View File

@@ -0,0 +1,408 @@
[
{
"name": "Deir ez-Zor",
"callsign": "DRZ",
"beacon_type": 10,
"hertz": 295000,
"channel": null
},
{
"name": "GAZIANTEP",
"callsign": "GAZ",
"beacon_type": 10,
"hertz": 432000,
"channel": null
},
{
"name": "BANIAS",
"callsign": "BAN",
"beacon_type": 10,
"hertz": 304000,
"channel": null
},
{
"name": "ALEPPO",
"callsign": "ALE",
"beacon_type": 10,
"hertz": 396000,
"channel": null
},
{
"name": "KAHRAMANMARAS",
"callsign": "KHM",
"beacon_type": 10,
"hertz": 374000,
"channel": null
},
{
"name": "MEZZEH",
"callsign": "MEZ",
"beacon_type": 10,
"hertz": 358000,
"channel": null
},
{
"name": "KLEYATE",
"callsign": "RA",
"beacon_type": 10,
"hertz": 450000,
"channel": null
},
{
"name": "KARIATAIN",
"callsign": "KTN",
"beacon_type": 10,
"hertz": 372500,
"channel": null
},
{
"name": "ALEPPO",
"callsign": "MER",
"beacon_type": 10,
"hertz": 365000,
"channel": null
},
{
"name": "TURAIF",
"callsign": "TRF",
"beacon_type": 4,
"hertz": 116100000,
"channel": null
},
{
"name": "Deir ez-Zor",
"callsign": "DRZ",
"beacon_type": 4,
"hertz": 117000000,
"channel": null
},
{
"name": "BAYSUR",
"callsign": "BAR",
"beacon_type": 4,
"hertz": 113900000,
"channel": null
},
{
"name": "ALEPPO",
"callsign": "ALE",
"beacon_type": 4,
"hertz": 114500000,
"channel": null
},
{
"name": "MARKA",
"callsign": "AMN",
"beacon_type": 4,
"hertz": 116300000,
"channel": null
},
{
"name": "GAZIANTEP",
"callsign": "GAZ",
"beacon_type": 4,
"hertz": 116700000,
"channel": null
},
{
"name": "ROSH-PINA",
"callsign": "ROP",
"beacon_type": 4,
"hertz": 115300000,
"channel": null
},
{
"name": "TANF",
"callsign": "TAN",
"beacon_type": 4,
"hertz": 114000000,
"channel": null
},
{
"name": "NATANIA",
"callsign": "NAT",
"beacon_type": 4,
"hertz": 112400000,
"channel": null
},
{
"name": "KAHRAMANMARAS",
"callsign": "KHM",
"beacon_type": 4,
"hertz": 113900000,
"channel": null
},
{
"name": "KARIATAIN",
"callsign": "KTN",
"beacon_type": 4,
"hertz": 117700000,
"channel": null
},
{
"name": "",
"callsign": "IADA",
"beacon_type": 14,
"hertz": 108700000,
"channel": null
},
{
"name": "",
"callsign": "IADA",
"beacon_type": 15,
"hertz": 108700000,
"channel": null
},
{
"name": "ADANA",
"callsign": "ADN",
"beacon_type": 11,
"hertz": 395000000,
"channel": null
},
{
"name": "ADANA",
"callsign": "ADA",
"beacon_type": 4,
"hertz": 112700000,
"channel": null
},
{
"name": "KALDE",
"callsign": "KAD",
"beacon_type": 4,
"hertz": 112600000,
"channel": null
},
{
"name": "",
"callsign": "IBB",
"beacon_type": 15,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "IKK",
"beacon_type": 14,
"hertz": 110700000,
"channel": null
},
{
"name": "",
"callsign": "BIL",
"beacon_type": 14,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IBB",
"beacon_type": 14,
"hertz": 110100000,
"channel": null
},
{
"name": "",
"callsign": "BIL",
"beacon_type": 15,
"hertz": 109500000,
"channel": null
},
{
"name": "",
"callsign": "IKK",
"beacon_type": 15,
"hertz": 110700000,
"channel": null
},
{
"name": "BEIRUT",
"callsign": "BOD",
"beacon_type": 11,
"hertz": 351000000,
"channel": null
},
{
"name": "",
"callsign": "IDA",
"beacon_type": 15,
"hertz": 109900000,
"channel": null
},
{
"name": "",
"callsign": "IDA",
"beacon_type": 14,
"hertz": 109900000,
"channel": null
},
{
"name": "Damascus",
"callsign": "DAM",
"beacon_type": 4,
"hertz": 116000000,
"channel": null
},
{
"name": "",
"callsign": "DAML",
"beacon_type": 14,
"hertz": 111100000,
"channel": null
},
{
"name": "DAMASCUS",
"callsign": "DAL",
"beacon_type": 11,
"hertz": 342000000,
"channel": null
},
{
"name": "ABYAD",
"callsign": "ABD",
"beacon_type": 10,
"hertz": 264000,
"channel": null
},
{
"name": "",
"callsign": "DAML",
"beacon_type": 15,
"hertz": 111100000,
"channel": null
},
{
"name": "HATAY",
"callsign": "HTY",
"beacon_type": 4,
"hertz": 112050000,
"channel": null
},
{
"name": "",
"callsign": "IHAT",
"beacon_type": 14,
"hertz": 108900000,
"channel": null
},
{
"name": "",
"callsign": "IHAT",
"beacon_type": 15,
"hertz": 108900000,
"channel": null
},
{
"name": "HATAY",
"callsign": "HTY",
"beacon_type": 10,
"hertz": 336000,
"channel": null
},
{
"name": "",
"callsign": "IHTY",
"beacon_type": 15,
"hertz": 108150000,
"channel": null
},
{
"name": "",
"callsign": "IHTY",
"beacon_type": 14,
"hertz": 108150000,
"channel": null
},
{
"name": "INCIRLIC",
"callsign": "DAN",
"beacon_type": 6,
"hertz": 108400000,
"channel": 21
},
{
"name": "",
"callsign": "IDAN",
"beacon_type": 14,
"hertz": 109300000,
"channel": null
},
{
"name": "",
"callsign": "IDAN",
"beacon_type": 15,
"hertz": 109300000,
"channel": null
},
{
"name": "",
"callsign": "DANM",
"beacon_type": 15,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "DANM",
"beacon_type": 14,
"hertz": 111700000,
"channel": null
},
{
"name": "",
"callsign": "IBA",
"beacon_type": 15,
"hertz": 109100000,
"channel": null
},
{
"name": "",
"callsign": "IBA",
"beacon_type": 14,
"hertz": 109100000,
"channel": null
},
{
"name": "LATAKIA",
"callsign": "LTK",
"beacon_type": 4,
"hertz": 114800000,
"channel": null
},
{
"name": "LATAKIA",
"callsign": "LTK",
"beacon_type": 11,
"hertz": 414000000,
"channel": null
},
{
"name": "PALMYRA",
"callsign": "PLR",
"beacon_type": 10,
"hertz": 363000,
"channel": null
},
{
"name": "PALMYRA",
"callsign": "PAL",
"beacon_type": 10,
"hertz": 337000,
"channel": null
},
{
"name": "RAMATDAVID",
"callsign": "RMD",
"beacon_type": 10,
"hertz": 368000,
"channel": null
},
{
"name": "Cheka",
"callsign": "CAK",
"beacon_type": 4,
"hertz": 116200000,
"channel": null
}
]

Binary file not shown.

38
resources/fonts/OFL.txt Normal file
View File

@@ -0,0 +1,38 @@
—————————————————————————————-
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
—————————————————————————————-
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
“Font Software” refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
“Reserved Font Name” refers to any names specified as such after the copyright statement(s).
“Original Version” refers to the collection of Font Software components as distributed by the Copyright Holder(s).
“Modified Version” refers to any derivative made by adding to, deleting, or substituting—in part or in whole—any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
“Author” refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,29 @@
# this is a list of lua scripts that will be injected in the mission, in the same order
mist.lua
Moose.lua
CTLD.lua
NIOD.lua
WeatherMark.lua
veaf.lua
dcsUnits.lua
# JTACAutoLase is an empty file, only there to disable loading the official script (already included in CTLD)
JTACAutoLase.lua
veafAssets.lua
veafCarrierOperations.lua
veafCarrierOperations2.lua
veafCasMission.lua
veafCombatMission.lua
veafCombatZone.lua
veafGrass.lua
veafInterpreter.lua
veafMarkers.lua
veafMove.lua
veafNamedPoints.lua
veafRadio.lua
veafRemote.lua
veafSecurity.lua
veafShortcuts.lua
veafSpawn.lua
veafTransportMission.lua
veafUnits.lua
missionConfig.lua

View File

@@ -0,0 +1,29 @@
rem this can be used to easily create hardlinks from your plugin development folder
mklink mist.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\mist.lua
mklink Moose.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\Moose.lua
mklink CTLD.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\CTLD.lua
mklink NIOD.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\NIOD.lua
mklink WeatherMark.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\community\WeatherMark.lua
mklink veaf.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veaf.lua
mklink dcsUnits.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\dcsUnits.lua
mklink JTACAutoLase.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\JTACAutoLase.lua
mklink veafAssets.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafAssets.lua
mklink veafCarrierOperations.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCarrierOperations.lua
mklink veafCarrierOperations2.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCarrierOperations2.lua
mklink veafCasMission.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCasMission.lua
mklink veafCombatMission.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCombatMission.lua
mklink veafCombatZone.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafCombatZone.lua
mklink veafGrass.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafGrass.lua
mklink veafInterpreter.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafInterpreter.lua
mklink veafMarkers.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafMarkers.lua
mklink veafMove.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafMove.lua
mklink veafNamedPoints.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafNamedPoints.lua
mklink veafRadio.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafRadio.lua
mklink veafRemote.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafRemote.lua
mklink veafSecurity.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafSecurity.lua
mklink veafShortcuts.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafShortcuts.lua
mklink veafSpawn.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafSpawn.lua
mklink veafTransportMission.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafTransportMission.lua
mklink veafUnits.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\veafUnits.lua
mklink missionConfig.lua d:\dev\_VEAF\VEAF-Mission-Creation-Tools\src\scripts\veaf\missionConfig.lua

View File

@@ -198,6 +198,15 @@ QLabel[style="icon-plane"]{
color:white;
}
QLabel[style="icon-armor"]{
background-color:#48719D;
min-height:24px;
max-width: 64px;
border: 1px solid black;
text-align:center;
color:white;
}
QLabel[style="BARCAP"]{
border: 1px solid black;
background-color: #445299;

View File

@@ -106,6 +106,15 @@ QLabel[style="icon-plane"]{
color:white;
}
QLabel[style="icon-armor"]{
background-color:#48719D;
min-height:24px;
max-width: 64px;
border: 1px solid black;
text-align:center;
color:white;
}
QLabel[style="bordered"]{
border: 1px solid black;
}

View File

@@ -1,6 +1,6 @@
import os
import sys
from pydcs import dcs
import dcs
from game import db
from gen.aircraft import AircraftConflictGenerator
@@ -30,6 +30,6 @@ for t, uts in db.UNIT_BY_TASK.items():
altitude=10000
)
g.task = t.name
airgen._setup_group(g, t, 0)
airgen._setup_group(g, t, 0, {})
mis.save("loadout_test.miz")

View File

@@ -0,0 +1,183 @@
"""Generates resources/dcs/beacons.json from the DCS installation.
DCS has a beacons.lua file for each terrain mod that includes information about
the radio beacons present on the map:
beacons = {
{
display_name = _('INCIRLIC');
beaconId = 'airfield16_0';
type = BEACON_TYPE_VORTAC;
callsign = 'DAN';
frequency = 108400000.000000;
channel = 21;
position = { 222639.437500, 73.699811, -33216.257813 };
direction = 0.000000;
positionGeo = { latitude = 37.015611, longitude = 35.448194 };
sceneObjects = {'t:124814096'};
};
...
}
"""
import argparse
from contextlib import contextmanager
import dataclasses
import gettext
import os
from pathlib import Path
import textwrap
from typing import Dict, Iterable, Union
import lupa
import game # Needed to resolve cyclic import, for some reason.
from gen.beacons import Beacon, BeaconType, BEACONS_RESOURCE_PATH
THIS_DIR = Path(__file__).parent.resolve()
SRC_DIR = THIS_DIR.parents[1]
EXPORT_DIR = SRC_DIR / BEACONS_RESOURCE_PATH
@contextmanager
def cd(path: Path):
cwd = os.getcwd()
os.chdir(path)
try:
yield
finally:
os.chdir(cwd)
def convert_lua_frequency(raw: Union[float, int]) -> int:
if isinstance(raw, float):
if not raw.is_integer():
# The values are in hertz, and everything should be a whole number.
raise ValueError(f"Unexpected non-integer frequency: {raw}")
return int(raw)
else:
return raw
def beacons_from_terrain(dcs_path: Path, path: Path) -> Iterable[Beacon]:
# TODO: Fix case-sensitive issues.
# The beacons.lua file differs by case in some terrains. Will need to be
# fixed if the tool is to be run on Linux, but presumably the server
# wouldn't be able to find these anyway.
beacons_lua = path / "beacons.lua"
with cd(dcs_path):
lua = lupa.LuaRuntime()
lua.execute(textwrap.dedent("""\
function module(name)
end
"""))
bind_gettext = lua.eval(textwrap.dedent("""\
function(py_gettext)
package.preload["i_18n"] = function()
return {
translate = py_gettext
}
end
end
"""))
translator = gettext.translation(
"messages", path / "l10n", languages=["en"])
def translate(message_name: str) -> str:
if not message_name:
return message_name
return translator.gettext(message_name)
bind_gettext(translate)
src = beacons_lua.read_text()
lua.execute(src)
beacon_types_map: Dict[int, BeaconType] = {}
for beacon_type in BeaconType:
beacon_value = lua.eval(beacon_type.name)
beacon_types_map[beacon_value] = beacon_type
beacons = lua.eval("beacons")
for beacon in beacons.values():
beacon_type_lua = beacon["type"]
if beacon_type_lua not in beacon_types_map:
raise KeyError(
f"Unknown beacon type {beacon_type_lua}. Check that all "
f"beacon types in {beacon_types_path} are present in "
f"{BeaconType.__class__.__name__}"
)
beacon_type = beacon_types_map[beacon_type_lua]
yield Beacon(
beacon["display_name"],
beacon["callsign"],
beacon_type,
convert_lua_frequency(beacon["frequency"]),
getattr(beacon, "channel", None)
)
class Importer:
"""Imports beacon definitions from each available terrain mod.
Only beacons for maps owned by the user can be imported. Other maps that
have been previously imported will not be disturbed.
"""
def __init__(self, dcs_path: Path, export_dir: Path) -> None:
self.dcs_path = dcs_path
self.export_dir = export_dir
def run(self) -> None:
"""Exports the beacons for each available terrain mod."""
terrains_path = self.dcs_path / "Mods" / "terrains"
self.export_dir.mkdir(parents=True, exist_ok=True)
for terrain in terrains_path.iterdir():
beacons = beacons_from_terrain(self.dcs_path, terrain)
self.export_beacons(terrain.name, beacons)
def export_beacons(self, terrain: str, beacons: Iterable[Beacon]) -> None:
terrain_py_path = self.export_dir / f"{terrain.lower()}.json"
import json
terrain_py_path.write_text(json.dumps([
dataclasses.asdict(b) for b in beacons
], indent=True))
def parse_args() -> argparse.Namespace:
"""Parses and returns command line arguments."""
parser = argparse.ArgumentParser()
def resolved_path(val: str) -> Path:
"""Returns the given string as a fully resolved Path."""
return Path(val).resolve()
parser.add_argument(
"--export-to",
type=resolved_path,
default=EXPORT_DIR,
help="Output directory for generated JSON files.")
parser.add_argument(
"dcs_path",
metavar="DCS_PATH",
type=resolved_path,
help="Path to DCS installation."
)
return parser.parse_args()
def main() -> None:
"""Program entry point."""
args = parse_args()
Importer(args.dcs_path, args.export_to).run()
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,6 @@
import typing
from pydcs import dcs
import dcs
from dcs.mapping import Point
from .controlpoint import ControlPoint

View File

@@ -27,7 +27,6 @@ class ControlPoint:
full_name = None # type: str
base = None # type: theater.base.Base
at = None # type: db.StartPosition
icls = 1
allow_sea_units = True
connected_points = None # type: typing.List[ControlPoint]
@@ -38,7 +37,6 @@ class ControlPoint:
frontline_offset = 0.0
cptype: ControlPointType = None
ICLS_counter = 1
alt = 0
def __init__(self, id: int, name: str, position: Point, at, radials: typing.Collection[int], size: int, importance: float,
@@ -63,10 +61,6 @@ class ControlPoint:
self.base = theater.base.Base()
self.cptype = cptype
self.stances = {}
self.tacanY = False
self.tacanN = None
self.tacanI = "TAC"
self.icls = 0
self.airport = None
@classmethod
@@ -81,11 +75,6 @@ class ControlPoint:
import theater.conflicttheater
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP)
cp.tacanY = False
cp.tacanN = random.randint(26, 49)
cp.tacanI = random.choice(["STE", "CVN", "CVH", "CCV", "ACC", "ARC", "GER", "ABR", "LIN", "TRU"])
ControlPoint.ICLS_counter = ControlPoint.ICLS_counter + 1
cp.icls = ControlPoint.ICLS_counter
return cp
@classmethod
@@ -93,9 +82,6 @@ class ControlPoint:
import theater.conflicttheater
cp = cls(id, name, at, at, theater.conflicttheater.LAND, theater.conflicttheater.SIZE_SMALL, 1,
has_frontline=False, cptype=ControlPointType.LHA_GROUP)
cp.tacanY = False
cp.tacanN = random.randint(1,25)
cp.tacanI = random.choice(["LHD", "LHA", "LHB", "LHC", "LHD", "LDS"])
return cp
@property

View File

@@ -2,7 +2,7 @@ import json
import os
from shutil import copyfile
from pydcs import dcs
import dcs
from userdata import persistency