Compare commits

..

5 Commits

Author SHA1 Message Date
David Pierron
45c67534e6 added whatever was necessary to generate
the new IADS CPs on the map
2020-10-28 12:05:05 +01:00
David Pierron
bcc3333d1b typing corrections 2020-10-28 12:04:40 +01:00
David Pierron
f9ee36db83 Merge remote-tracking branch 'origin/develop' into plugin-skynet-iads 2020-10-28 08:10:04 +01:00
David Pierron
ca5204634a added ground object templates :
- iads-power
- iads-controlcenter
- iads-ewr
- iads-commnode
2020-10-28 08:09:51 +01:00
David Pierron
14a3279b2c cleaner way of adding a faction parameter
to the groupgenerator constructor
2020-10-27 10:06:22 +01:00
310 changed files with 2796 additions and 6588 deletions

View File

@@ -41,10 +41,6 @@ jobs:
run: |
./venv/scripts/activate
mypy theater
- name: update build number
run: |
[IO.File]::WriteAllLines($pwd.path + "\resources\buildnumber", $env:GITHUB_RUN_NUMBER)
- name: Build binaries
run: |

View File

@@ -29,10 +29,6 @@ jobs:
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: Finalize version
run: |
New-Item -ItemType file resources\final
- name: mypy game
run: |
./venv/scripts/activate

12
.vscode/launch.json vendored
View File

@@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python: Main",
"type": "python",
@@ -15,6 +16,17 @@
},
"preLaunchTask": "Prepare Environment"
},
{
"name": "Python: build ground objects templates",
"type": "python",
"request": "launch",
"program": "resources\\tools\\generate_groundobject_templates.py",
"console": "integratedTerminal",
"env": {
"PYTHONPATH": ".;./pydcs"
},
"preLaunchTask": "Prepare Environment"
},
{
"name": "Python: Make Release",
"type": "python",

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
}

View File

@@ -12,10 +12,10 @@
![GitHub stars](https://img.shields.io/github/stars/khopa/dcs_liberation?style=social)
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player semi dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Logo](https://i.imgur.com/4hq0rLq.png)
![Logo](https://imgur.com/B6tvlBJ.png)
## Downloads
@@ -31,10 +31,6 @@ First, a big thanks to shdwp, for starting the original DCS Liberation project.
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
Excellent lua scripts DCS Liberation uses as plugins:
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
And for the JTAC feature, DCS Liberation embed Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
Please also show some support to these projects !

View File

@@ -1,62 +1,16 @@
# 2.2.1
# Features/Improvements
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
* **[Factions]** Added map Persian Gulf full by Plob
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
* **[UI]** Mission start screen now informs players about delayed flights.
* **[Units]** Added support for F-14A-135-GR
* **[Modding]** Possible to setup liveries overrides in factions definition files
## Fixes :
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
# 2.2.0
# 2.2.X
## Features/Improvements :
* **[Campaign Generator]** Added early warning radar generation
* **[Campaign Generator]** Added scud launcher sites
* **[Cheat Menu]** Added ability to capture base from mission planner
* **[Cheat Menu]** Added ability to show red ATO
* **[Factions]** Added WW2 factions that do not depend on WW2 asset pack
* **[Factions]** Cold War / Middle eastern factions will use Flak sites
* **[Flight Planner]** Flight planner overhaul, with package and TOT system
* **[Flight Planner]** Pick runways and ascent/descent based on headwind
* **[Map]** Added polygon debug mode display
* **[Map]** Highlight the selected flight path on the map
* **[Map]** Improved flight plan display settings
* **[Map]** Improved SAM display settings
* **[Map]** Improved flight plan display settings
* **[Map]** Caucasus and The Channel map use a new system to generate SAM and strike target location to reduce probability of targets generated in the middle of a forests
* **[Misc]** Flexible Dedicated Hosting Options for Mission Files via environment variables
* **[Moddability]** Custom campaigns can be designed through json files
* **[Moddability]** LUA plugins can now be injected into Liberation missions.
* **[Moddability]** Optional Skynet IADS lua plugin now included
* **[New Game]** Starting budget can be freely selected
* **[New Game]** Exanded information for faction and campaign selection in the new game wizard
* **[UI]** Add double and right click actions to many UI elements.
* **[UI]** Add polygon drawing mode for map background
* **[UI]** Added a warning if you press takeoff with no player enabled flights
* **[UI]** Packages and flights now visible in the main window sidebar
* **[Units/Factions]** Added bombers to some coalitions
* **[Units/Factions]** Added support for SU-57 mod by Cubanace
* **[Units]** Added Freya EWR sites to german WW2 factions
* **[Units]** Added support for many bombers (B-52H, B-1B, Tu-22, Tu-142)
* **[Units]** Added support for new P-47 variants
* **[Map]** Added polygon debug mode display
* **[New Game]** Starting budget can be freely selected
* **[Moddability]** Custom campaigns can be designed through json files
## Fixes :
* **[Campaign Generator]** Big airbases could end up without any airbase defense.
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
* **[Flight Planner]** Fix waypoint alitudes for helicopters
* **[Flight Planner]** Fixed CAS aircraft wandering away from frontline
* **[Maps]** Incirlik airbase was missing exclusions zones, so SAMS could end up being generated on the runway
* **[Mission Generator]** Fixed player/client confusion when a flight had only one player slot.
* **[Radios]** Fix A-10C radio
* **[UI]** Many missing unit icons were added
* **[Campaign generator]** Ship group and offshore buildings should not be generated on land anymore
* **[UI]** Missing TER weapons in custom payload now selectable.
# 2.1.5
@@ -335,4 +289,4 @@ Sorry :(
* **[Mission Generator]** Planned flights will spawn even if their home base has been captured or is being contested by enemy ground units.
* **[Campaign Generator]** Base defenses would not be generated on Normandy map and in some rare cases on others maps as well
* **[Mission Planning]** CAS waypoints created from the "Predefined waypoint selector" would not be at the exact location of the frontline
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)
* **[Naming]** CAP mission flown from airbase are not named BARCAP anymore (CAP from carrier is still named BARCAP)

View File

@@ -1,3 +1,2 @@
from .game import Game
from . import db
from .version import VERSION
from . import db

View File

@@ -1,11 +1,10 @@
import inspect
import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick', 'aa', 'iads-controlcenter', 'iads-ewr', 'iads-commnode', 'iads-power']
WW2_FREE = ['fuel', 'factory', 'ware']
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp']
WW2_GERMANY_BUILDINGS = ['fuel', 'factory', 'ww2bunker', 'ww2bunker', 'ww2bunker', 'allycamp', 'allycamp', 'aa']
WW2_ALLIES_BUILDINGS = ['fuel', 'factory', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'allycamp', 'aa']
FORTIFICATION_BUILDINGS = ['Siegfried Line', 'Concertina wire', 'Concertina Wire', 'Czech hedgehogs 1', 'Czech hedgehogs 2',
'Dragonteeth 1', 'Dragonteeth 2', 'Dragonteeth 3', 'Dragonteeth 4', 'Dragonteeth 5',

View File

@@ -11,12 +11,10 @@ from dcs.planes import (
MiG_19P,
MiG_21Bis,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW
SpitfireLFMkIXCW,
)
from pydcs_extensions.a4ec.a4ec import A_4E_C
@@ -43,8 +41,6 @@ GUNFIGHTERS = [
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,

View File

@@ -1,5 +1,4 @@
from dataclasses import dataclass
from datetime import timedelta
from game.utils import nm_to_meter, feet_to_meter
@@ -16,8 +15,6 @@ class Doctrine:
sead_max_range: int
rendezvous_altitude: int
hold_distance: int
push_distance: int
join_distance: int
split_distance: int
ingress_egress_distance: int
@@ -28,14 +25,11 @@ class Doctrine:
max_patrol_altitude: int
pattern_altitude: int
cap_duration: timedelta
cap_min_track_length: int
cap_max_track_length: int
cap_min_distance_from_cp: int
cap_max_distance_from_cp: int
cas_duration: timedelta
MODERN_DOCTRINE = Doctrine(
cap=True,
@@ -46,8 +40,6 @@ MODERN_DOCTRINE = Doctrine(
strike_max_range=1500000,
sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(25000),
hold_distance=nm_to_meter(15),
push_distance=nm_to_meter(20),
join_distance=nm_to_meter(20),
split_distance=nm_to_meter(20),
ingress_egress_distance=nm_to_meter(45),
@@ -56,12 +48,10 @@ MODERN_DOCTRINE = Doctrine(
min_patrol_altitude=feet_to_meter(15000),
max_patrol_altitude=feet_to_meter(33000),
pattern_altitude=feet_to_meter(5000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nm_to_meter(15),
cap_max_track_length=nm_to_meter(40),
cap_min_distance_from_cp=nm_to_meter(10),
cap_max_distance_from_cp=nm_to_meter(40),
cas_duration=timedelta(minutes=30),
)
COLDWAR_DOCTRINE = Doctrine(
@@ -73,8 +63,6 @@ COLDWAR_DOCTRINE = Doctrine(
strike_max_range=1500000,
sead_max_range=1500000,
rendezvous_altitude=feet_to_meter(22000),
hold_distance=nm_to_meter(10),
push_distance=nm_to_meter(10),
join_distance=nm_to_meter(10),
split_distance=nm_to_meter(10),
ingress_egress_distance=nm_to_meter(30),
@@ -83,12 +71,10 @@ COLDWAR_DOCTRINE = Doctrine(
min_patrol_altitude=feet_to_meter(10000),
max_patrol_altitude=feet_to_meter(24000),
pattern_altitude=feet_to_meter(5000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nm_to_meter(12),
cap_max_track_length=nm_to_meter(24),
cap_min_distance_from_cp=nm_to_meter(8),
cap_max_distance_from_cp=nm_to_meter(25),
cas_duration=timedelta(minutes=30),
)
WWII_DOCTRINE = Doctrine(
@@ -99,8 +85,6 @@ WWII_DOCTRINE = Doctrine(
antiship=True,
strike_max_range=1500000,
sead_max_range=1500000,
hold_distance=nm_to_meter(5),
push_distance=nm_to_meter(5),
join_distance=nm_to_meter(5),
split_distance=nm_to_meter(5),
rendezvous_altitude=feet_to_meter(10000),
@@ -110,10 +94,8 @@ WWII_DOCTRINE = Doctrine(
min_patrol_altitude=feet_to_meter(4000),
max_patrol_altitude=feet_to_meter(15000),
pattern_altitude=feet_to_meter(5000),
cap_duration=timedelta(minutes=30),
cap_min_track_length=nm_to_meter(8),
cap_max_track_length=nm_to_meter(18),
cap_min_distance_from_cp=nm_to_meter(0),
cap_max_distance_from_cp=nm_to_meter(5),
cas_duration=timedelta(minutes=30),
)

View File

@@ -44,7 +44,6 @@ from dcs.planes import (
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
@@ -104,7 +103,7 @@ from dcs.planes import (
Tu_95MS,
WingLoong_I,
Yak_40,
plane_map
plane_map,
)
from dcs.ships import (
Armed_speedboat,
@@ -154,6 +153,7 @@ from dcs.vehicles import (
)
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from game.factions.faction import Faction
# PATCH pydcs data with MODS
from game.factions.faction_loader import FactionLoader
from pydcs_extensions.a4ec.a4ec import A_4E_C
@@ -202,6 +202,7 @@ vehicle_map["Toyota_vert"] = frenchpack.DIM__TOYOTA_GREEN
vehicle_map["Toyota_desert"] = frenchpack.DIM__TOYOTA_DESERT
vehicle_map["Kamikaze"] = frenchpack.DIM__KAMIKAZE
"""
---------- BEGINNING OF CONFIGURATION SECTION
"""
@@ -270,7 +271,6 @@ PRICES = {
F_15E: 24,
F_16C_50: 20,
F_16A: 14,
F_14A_135_GR: 20,
F_14B: 24,
Tornado_IDS: 20,
Tornado_GR4: 20,
@@ -399,30 +399,27 @@ PRICES = {
Unarmed.Transport_M818: 3,
# WW2
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G: 24,
Armor.MT_Pz_Kpfw_IV_Ausf_H: 16,
Armor.HT_Pz_Kpfw_VI_Tiger_I: 24,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II: 26,
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G:24,
Armor.MT_Pz_Kpfw_IV_Ausf_H:16,
Armor.HT_Pz_Kpfw_VI_Tiger_I:24,
Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II:26,
Armor.TD_Jagdpanther_G1: 18,
Armor.TD_Jagdpanzer_IV: 11,
Armor.Sd_Kfz_184_Elefant: 18,
Armor.APC_Sd_Kfz_251: 4,
Armor.AC_Sd_Kfz_234_2_Puma: 8,
Armor.MT_M4_Sherman: 12,
Armor.MT_M4A4_Sherman_Firefly: 16,
Armor.CT_Cromwell_IV: 12,
Armor.M30_Cargo_Carrier: 2,
Armor.APC_M2A1: 4,
Armor.CT_Centaur_IV: 10,
Armor.APC_Sd_Kfz_251:4,
Armor.IFV_Sd_Kfz_234_2_Puma:8,
Armor.MT_M4_Sherman:12,
Armor.MT_M4A4_Sherman_Firefly:16,
Armor.CT_Cromwell_IV:12,
Armor.M30_Cargo_Carrier:2,
Armor.APC_M2A1:4,
Armor.ST_Centaur_IV: 10,
Armor.HIT_Churchill_VII: 16,
Armor.LAC_M8_Greyhound: 8,
Armor.TD_M10_GMC: 14,
Armor.StuG_III_Ausf__G: 12,
Artillery.M12_GMC: 10,
Artillery.Sturmpanzer_IV_Brummbär: 10,
Armor.Daimler_Armoured_Car: 8,
Armor.LT_Mk_VII_Tetrarch: 8,
Armor.M4_Tractor: 2,
# ship
CV_1143_5_Admiral_Kuznetsov: 100,
@@ -501,16 +498,12 @@ PRICES = {
AirDefence.AAA_Flak_38: 6,
AirDefence.AAA_8_8cm_Flak_36: 8,
AirDefence.AAA_8_8cm_Flak_37: 9,
AirDefence.AAA_Flak_Vierling_38: 5,
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: 10,
AirDefence.EWR_FuMG_401_Freya_LZ: 25,
AirDefence.AAA_Bofors_40mm: 8,
AirDefence.AAA_M1_37mm: 7,
AirDefence.AAA_M45_Quadmount: 4,
AirDefence.AA_gun_QF_3_7: 10,
# FRENCH PACK MOD
frenchpack.AMX_10RCR: 10,
@@ -575,7 +568,6 @@ UNIT_BY_TASK = {
MiG_31,
FA_18C_hornet,
F_15C,
F_14A_135_GR,
F_14B,
F_16A,
F_16C_50,
@@ -751,13 +743,13 @@ UNIT_BY_TASK = {
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
Armor.APC_Sd_Kfz_251,
Armor.APC_Sd_Kfz_251,
Armor.APC_Sd_Kfz_251,
Armor.APC_Sd_Kfz_251,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.CT_Cromwell_IV,
@@ -770,12 +762,12 @@ UNIT_BY_TASK = {
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
Armor.TD_Jagdpanther_G1,
Armor.TD_Jagdpanzer_IV,
Armor.Sd_Kfz_184_Elefant,
Armor.APC_Sd_Kfz_251,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.CT_Cromwell_IV,
@@ -784,8 +776,8 @@ UNIT_BY_TASK = {
Armor.M30_Cargo_Carrier,
Armor.APC_M2A1,
Armor.APC_M2A1,
Armor.CT_Centaur_IV,
Armor.CT_Centaur_IV,
Armor.ST_Centaur_IV,
Armor.ST_Centaur_IV,
Armor.HIT_Churchill_VII,
Armor.LAC_M8_Greyhound,
Armor.LAC_M8_Greyhound,
@@ -901,6 +893,7 @@ SAM_CONVERT = {
}
}
"""
Units that will always be spawned in the air
"""
@@ -911,18 +904,17 @@ TAKEOFF_BAN: List[Type[FlyingType]] = [
Units that will be always spawned in the air if launched from the carrier
"""
CARRIER_TAKEOFF_BAN: List[Type[FlyingType]] = [
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
Su_33, # Kuznecow is bugged in a way that only 2 aircraft could be spawned
]
"""
Units separated by country.
country : DCS Country name
"""
FACTIONS = FactionLoader()
FACTIONS: Dict[str, Faction] = FactionLoader.load_factions()
CARRIER_TYPE_BY_PLANE = {
FA_18C_hornet: CVN_74_John_C__Stennis,
F_14A_135_GR: CVN_74_John_C__Stennis,
F_14B: CVN_74_John_C__Stennis,
Ka_50: LHA_1_Tarawa,
SA342M: LHA_1_Tarawa,
@@ -961,10 +953,23 @@ COMMON_OVERRIDE = {
PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
B_1B: COMMON_OVERRIDE,
B_52H: COMMON_OVERRIDE,
F_117A: COMMON_OVERRIDE,
F_15E: COMMON_OVERRIDE,
B_1B: {
CAS: "GBU-38*16, CBU-97*20",
PinpointStrike: "GBU-31*8, GBU-38*32",
GroundAttack: "GBU-31*8, GBU-38*32",
},
B_52H: {
PinpointStrike: "AGM-86C*20",
GroundAttack: "Mk 82*51",
},
F_117A: {
PinpointStrike: "GBU-10*2",
},
F_15E: {
CAS: "AIM-120B*2,AIM-9M*2,FUEL,GBU-12*4,GBU-38*4,AGM-65D*2",
GroundAttack: "AIM-120B*2,AIM-9M*2,FUEL*3,CBU-97*12",
PinpointStrike: "AIM-120B*2,AIM-9M*2,FUEL,GBU-31*4,AGM-154C*2",
},
FA_18C_hornet: {
CAP: "CAP HEAVY",
Intercept: "CAP HEAVY",
@@ -988,15 +993,18 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
Tu_160: {
PinpointStrike: "Kh-65*12",
},
Tu_22M3: COMMON_OVERRIDE,
Tu_95MS: COMMON_OVERRIDE,
Tu_22M3: {
GroundAttack: "FAB-500*33, FAB-250*36",
},
Tu_95MS: {
PinpointStrike: "Kh-65*6",
},
A_10A: COMMON_OVERRIDE,
A_10C: COMMON_OVERRIDE,
A_10C_2: COMMON_OVERRIDE,
AV8BNA: COMMON_OVERRIDE,
C_101CC: COMMON_OVERRIDE,
F_5E_3: COMMON_OVERRIDE,
F_14A_135_GR: COMMON_OVERRIDE,
F_14B: COMMON_OVERRIDE,
F_15C: COMMON_OVERRIDE,
F_16C_50: COMMON_OVERRIDE,
@@ -1006,14 +1014,14 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
MiG_19P: COMMON_OVERRIDE,
MiG_21Bis: COMMON_OVERRIDE,
AJS37: COMMON_OVERRIDE,
Su_25T: COMMON_OVERRIDE,
Su_25: COMMON_OVERRIDE,
Su_27: COMMON_OVERRIDE,
Su_33: COMMON_OVERRIDE,
MiG_29A: COMMON_OVERRIDE,
MiG_29G: COMMON_OVERRIDE,
MiG_29S: COMMON_OVERRIDE,
Su_24M: COMMON_OVERRIDE,
Su_25T:COMMON_OVERRIDE,
Su_25:COMMON_OVERRIDE,
Su_27:COMMON_OVERRIDE,
Su_33:COMMON_OVERRIDE,
MiG_29A:COMMON_OVERRIDE,
MiG_29G:COMMON_OVERRIDE,
MiG_29S:COMMON_OVERRIDE,
Su_24M:COMMON_OVERRIDE,
Su_30: COMMON_OVERRIDE,
Su_34: COMMON_OVERRIDE,
Su_57: COMMON_OVERRIDE,
@@ -1022,21 +1030,21 @@ PLANE_PAYLOAD_OVERRIDES: Dict[Type[PlaneType], Dict[Type[Task], str]] = {
Tornado_GR4: COMMON_OVERRIDE,
Tornado_IDS: COMMON_OVERRIDE,
Mirage_2000_5: COMMON_OVERRIDE,
MiG_31: COMMON_OVERRIDE,
SA342M: COMMON_OVERRIDE,
SA342L: COMMON_OVERRIDE,
SA342Mistral: COMMON_OVERRIDE,
Mi_8MT: COMMON_OVERRIDE,
Mi_24V: COMMON_OVERRIDE,
Mi_28N: COMMON_OVERRIDE,
Ka_50: COMMON_OVERRIDE,
L_39ZA: COMMON_OVERRIDE,
L_39C: COMMON_OVERRIDE,
MiG_31:COMMON_OVERRIDE,
SA342M:COMMON_OVERRIDE,
SA342L:COMMON_OVERRIDE,
SA342Mistral:COMMON_OVERRIDE,
Mi_8MT:COMMON_OVERRIDE,
Mi_24V:COMMON_OVERRIDE,
Mi_28N:COMMON_OVERRIDE,
Ka_50:COMMON_OVERRIDE,
L_39ZA:COMMON_OVERRIDE,
L_39C:COMMON_OVERRIDE,
Su_17M4: COMMON_OVERRIDE,
F_4E: COMMON_OVERRIDE,
P_47D_30: COMMON_OVERRIDE,
P_47D_30bl1: COMMON_OVERRIDE,
P_47D_40: 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,
@@ -1129,7 +1137,6 @@ PLAYER_BUDGET_BASE = 20
CARRIER_CAPABLE = [
FA_18C_hornet,
F_14A_135_GR,
F_14B,
AV8BNA,
Su_33,
@@ -1165,6 +1172,7 @@ LHA_CAPABLE = [
SA342Mistral
]
"""
---------- END OF CONFIGURATION SECTION
"""
@@ -1216,20 +1224,16 @@ def find_unittype(for_task: Task, country_name: str) -> List[UnitType]:
def find_infantry(country_name: str) -> List[UnitType]:
inf = [
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS, Infantry.Paratrooper_AKS,
Infantry.Soldier_RPG,
Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4, Infantry.Infantry_M4,
Infantry.Soldier_M249,
Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK, Infantry.Soldier_AK,
Infantry.Paratrooper_RPG_16,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_Soldier_Rus,
Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4, Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus, Infantry.Infantry_Soldier_Rus,
Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1, Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98, Infantry.Infantry_Mauser_98,
Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand, Infantry.Infantry_M1_Garand,
Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents, Infantry.Infantry_Soldier_Insurgents
]
@@ -1360,7 +1364,7 @@ def unitdict_from(fd: AssignedUnitsDict) -> Dict:
def country_id_from_name(name):
for k, v in country_dict.items():
for k,v in country_dict.items():
if v.name == name:
return k
return -1
@@ -1378,17 +1382,14 @@ def _validate_db():
for unit_type in total_set:
assert unit_type in PRICES, "{} not in prices".format(unit_type)
_validate_db()
class DefaultLiveries:
class Default(Enum):
af_standard = ""
OH_58D.Liveries = DefaultLiveries
F_16C_50.Liveries = DefaultLiveries
P_51D_30_NA.Liveries = DefaultLiveries
Ju_88A4.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries
B_17G.Liveries = DefaultLiveries

View File

@@ -24,7 +24,7 @@ class DebriefingDeadUnitInfo:
class Debriefing:
def __init__(self, state_data, game):
self.state_data = state_data
self.base_capture_events = state_data["base_capture_events"]
self.killed_aircrafts = state_data["killed_aircrafts"]
self.killed_ground_units = state_data["killed_ground_units"]
self.weapons_fired = state_data["weapons_fired"]
@@ -87,8 +87,8 @@ class Debriefing:
for i, ground_object in enumerate(cp.ground_objects):
logging.info(unit)
logging.info(ground_object.group_name)
if ground_object.is_same_group(unit):
logging.info(ground_object.string_identifier)
if ground_object.matches_string_identifier(unit):
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
self.dead_buildings.append(unit)
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
@@ -162,18 +162,6 @@ class Debriefing:
logging.info(self.player_dead_buildings_dict)
logging.info(self.enemy_dead_buildings_dict)
@property
def base_capture_events(self):
"""Keeps only the last instance of a base capture event for each base ID"""
reversed_captures = [i for i in self.state_data["base_capture_events"][::-1]]
last_base_cap_indexes = []
for idx, base in enumerate(i.split("||")[0] for i in reversed_captures):
if base in [x[1] for x in last_base_cap_indexes]:
continue
else:
last_base_cap_indexes.append((idx, base))
return [reversed_captures[idx[0]] for idx in last_base_cap_indexes]
class PollDebriefingFileThread(threading.Thread):
"""Thread class with a stop() method. The thread itself has to check

View File

@@ -14,6 +14,7 @@ from game.infos.information import Information
from game.operation.operation import Operation
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
from theater.start_generator import generate_airbase_defense_group
if TYPE_CHECKING:
from ..game import Game
@@ -144,13 +145,9 @@ class Event:
for i, ground_object in enumerate(cp.ground_objects):
if ground_object.is_dead:
continue
if (
(ground_object.group_name == destroyed_ground_unit_name)
or
(ground_object.is_same_group(destroyed_ground_unit_name))
):
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
if ground_object.matches_string_identifier(destroyed_ground_unit_name):
logging.info("cp {} killing ground object {}".format(cp, ground_object.string_identifier))
cp.ground_objects[i].is_dead = True
info = Information("Building destroyed",
@@ -165,7 +162,7 @@ class Event:
"",
self.game.turn)
for i, ground_object in enumerate(cp.ground_objects):
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA", "EWR"]:
if ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
for g in ground_object.groups:
if not hasattr(g, "units_losts"):
g.units_losts = []

View File

@@ -10,7 +10,7 @@ from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@@ -57,9 +57,6 @@ class Faction:
# Possible SAMS site generators for this faction
sams: List[str] = field(default_factory=list)
# Possible EWR generators for this faction.
ewrs: List[str] = field(default_factory=list)
# Possible Missile site generators for this faction
missiles: List[str] = field(default_factory=list)
@@ -105,9 +102,6 @@ class Faction:
# List of available buildings for this faction
building_set: List[str] = field(default_factory=list)
# List of default livery overrides
liveries_overrides: Dict[UnitType, List[str]] = field(default_factory=dict)
@classmethod
def from_json(cls: Type[Faction], json: Dict[str, Any]) -> Faction:
@@ -138,7 +132,6 @@ class Faction:
json.get("logistics_units", []))
faction.sams = json.get("sams", [])
faction.ewrs = json.get("ewrs", [])
faction.shorads = json.get("shorads", [])
faction.missiles = json.get("missiles", [])
faction.requirements = json.get("requirements", {})
@@ -177,8 +170,6 @@ class Faction:
building_set = json.get("building_set", "default")
if building_set == "default":
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
elif building_set == "ww2free":
faction.building_set = WW2_FREE
elif building_set == "ww2ally":
faction.building_set = WW2_ALLIES_BUILDINGS
elif building_set == "ww2germany":
@@ -186,14 +177,6 @@ class Faction:
else:
faction.building_set = DEFAULT_AVAILABLE_BUILDINGS
# Load liveries override
faction.liveries_overrides = {}
liveries_overrides = json.get("liveries_overrides", {})
for k, v in liveries_overrides.items():
k = load_aircraft(k)
if k is not None:
faction.liveries_overrides[k] = [s.lower() for s in v]
return faction
@property

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Iterator, Optional, Type
from typing import Dict, Type
from game.factions.faction import Faction
@@ -10,18 +10,6 @@ FACTION_DIRECTORY = Path("./resources/factions/")
class FactionLoader:
def __init__(self) -> None:
self._factions: Optional[Dict[str, Faction]] = None
@property
def factions(self) -> Dict[str, Faction]:
self.initialize()
assert self._factions is not None
return self._factions
def initialize(self) -> None:
if self._factions is None:
self._factions = self.load_factions()
@classmethod
def load_factions(cls: Type[FactionLoader]) -> Dict[str, Faction]:
@@ -38,9 +26,3 @@ class FactionLoader:
logging.exception(f"Unable to load faction : {f}")
return factions
def __getitem__(self, name: str) -> Faction:
return self.factions[name]
def __iter__(self) -> Iterator[str]:
return iter(self.factions.keys())

View File

@@ -3,7 +3,7 @@ import math
import random
import sys
from datetime import date, datetime, timedelta
from typing import Dict, List
from typing import Any, Dict, List
from dcs.action import Coalition
from dcs.mapping import Point
@@ -15,7 +15,6 @@ from game import db
from game.db import PLAYER_BUDGET_BASE, REWARDS
from game.inventory import GlobalAircraftInventory
from game.models.game_stats import GameStats
from game.plugins import LuaPluginManager
from gen.ato import AirTaskingOrder
from gen.conflictgen import Conflict
from gen.flights.ai_flight_planner import CoalitionMissionPlanner
@@ -30,6 +29,7 @@ from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .infos.information import Information
from .settings import Settings
from plugin import LuaPluginManager
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
@@ -147,6 +147,30 @@ class Game:
front_line.control_point_a,
front_line.control_point_b)
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> List[UnitType]:
importance_factor = (cp.importance - IMPORTANCE_LOW) / (IMPORTANCE_HIGH - IMPORTANCE_LOW)
if for_task == AirDefence and not self.settings.sams:
return [x for x in db.find_unittype(AirDefence, self.enemy_name) if x not in db.SAM_BAN]
else:
return db.choose_units(for_task, importance_factor, COMMISION_UNIT_VARIETY, self.enemy_name)
def _commision_units(self, cp: ControlPoint):
for for_task in [CAS, CAP, AirDefence]:
limit = COMMISION_LIMITS_FACTORS[for_task] * math.pow(cp.importance,
COMMISION_LIMITS_SCALE) * self.settings.multiplier
missing_units = limit - cp.base.total_units(for_task)
if missing_units > 0:
awarded_points = COMMISION_AMOUNTS_FACTORS[for_task] * math.pow(cp.importance,
COMMISION_AMOUNTS_SCALE) * self.settings.multiplier
points_to_spend = cp.base.append_commision_points(for_task, awarded_points)
if points_to_spend > 0:
unittypes = self.commision_unit_types(cp, for_task)
if len(unittypes) > 0:
d = {random.choice(unittypes): points_to_spend}
logging.info("Commision {}: {}".format(cp, d))
cp.base.commision_units(d)
@property
def budget_reward_amount(self):
reward = 0
@@ -154,7 +178,7 @@ class Game:
reward = PLAYER_BUDGET_BASE * len(self.theater.player_points())
for cp in self.theater.player_points():
for g in cp.ground_objects:
if g.category in REWARDS.keys() and not g.is_dead:
if g.category in REWARDS.keys():
reward = reward + REWARDS[g.category]
return reward
else:
@@ -202,8 +226,11 @@ class Game:
return event and event.name and event.name == self.player_name
def on_load(self) -> None:
LuaPluginManager.load_settings(self.settings)
ObjectiveDistanceCache.set_theater(self.theater)
# set the settings in all plugins
for plugin in LuaPluginManager().getPlugins():
plugin.setSettings(self.settings)
# Save game compatibility.
@@ -277,7 +304,7 @@ class Game:
production = 0.0
for enemy_point in self.theater.enemy_points():
for g in enemy_point.ground_objects:
if g.category in REWARDS.keys() and not g.is_dead:
if g.category in REWARDS.keys():
production = production + REWARDS[g.category]
production = production * 0.75

View File

@@ -49,10 +49,7 @@ class ControlPointAircraftInventory:
Args:
aircraft: The type of aircraft to query.
"""
try:
return self.inventory[aircraft]
except KeyError:
return 0
return self.inventory[aircraft]
@property
def types_available(self) -> Iterator[UnitType]:

View File

@@ -35,4 +35,6 @@ class FrontlineAttackOperation(Operation):
conflict=conflict)
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."
super(FrontlineAttackOperation, self).generate()

View File

@@ -14,14 +14,13 @@ from dcs.translation import String
from dcs.triggers import TriggerStart
from dcs.unittype import UnitType
from game.plugins import LuaPluginManager
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
from gen.briefinggen import BriefingGenerator
from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
@@ -29,6 +28,7 @@ from gen.kneeboard import KneeboardGenerator
from gen.radios import RadioFrequency, RadioRegistry
from gen.tacan import TacanRegistry
from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator
from plugin import LuaPluginManager
from theater import ControlPoint
from .. import db
from ..debriefing import Debriefing
@@ -90,7 +90,8 @@ class Operation:
def initialize(self, mission: Mission, conflict: Conflict):
self.current_mission = mission
self.conflict = conflict
# self.briefinggen = BriefingGenerator(self.current_mission, self.game) Is it safe to remove this, or does it also break save compat?
self.briefinggen = BriefingGenerator(self.current_mission,
self.conflict, self.game)
def prepare(self, terrain: Terrain, is_quick: bool):
with open("resources/default_options.lua", "r") as f:
@@ -165,37 +166,6 @@ class Operation:
trigger.add_action(DoScriptFile(fileref))
self.current_mission.triggerrules.triggers.append(trigger)
def notify_info_generators(
self,
groundobjectgen: GroundObjectsGenerator,
airsupportgen: AirSupportConflictGenerator,
jtacs: List[JtacInfo],
airgen: AircraftConflictGenerator,
):
"""Generates subscribed MissionInfoGenerator objects (currently kneeboards and briefings)
"""
gens: List[MissionInfoGenerator] = [
KneeboardGenerator(self.current_mission, self.game),
BriefingGenerator(self.current_mission, self.game)
]
for gen in gens:
for dynamic_runway in groundobjectgen.runways.values():
gen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
gen.add_tanker(tanker)
if self.is_awacs_enabled:
for awacs in airsupportgen.air_support.awacs:
gen.add_awacs(awacs)
for jtac in jtacs:
gen.add_jtac(jtac)
for flight in airgen.flights:
gen.add_flight(flight)
gen.generate()
def generate(self):
radio_registry = RadioRegistry()
tacan_registry = TacanRegistry()
@@ -330,7 +300,13 @@ class Operation:
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.dcsGroupName,
"callsign": tanker.callsign,
@@ -341,6 +317,8 @@ class Operation:
if self.is_awacs_enabled:
for awacs in airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName,
"callsign": awacs.callsign,
@@ -348,6 +326,8 @@ class Operation:
}
for jtac in jtacs:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
luaData["JTACs"][jtac.callsign] = {
"dcsGroupName": jtac.dcsGroupName,
"callsign": jtac.callsign,
@@ -357,6 +337,8 @@ class Operation:
}
for flight in airgen.flights:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
if flight.friendly and flight.flight_type in [FlightType.ANTISHIP, FlightType.DEAD, FlightType.SEAD, FlightType.STRIKE]:
flightType = flight.flight_type.name
flightTarget = flight.package.target
@@ -374,6 +356,11 @@ class Operation:
"type": flightTargetType,
"position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
}
self.briefinggen.generate()
kneeboard_generator.generate()
# set a LUA table with data from Liberation that we want to set
# at the moment it contains Liberation's install path, and an overridable definition for the JTACAutoLase function
@@ -473,14 +460,37 @@ dcsLiberation.TargetPoints = {
self.current_mission.triggerrules.triggers.append(trigger)
# Inject Plugins Lua Scripts and data
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(self)
plugin.inject_configuration(self)
for plugin in LuaPluginManager().getPlugins():
plugin.injectScripts(self)
plugin.injectConfiguration(self)
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen)
kneeboard_generator = KneeboardGenerator(self.current_mission)
for dynamic_runway in groundobjectgen.runways.values():
self.briefinggen.add_dynamic_runway(dynamic_runway)
for tanker in airsupportgen.air_support.tankers:
self.briefinggen.add_tanker(tanker)
kneeboard_generator.add_tanker(tanker)
if self.is_awacs_enabled:
for awacs in airsupportgen.air_support.awacs:
self.briefinggen.add_awacs(awacs)
kneeboard_generator.add_awacs(awacs)
for jtac in jtacs:
self.briefinggen.add_jtac(jtac)
kneeboard_generator.add_jtac(jtac)
for flight in airgen.flights:
self.briefinggen.add_flight(flight)
kneeboard_generator.add_flight(flight)
self.briefinggen.generate()
kneeboard_generator.generate()
def assign_channels_to_flights(self, flights: List[FlightData],
air_support: AirSupport) -> None:

View File

@@ -1,2 +0,0 @@
from .luaplugin import LuaPlugin
from .manager import LuaPluginManager

View File

@@ -1,180 +0,0 @@
from __future__ import annotations
import json
import logging
import textwrap
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, TYPE_CHECKING
from game.settings import Settings
if TYPE_CHECKING:
from game.operation.operation import Operation
class LuaPluginWorkOrder:
def __init__(self, parent_mnemonic: str, filename: str, mnemonic: str,
disable: bool) -> None:
self.parent_mnemonic = parent_mnemonic
self.filename = filename
self.mnemonic = mnemonic
self.disable = disable
def work(self, operation: Operation) -> None:
if self.disable:
operation.bypass_plugin_script(self.mnemonic)
else:
operation.inject_plugin_script(self.parent_mnemonic, self.filename,
self.mnemonic)
class PluginSettings:
def __init__(self, identifier: str, enabled_by_default: bool) -> None:
self.identifier = identifier
self.enabled_by_default = enabled_by_default
self.settings = Settings()
self.initialize_settings()
def set_settings(self, settings: Settings):
self.settings = settings
self.initialize_settings()
def initialize_settings(self) -> None:
# Plugin options are saved in the game's Settings, but it's possible for
# plugins to change across loads. If new plugins are added or new
# options added to those plugins, initialize the new settings.
self.settings.initialize_plugin_option(self.identifier,
self.enabled_by_default)
@property
def enabled(self) -> bool:
return self.settings.plugin_option(self.identifier)
def set_enabled(self, enabled: bool) -> None:
self.settings.set_plugin_option(self.identifier, enabled)
class LuaPluginOption(PluginSettings):
def __init__(self, identifier: str, name: str,
enabled_by_default: bool) -> None:
super().__init__(identifier, enabled_by_default)
self.name = name
@dataclass(frozen=True)
class LuaPluginDefinition:
identifier: str
name: str
present_in_ui: bool
enabled_by_default: bool
options: List[LuaPluginOption]
work_orders: List[LuaPluginWorkOrder]
config_work_orders: List[LuaPluginWorkOrder]
@classmethod
def from_json(cls, name: str, path: Path) -> LuaPluginDefinition:
data = json.loads(path.read_text())
options = []
for option in data.get("specificOptions"):
option_id = option["mnemonic"]
options.append(LuaPluginOption(
identifier=f"{name}.{option_id}",
name=option.get("nameInUI", name),
enabled_by_default=option.get("defaultValue")
))
work_orders = []
for work_order in data.get("scriptsWorkOrders"):
work_orders.append(LuaPluginWorkOrder(
name, work_order.get("file"), work_order["mnemonic"],
work_order.get("disable", False)
))
config_work_orders = []
for work_order in data.get("configurationWorkOrders"):
config_work_orders.append(LuaPluginWorkOrder(
name, work_order.get("file"), work_order["mnemonic"],
work_order.get("disable", False)
))
return cls(
identifier=name,
name=data["nameInUI"],
present_in_ui=not data.get("skipUI", False),
enabled_by_default=data.get("defaultValue", False),
options=options,
work_orders=work_orders,
config_work_orders=config_work_orders
)
class LuaPlugin(PluginSettings):
def __init__(self, definition: LuaPluginDefinition) -> None:
self.definition = definition
super().__init__(self.definition.identifier,
self.definition.enabled_by_default)
@property
def name(self) -> str:
return self.definition.name
@property
def show_in_ui(self) -> bool:
return self.definition.present_in_ui
@property
def options(self) -> List[LuaPluginOption]:
return self.definition.options
@classmethod
def from_json(cls, name: str, path: Path) -> Optional[LuaPlugin]:
try:
definition = LuaPluginDefinition.from_json(name, path)
except KeyError:
logging.exception("Required plugin configuration value missing")
return None
return cls(definition)
def set_settings(self, settings: Settings):
super().set_settings(settings)
for option in self.definition.options:
option.set_settings(self.settings)
def inject_scripts(self, operation: Operation) -> None:
for work_order in self.definition.work_orders:
work_order.work(operation)
def inject_configuration(self, operation: Operation) -> None:
# inject the plugin options
if self.options:
option_decls = []
for option in self.options:
enabled = str(option.enabled).lower()
name = option.identifier
option_decls.append(
f" dcsLiberation.plugins.{name} = {enabled}")
joined_options = "\n".join(option_decls)
lua = textwrap.dedent(f"""\
-- {self.identifier} plugin configuration.
if dcsLiberation then
if not dcsLiberation.plugins then
dcsLiberation.plugins = {{}}
end
dcsLiberation.plugins.{self.identifier} = {{}}
{joined_options}
end
""")
operation.inject_lua_trigger(
lua, f"{self.identifier} plugin configuration")
for work_order in self.definition.config_work_orders:
work_order.work(operation)

View File

@@ -1,50 +0,0 @@
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional
from game.settings import Settings
from game.plugins.luaplugin import LuaPlugin
class LuaPluginManager:
_plugins_loaded = False
_plugins: Dict[str, LuaPlugin] = {}
@classmethod
def _load_plugins(cls) -> None:
plugins_path = Path("resources/plugins")
path = plugins_path / "plugins.json"
if not path.exists():
raise RuntimeError(f"{path} does not exist. Cannot continue.")
logging.info(f"Reading plugins list from {path}")
data = json.loads(path.read_text())
for name in data:
plugin_path = plugins_path / name / "plugin.json"
if not plugin_path.exists():
raise RuntimeError(
f"Invalid plugin configuration: required plugin {name} "
f"does not exist at {plugin_path}")
logging.info(f"Loading plugin {name} from {plugin_path}")
plugin = LuaPlugin.from_json(name, plugin_path)
if plugin is not None:
cls._plugins[name] = plugin
cls._plugins_loaded = True
@classmethod
def _get_plugins(cls) -> Dict[str, LuaPlugin]:
if not cls._plugins_loaded:
cls._load_plugins()
return cls._plugins
@classmethod
def plugins(cls) -> List[LuaPlugin]:
return list(cls._get_plugins().values())
@classmethod
def load_settings(cls, settings: Settings) -> None:
for plugin in cls.plugins():
plugin.set_settings(settings)

View File

@@ -1,5 +1,4 @@
from typing import Dict
from plugin import LuaPluginManager
class Settings:
@@ -21,7 +20,7 @@ class Settings:
self.night_disabled = False
self.external_views_allowed = True
self.supercarrier = False
self.multiplier = 1.0
self.multiplier = 1
self.generate_marks = True
self.sams = True # Legacy parameter do not use
self.cold_start = False # Legacy parameter do not use
@@ -41,30 +40,14 @@ class Settings:
self.perf_culling_distance = 100
# LUA Plugins system
self.plugins: Dict[str, bool] = {}
self.plugins = {}
for plugin in LuaPluginManager().getPlugins():
plugin.setSettings(self)
# Cheating
self.show_red_ato = False
self.never_delay_player_flights = False
@staticmethod
def plugin_settings_key(identifier: str) -> str:
return f"plugins.{identifier}"
def initialize_plugin_option(self, identifier: str,
default_value: bool) -> None:
try:
self.plugin_option(identifier)
except KeyError:
self.set_plugin_option(identifier, default_value)
def plugin_option(self, identifier: str) -> bool:
return self.plugins[self.plugin_settings_key(identifier)]
def set_plugin_option(self, identifier: str, enabled: bool) -> None:
self.plugins[self.plugin_settings_key(identifier)] = enabled
def __setstate__(self, state) -> None:
# __setstate__ is called with the dict of the object being unpickled. We
# can provide save compatibility for new settings options (which

View File

@@ -1,18 +0,0 @@
from pathlib import Path
def _build_version_string() -> str:
components = ["2.2.0"]
build_number_path = Path("resources/buildnumber")
if build_number_path.exists():
with build_number_path.open("r") as build_number_file:
components.append(build_number_file.readline())
if not Path("resources/final").exists():
components.append("preview")
return "-".join(components)
#: Current version of Liberation.
VERSION = _build_version_string()

View File

@@ -3,9 +3,7 @@ from __future__ import annotations
import logging
import random
from dataclasses import dataclass
from datetime import timedelta
from functools import cached_property
from typing import Dict, List, Optional, Type, Union, TYPE_CHECKING
from typing import Dict, List, Optional, Type, Union
from dcs import helicopters
from dcs.action import AITaskPush, ActivateGroup
@@ -13,6 +11,7 @@ from dcs.condition import CoalitionHasAirdrome, TimeAfter
from dcs.country import Country
from dcs.flyingunit import FlyingUnit
from dcs.helicopters import UH_1H, helicopter_map
from dcs.mapping import Point
from dcs.mission import Mission, StartType
from dcs.planes import (
AJS37,
@@ -25,13 +24,11 @@ from dcs.planes import (
JF_17,
Ju_88A4,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_33, A_20G, Tu_22M3, B_52H,
Su_33,
)
from dcs.point import MovingPoint, PointAction
from dcs.task import (
@@ -56,7 +53,7 @@ from dcs.task import (
SEAD,
StartCommand,
Targets,
Task, WeaponType,
Task,
)
from dcs.terrain.terrain import Airport
from dcs.translation import String
@@ -84,18 +81,10 @@ from dcs.mapping import Point
from theater import TheaterGroundObject
from theater.controlpoint import ControlPoint, ControlPointType
from .conflictgen import Conflict
from .flights.flightplan import (
CasFlightPlan,
FormationFlightPlan,
PatrollingFlightPlan,
)
from .flights.traveltime import TotEstimator
from .flights.traveltime import PackageWaypointTiming, TotEstimator
from .naming import namegen
from .runways import RunwayAssigner
if TYPE_CHECKING:
from game import Game
WARM_START_HELI_AIRSPEED = 120
WARM_START_HELI_ALT = 500
WARM_START_ALTITUDE = 3000
@@ -115,11 +104,6 @@ GERMAN_WW2_CHANNEL = MHz(40)
HELICOPTER_CHANNEL = MHz(127)
UHF_FALLBACK_CHANNEL = MHz(251)
TARGET_WAYPOINTS = (
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
# TODO: Get radio information for all the special cases.
def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
@@ -139,8 +123,6 @@ def get_fallback_channel(unit_type: UnitType) -> RadioFrequency:
allied_ww2_aircraft = [
I_16,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
@@ -166,26 +148,6 @@ class ChannelNamer:
return f"COMM{radio_id} Ch {channel_id}"
class SingleRadioChannelNamer(ChannelNamer):
"""Channel namer for the aircraft with only a single radio.
Aircraft like the MiG-19P and the MiG-21bis only have a single radio, so
it's not necessary for us to name the radio when naming the channel.
"""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"Ch {channel_id}"
class HueyChannelNamer(ChannelNamer):
"""Channel namer for the UH-1H."""
@staticmethod
def channel_name(radio_id: int, channel_id: int) -> str:
return f"COM3 Ch {channel_id}"
class MirageChannelNamer(ChannelNamer):
"""Channel namer for the M-2000."""
@@ -261,7 +223,7 @@ class FlightData:
friendly: bool
#: Number of seconds after mission start the flight is set to depart.
departure_delay: timedelta
departure_delay: int
#: Arrival airport.
arrival: RunwayData
@@ -283,7 +245,7 @@ class FlightData:
def __init__(self, package: Package, flight_type: FlightType,
units: List[FlyingUnit], size: int, friendly: bool,
departure_delay: timedelta, departure: RunwayData,
departure_delay: int, departure: RunwayData,
arrival: RunwayData, divert: Optional[RunwayData],
waypoints: List[FlightWaypoint],
intra_flight_channel: RadioFrequency) -> None:
@@ -411,28 +373,16 @@ class CommonRadioChannelAllocator(RadioChannelAllocator):
@dataclass(frozen=True)
class NoOpChannelAllocator(RadioChannelAllocator):
"""Channel allocator for aircraft that don't support preset channels."""
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 FarmerRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the MiG-19P."""
def assign_channels_for_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
# The Farmer only has 6 preset channels. It also only has a VHF radio,
# and currently our ATC data and AWACS are only in the UHF band.
radio_id = 1
flight.assign_channel(radio_id, 1, flight.intra_flight_channel)
# TODO: Assign 4-6 to VHF frequencies of departure, arrival, and divert.
# TODO: Assign 2 and 3 to AWACS if it is VHF.
@dataclass(frozen=True)
class ViggenRadioChannelAllocator(RadioChannelAllocator):
"""Preset channel allocator for the AJS37."""
@@ -500,7 +450,7 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
# VHF for intraflight is not accepted anymore by DCS
# (see https://forums.eagle.ru/showthread.php?p=4499738).
intra_flight_radio=get_radio("AN/ARC-164"),
channel_allocator=NoOpChannelAllocator()
channel_allocator=WarthogRadioChannelAllocator()
),
"AJS37": AircraftData(
@@ -569,15 +519,6 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
channel_namer=ViperChannelNamer
),
"Ka-50": AircraftData(
inter_flight_radio=get_radio("R-800L1"),
intra_flight_radio=get_radio("R-800L1"),
# The R-800L1 doesn't have preset channels, and the other radio is for
# communications with FAC and ground units, which don't currently have
# radios assigned, so no channels to configure.
channel_allocator=NoOpChannelAllocator(),
),
"M-2000C": AircraftData(
inter_flight_radio=get_radio("TRT ERA 7000 V/UHF"),
intra_flight_radio=get_radio("TRT ERA 7200 UHF"),
@@ -588,29 +529,6 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
channel_namer=MirageChannelNamer
),
"MiG-15bis": AircraftData(
inter_flight_radio=get_radio("RSI-6K HF"),
intra_flight_radio=get_radio("RSI-6K HF"),
channel_allocator=NoOpChannelAllocator(),
),
"MiG-19P": AircraftData(
inter_flight_radio=get_radio("RSIU-4V"),
intra_flight_radio=get_radio("RSIU-4V"),
channel_allocator=FarmerRadioChannelAllocator(),
channel_namer=SingleRadioChannelNamer
),
"MiG-21Bis": AircraftData(
inter_flight_radio=get_radio("RSIU-5V"),
intra_flight_radio=get_radio("RSIU-5V"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=1
),
channel_namer=SingleRadioChannelNamer,
),
"P-51D": AircraftData(
inter_flight_radio=get_radio("SCR522"),
intra_flight_radio=get_radio("SCR522"),
@@ -620,19 +538,6 @@ AIRCRAFT_DATA: Dict[str, AircraftData] = {
),
channel_namer=SCR522ChannelNamer
),
"UH-1H": AircraftData(
inter_flight_radio=get_radio("AN/ARC-51BX"),
# Ideally this would use the AN/ARC-131 because that radio is supposed
# to be used for flight comms, but DCS won't allow it as the flight's
# frequency, nor will it allow the AN/ARC-134.
intra_flight_radio=get_radio("AN/ARC-51BX"),
channel_allocator=CommonRadioChannelAllocator(
inter_flight_radio_index=1,
intra_flight_radio_index=1
),
channel_namer=HueyChannelNamer
)
}
AIRCRAFT_DATA["A-10C_2"] = AIRCRAFT_DATA["A-10C"]
AIRCRAFT_DATA["P-51D-30-NA"] = AIRCRAFT_DATA["P-51D"]
@@ -641,7 +546,7 @@ AIRCRAFT_DATA["P-47D-30"] = AIRCRAFT_DATA["P-51D"]
class AircraftConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, settings: Settings,
game: Game, radio_registry: RadioRegistry):
game, radio_registry: RadioRegistry):
self.m = mission
self.game = game
self.settings = settings
@@ -649,21 +554,6 @@ class AircraftConflictGenerator:
self.radio_registry = radio_registry
self.flights: List[FlightData] = []
@cached_property
def use_client(self) -> bool:
"""True if Client should be used instead of Player."""
blue_clients = self.client_slots_in_ato(self.game.blue_ato)
red_clients = self.client_slots_in_ato(self.game.red_ato)
return blue_clients + red_clients > 1
@staticmethod
def client_slots_in_ato(ato: AirTaskingOrder) -> int:
total = 0
for package in ato.packages:
for flight in package.flights:
total += flight.client_count
return total
def get_intra_flight_channel(self, airframe: UnitType) -> RadioFrequency:
"""Allocates an intra-flight channel to a group.
@@ -713,23 +603,13 @@ class AircraftConflictGenerator:
for unit_instance in group.units:
unit_instance.livery_id = db.PLANE_LIVERY_OVERRIDES[unit_type]
# Override livery by faction file data
if flight.from_cp.captured:
faction = self.game.player_faction
else:
faction = self.game.enemy_faction
if unit_type in faction.liveries_overrides:
livery = random.choice(faction.liveries_overrides[unit_type])
for unit_instance in group.units:
unit_instance.livery_id = livery
single_client = flight.client_count == 1
for idx in range(0, min(len(group.units), flight.client_count)):
unit = group.units[idx]
if self.use_client:
unit.set_client()
else:
if single_client:
unit.set_player()
else:
unit.set_client()
# Do not generate player group with late activation.
if group.late_activation:
@@ -765,8 +645,7 @@ class AircraftConflictGenerator:
units=group.units,
size=len(group.units),
friendly=flight.from_cp.captured,
# Set later.
departure_delay=timedelta(),
departure_delay=flight.scheduled_in,
departure=departure_runway,
arrival=departure_runway,
# TODO: Support for divert airfields.
@@ -906,6 +785,7 @@ class AircraftConflictGenerator:
for package in ato.packages:
if not package.flights:
continue
timing = PackageWaypointTiming.for_package(package)
for flight in package.flights:
culled = self.game.position_culled(flight.from_cp.position)
if flight.client_count == 0 and culled:
@@ -915,10 +795,10 @@ class AircraftConflictGenerator:
group = self.generate_planned_flight(flight.from_cp, country,
flight)
self.setup_flight_group(group, package, flight, dynamic_runways)
self.create_waypoints(group, package, flight)
self.create_waypoints(group, package, flight, timing)
def set_activation_time(self, flight: Flight, group: FlyingGroup,
delay: timedelta) -> None:
delay: int) -> None:
# Note: Late activation causes the waypoint TOTs to look *weird* in the
# mission editor. Waypoint times will be relative to the group
# activation time rather than in absolute local time. A flight delayed
@@ -928,22 +808,20 @@ class AircraftConflictGenerator:
activation_trigger = TriggerOnce(
Event.NoEvent, f"FlightLateActivationTrigger{group.id}")
activation_trigger.add_condition(
TimeAfter(seconds=int(delay.total_seconds())))
activation_trigger.add_condition(TimeAfter(seconds=delay))
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
activation_trigger.add_action(ActivateGroup(group.id))
self.m.triggerrules.triggers.append(activation_trigger)
def set_startup_time(self, flight: Flight, group: FlyingGroup,
delay: timedelta) -> None:
delay: int) -> None:
# Uncontrolled causes the AI unit to spawn, but not begin startup.
group.uncontrolled = True
activation_trigger = TriggerOnce(Event.NoEvent,
f"FlightStartTrigger{group.id}")
activation_trigger.add_condition(
TimeAfter(seconds=int(delay.total_seconds())))
activation_trigger.add_condition(TimeAfter(seconds=delay))
self.prevent_spawn_at_hostile_airbase(flight, activation_trigger)
group.add_trigger_action(StartCommand())
@@ -1004,6 +882,7 @@ class AircraftConflictGenerator:
at=cp.position)
group.points[0].alt = 1500
flight.group = group
return group
@staticmethod
@@ -1024,8 +903,7 @@ class AircraftConflictGenerator:
group.points[0].tasks.append(OptRTBOnOutOfAmmo(rtb_winchester))
group.points[0].tasks.append(OptRTBOnBingoFuel(True))
# Do not restrict afterburner.
# https://forums.eagle.ru/forum/english/digital-combat-simulator/dcs-world-2-5/bugs-and-problems-ai/ai-ad/7121294-ai-stuck-at-high-aoa-after-making-sharp-turn-if-afterburner-is-restricted
group.points[0].tasks.append(OptRestrictAfterburner(True))
@staticmethod
def configure_eplrs(group: FlyingGroup, flight: Flight) -> None:
@@ -1057,25 +935,13 @@ class AircraftConflictGenerator:
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
roe=OptROE.Values.WeaponHold,
rtb_winchester=OptRTBOnOutOfAmmo.Values.Unguided,
restrict_jettison=True)
def configure_dead(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = SEAD.name
self._setup_group(group, SEAD, package, flight, dynamic_runways)
self.configure_behavior(
group,
react_on_threat=OptReactOnThreat.Values.EvadeFire,
roe=OptROE.Values.OpenFire,
rtb_winchester=OptRTBOnOutOfAmmo.Values.ASM,
restrict_jettison=True)
def configure_sead(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = SEAD.name
self._setup_group(group, SEAD, package, flight, dynamic_runways)
self.configure_behavior(
@@ -1088,7 +954,7 @@ class AircraftConflictGenerator:
def configure_strike(self, group: FlyingGroup, package: Package,
flight: Flight,
dynamic_runways: Dict[str, RunwayData]) -> None:
group.task = GroundAttack.name
group.task = PinpointStrike.name
self._setup_group(group, GroundAttack, package, flight, dynamic_runways)
self.configure_behavior(
group,
@@ -1133,9 +999,7 @@ class AircraftConflictGenerator:
self.configure_cap(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.CAS, FlightType.BAI]:
self.configure_cas(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.DEAD, ]:
self.configure_dead(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.SEAD, ]:
elif flight_type in [FlightType.SEAD, FlightType.DEAD]:
self.configure_sead(group, package, flight, dynamic_runways)
elif flight_type in [FlightType.STRIKE]:
self.configure_strike(group, package, flight, dynamic_runways)
@@ -1148,8 +1012,8 @@ class AircraftConflictGenerator:
self.configure_eplrs(group, flight)
def create_waypoints(
self, group: FlyingGroup, package: Package, flight: Flight) -> None:
def create_waypoints(self, group: FlyingGroup, package: Package,
flight: Flight, timing: PackageWaypointTiming) -> None:
for waypoint in flight.points:
waypoint.tot = None
@@ -1158,32 +1022,15 @@ class AircraftConflictGenerator:
flight.from_cp)
self.set_takeoff_time(takeoff_point, package, flight, group)
filtered_points = [] # type: List[FlightWaypoint]
filtered_points = []
for point in flight.points:
if point.only_for_player and not flight.client_count:
continue
filtered_points.append(point)
# Only add 1 target waypoint for Viggens. This only affects player flights,
# the Viggen can't have more than 9 waypoints which leaves us with two target point
# under the current flight plans.
# TODO: Make this smarter, it currently selects a random unit in the group for target,
# this could be updated to make it pick the "best" two targets in the group.
if flight.unit_type is AJS37 and flight.client_count:
viggen_target_points = [
(idx, point) for idx, point in enumerate(filtered_points) if point.waypoint_type in TARGET_WAYPOINTS
]
if viggen_target_points:
keep_target = viggen_target_points[random.randint(0, len(viggen_target_points) - 1)]
filtered_points = [
point for idx, point in enumerate(filtered_points) if (
point.waypoint_type not in TARGET_WAYPOINTS or idx == keep_target[0]
)
]
for idx, point in enumerate(filtered_points):
PydcsWaypointBuilder.for_waypoint(
point, group, package, flight, self.m
point, group, flight, timing, self.m
).build()
# Set here rather than when the FlightData is created so they waypoints
@@ -1191,31 +1038,15 @@ class AircraftConflictGenerator:
self.flights[-1].waypoints = [takeoff_point] + flight.points
self._setup_custom_payload(flight, group)
def should_delay_flight(self, flight: Flight,
start_time: timedelta) -> bool:
if start_time.total_seconds() <= 0:
return False
if not flight.client_count:
return True
if start_time < timedelta(minutes=10):
# Don't bother delaying client flights with short start delays. Much
# more than ten minutes starts to eat into fuel a bit more
# (espeicially for something fuel limited like a Harrier).
return False
return not self.settings.never_delay_player_flights
def set_takeoff_time(self, waypoint: FlightWaypoint, package: Package,
flight: Flight, group: FlyingGroup) -> None:
estimator = TotEstimator(package)
start_time = estimator.mission_start_time(flight)
if self.should_delay_flight(flight, start_time):
if start_time > 0:
if self.should_activate_late(flight):
# Late activation causes the aircraft to not be spawned
# until triggered.
# Late activation causes the aircraft to not be spawned until
# triggered.
self.set_activation_time(flight, group, start_time)
elif flight.start_type == "Cold":
# Setting the start time causes the AI to wait until the
@@ -1225,12 +1056,18 @@ class AircraftConflictGenerator:
# And setting *our* waypoint TOT causes the takeoff time to show up in
# the player's kneeboard.
waypoint.tot = estimator.takeoff_time_for_flight(flight)
# And finally assign it to the FlightData info so it shows correctly in
# the briefing.
self.flights[-1].departure_delay = start_time
@staticmethod
def should_activate_late(flight: Flight) -> bool:
if flight.client_count:
# Never delay players. Note that cold start player flights with
# AI members will still be marked as uncontrolled until the start
# trigger fires to postpone engine start.
#
# Player flights that start on the runway or in the air will start
# immediately, and AI flight members will not be delayed.
return False
if flight.start_type != "Cold":
# Avoid spawning aircraft in the air or on the runway until it's
# time for their mission. Also avoid burning through gas spawning
@@ -1248,12 +1085,12 @@ class AircraftConflictGenerator:
class PydcsWaypointBuilder:
def __init__(self, waypoint: FlightWaypoint, group: FlyingGroup,
package: Package, flight: Flight,
flight: Flight, timing: PackageWaypointTiming,
mission: Mission) -> None:
self.waypoint = waypoint
self.group = group
self.package = package
self.flight = flight
self.timing = timing
self.mission = mission
def build(self) -> MovingPoint:
@@ -1262,54 +1099,35 @@ class PydcsWaypointBuilder:
waypoint.alt_type = self.waypoint.alt_type
waypoint.name = String(self.waypoint.name)
tot = self.flight.flight_plan.tot_for_waypoint(self.waypoint)
if tot is not None:
self.set_waypoint_tot(waypoint, tot)
return waypoint
def set_waypoint_tot(self, waypoint: MovingPoint, tot: timedelta) -> None:
def set_waypoint_tot(self, waypoint: MovingPoint, tot: int) -> None:
self.waypoint.tot = tot
if not self._viggen_client_tot():
waypoint.ETA = int(tot.total_seconds())
waypoint.ETA_locked = True
waypoint.speed_locked = False
waypoint.ETA = tot
waypoint.ETA_locked = True
waypoint.speed_locked = False
@classmethod
def for_waypoint(cls, waypoint: FlightWaypoint, group: FlyingGroup,
package: Package, flight: Flight,
flight: Flight, timing: PackageWaypointTiming,
mission: Mission) -> PydcsWaypointBuilder:
builders = {
FlightWaypointType.EGRESS: EgressPointBuilder,
FlightWaypointType.INGRESS_CAS: CasIngressBuilder,
FlightWaypointType.INGRESS_DEAD: DeadIngressBuilder,
FlightWaypointType.INGRESS_ESCORT: IngressBuilder,
FlightWaypointType.INGRESS_SEAD: SeadIngressBuilder,
FlightWaypointType.INGRESS_STRIKE: StrikeIngressBuilder,
FlightWaypointType.JOIN: JoinPointBuilder,
FlightWaypointType.LANDING_POINT: LandingPointBuilder,
FlightWaypointType.LOITER: HoldPointBuilder,
FlightWaypointType.PATROL_TRACK: RaceTrackBuilder,
FlightWaypointType.SPLIT: SplitPointBuilder,
FlightWaypointType.TARGET_GROUP_LOC: TargetPointBuilder,
FlightWaypointType.TARGET_POINT: TargetPointBuilder,
FlightWaypointType.TARGET_SHIP: TargetPointBuilder,
}
builder = builders.get(waypoint.waypoint_type, DefaultWaypointBuilder)
return builder(waypoint, group, package, flight, mission)
def _viggen_client_tot(self) -> bool:
"""Viggen player aircraft consider any waypoint with a TOT set to be a target ("M") waypoint.
If the flight is a player controlled Viggen flight, no TOT should be set on any waypoint except actual target waypoints.
"""
if (
(self.flight.client_count > 0 and self.flight.unit_type == AJS37) and
(self.waypoint.waypoint_type not in TARGET_WAYPOINTS)
):
return True
else:
return False
def register_special_waypoints(self, targets) -> None:
"""Create special target waypoints for various aircraft"""
for i, t in enumerate(targets):
if self.group.units[0].unit_type == JF_17 and i < 4:
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
if self.group.units[0].unit_type == F_14B and i == 0:
self.group.add_nav_target_point(t.position, "ST")
return builder(waypoint, group, flight, timing, mission)
class DefaultWaypointBuilder(PydcsWaypointBuilder):
@@ -1323,35 +1141,32 @@ class HoldPointBuilder(PydcsWaypointBuilder):
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.Circle
))
if not isinstance(self.flight.flight_plan, FormationFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot configure hold for for {self.flight} because "
f"{flight_plan_type} does not define a push time. AI will push "
"immediately and may flight unsuitable speeds."
)
return waypoint
push_time = self.flight.flight_plan.push_time
push_time = self.timing.push_time(self.flight, self.waypoint)
self.waypoint.departure_time = push_time
loiter.stop_after_time(int(push_time.total_seconds()))
loiter.stop_after_time(push_time)
waypoint.add_task(loiter)
return waypoint
class CasIngressBuilder(PydcsWaypointBuilder):
class EgressPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if isinstance(self.flight.flight_plan, CasFlightPlan):
waypoint.add_task(EngageTargetsInZone(
position=self.flight.flight_plan.target,
radius=FRONTLINE_LENGTH / 2,
targets=[
Targets.All.GroundUnits.GroundVehicles,
Targets.All.GroundUnits.AirDefence.AAA,
Targets.All.GroundUnits.Infantry,
])
)
else:
self.set_waypoint_tot(waypoint, self.timing.egress)
return waypoint
class IngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.ingress)
return waypoint
class CasIngressBuilder(IngressBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
cas_waypoint = self.flight.waypoint_with_type((FlightWaypointType.CAS,))
if cas_waypoint is None:
logging.error(
"No CAS waypoint found. Falling back to search and engage")
waypoint.add_task(EngageTargets(
@@ -1362,17 +1177,28 @@ class CasIngressBuilder(PydcsWaypointBuilder):
Targets.All.GroundUnits.Infantry,
])
)
else:
waypoint.add_task(EngageTargetsInZone(
position=cas_waypoint.position,
radius=FRONTLINE_LENGTH / 2,
targets=[
Targets.All.GroundUnits.GroundVehicles,
Targets.All.GroundUnits.AirDefence.AAA,
Targets.All.GroundUnits.Infantry,
])
)
waypoint.add_task(OptROE(OptROE.Values.OpenFireWeaponFree))
return waypoint
class DeadIngressBuilder(PydcsWaypointBuilder):
class SeadIngressBuilder(IngressBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target_group = self.package.target
target_group = self.waypoint.targetGroup
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
if tgroup is not None: # the Mission group name because of SkyNet prefixes.
tgroup = self.mission.find_group(target_group.group_identifier)
if tgroup is not None:
task = AttackGroup(tgroup.id)
task.params["expend"] = "All"
task.params["attackQtyLimit"] = False
@@ -1381,36 +1207,20 @@ class DeadIngressBuilder(PydcsWaypointBuilder):
task.params["weaponType"] = 268402702 # Guided Weapons
task.params["groupAttack"] = True
waypoint.tasks.append(task)
else:
logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
self.register_special_waypoints(self.waypoint.targets)
for i, t in enumerate(self.waypoint.targets):
if self.group.units[0].unit_type == JF_17 and i < 4:
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
if self.group.units[0].unit_type == F_14B and i == 0:
self.group.add_nav_target_point(t.position, "ST")
if self.group.units[0].unit_type == AJS37 and i < 9:
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
return waypoint
class SeadIngressBuilder(PydcsWaypointBuilder):
class StrikeIngressBuilder(IngressBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
target_group = self.package.target
if isinstance(target_group, TheaterGroundObject):
tgroup = self.mission.find_group(target_group.group_name, search="match") # Match search is used due to TheaterGroundObject.name not matching
if tgroup is not None: # the Mission group name because of SkyNet prefixes.
waypoint.add_task(EngageTargetsInZone(
position=tgroup.position,
radius=nm_to_meter(30),
targets=[
Targets.All.GroundUnits.AirDefence,
])
)
else:
logging.error(f"Could not find group for DEAD mission {target_group.group_name}")
self.register_special_waypoints(self.waypoint.targets)
return waypoint
class StrikeIngressBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
if self.group.units[0].unit_type in [B_17G, B_52H, Tu_22M3]:
if self.group.units[0].unit_type == B_17G:
return self.build_bombing()
else:
return self.build_strike()
@@ -1433,43 +1243,29 @@ class StrikeIngressBuilder(PydcsWaypointBuilder):
bombing.params["attackQtyLimit"] = False
bombing.params["directionEnabled"] = False
bombing.params["altitudeEnabled"] = False
bombing.params["weaponType"] = WeaponType.Bombs.value
bombing.params["weaponType"] = 2032
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
return waypoint
def build_strike(self) -> MovingPoint:
waypoint = super().build()
for target in self.waypoint.targets:
targets = [target]
# If the target type is a group of units,
# then target each unit in the group with a Bombing task on their position
# (It is not perfect, we should have an engage Group task instead,
# but we don't have the group ref in the model there)
# TODO : for building group, engage all the buildings as well
if isinstance(target, TheaterGroundObject):
if len(target.units) > 0:
targets = target.units
for t in targets:
bombing = Bombing(t.position)
# If there is only one target, drop all ordnance in one pass
if len(self.waypoint.targets) == 1 and len(targets) == 1:
bombing.params["expend"] = "All"
bombing.params["weaponType"] = WeaponType.Auto.value
bombing.params["groupAttack"] = True
waypoint.tasks.append(bombing)
print(bombing)
# Register special waypoints
self.register_special_waypoints(targets)
for i, t in enumerate(self.waypoint.targets):
waypoint.tasks.append(Bombing(t.position))
if self.group.units[0].unit_type == JF_17 and i < 4:
self.group.add_nav_target_point(t.position, "PP" + str(i + 1))
if self.group.units[0].unit_type == F_14B and i == 0:
self.group.add_nav_target_point(t.position, "ST")
if self.group.units[0].unit_type == AJS37 and i < 9:
self.group.add_nav_target_point(t.position, "M" + str(i + 1))
return waypoint
class JoinPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.join)
if self.flight.flight_type == FlightType.ESCORT:
self.configure_escort_tasks(waypoint)
return waypoint
@@ -1525,20 +1321,27 @@ class RaceTrackBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
if not isinstance(self.flight.flight_plan, PatrollingFlightPlan):
flight_plan_type = self.flight.flight_plan.__class__.__name__
logging.error(
f"Cannot create race track for {self.flight} because "
f"{flight_plan_type} does not define a patrol.")
return waypoint
racetrack = ControlledTask(OrbitAction(
altitude=waypoint.alt,
pattern=OrbitAction.OrbitPattern.RaceTrack
))
self.set_waypoint_tot(
waypoint, self.flight.flight_plan.patrol_start_time)
racetrack.stop_after_time(
int(self.flight.flight_plan.patrol_end_time.total_seconds()))
self.set_waypoint_tot(waypoint,
self.timing.race_track_start(self.flight))
racetrack.stop_after_time(self.timing.race_track_end(self.flight))
waypoint.add_task(racetrack)
return waypoint
class SplitPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.split)
return waypoint
class TargetPointBuilder(PydcsWaypointBuilder):
def build(self) -> MovingPoint:
waypoint = super().build()
self.set_waypoint_tot(waypoint, self.timing.target)
return waypoint

View File

@@ -34,7 +34,7 @@ from gen.ground_forces.ai_ground_planner import (
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from game.plugins import LuaPluginManager
from plugin import LuaPluginManager
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
@@ -140,7 +140,9 @@ class GroundConflictGenerator:
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
# Add JTAC
if self.game.player_faction.has_jtac:
jtacPlugin = LuaPluginManager().getPlugin("jtacautolase")
useJTAC = jtacPlugin and jtacPlugin.isEnabled()
if self.game.player_faction.has_jtac and useJTAC:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.jtacs)

View File

@@ -11,14 +11,12 @@ the single CAP flight.
import logging
from collections import defaultdict
from dataclasses import dataclass, field
from datetime import timedelta
from typing import Dict, List, Optional
from dcs.mapping import Point
from theater.missiontarget import MissionTarget
from .flights.flight import Flight, FlightType
from .flights.flightplan import FormationFlightPlan
@dataclass(frozen=True)
@@ -53,70 +51,11 @@ class Package:
delay: int = field(default=0)
#: Desired TOT as an offset from mission start.
time_over_target: timedelta = field(default=timedelta())
#: Desired TOT measured in seconds from mission start.
time_over_target: int = field(default=0)
waypoints: Optional[PackageWaypoints] = field(default=None)
@property
def formation_speed(self) -> Optional[int]:
"""The speed of the package when in formation.
If none of the flights in the package will join a formation, this
returns None. This is nto uncommon, since only strike-like (strike,
DEAD, anti-ship, BAI, etc.) flights and their escorts fly in formation.
Others (CAP and CAS, currently) will coordinate in target timing but
fly their own path to the target.
"""
speeds = []
for flight in self.flights:
if isinstance(flight.flight_plan, FormationFlightPlan):
speeds.append(flight.flight_plan.best_flight_formation_speed)
if not speeds:
return None
return min(speeds)
# TODO: Should depend on the type of escort.
# SEAD might be able to leave before CAP.
@property
def escort_start_time(self) -> Optional[timedelta]:
times = []
for flight in self.flights:
waypoint = flight.flight_plan.request_escort_at()
if waypoint is None:
continue
tot = flight.flight_plan.tot_for_waypoint(waypoint)
if tot is None:
logging.error(
f"{flight} requested escort at {waypoint} but that "
"waypoint has no TOT. It may not be escorted.")
continue
times.append(tot)
if times:
return min(times)
return None
@property
def escort_end_time(self) -> Optional[timedelta]:
times = []
for flight in self.flights:
waypoint = flight.flight_plan.dismiss_escort_at()
if waypoint is None:
continue
tot = flight.flight_plan.tot_for_waypoint(waypoint)
if tot is None:
tot = flight.flight_plan.depart_time_for_waypoint(waypoint)
if tot is None:
logging.error(
f"{flight} dismissed escort at {waypoint} but that "
"waypoint has no TOT or departure time. It may not be "
"escorted.")
continue
times.append(tot)
if times:
return max(times)
return None
def add_flight(self, flight: Flight) -> None:
"""Adds a flight to the package."""
self.flights.append(flight)

View File

@@ -1,26 +1,21 @@
"""
Briefing generation logic
"""
from __future__ import annotations
import datetime
import os
import random
import logging
from collections import defaultdict
from dataclasses import dataclass
from theater.frontline import FrontLine
from typing import List, Dict, TYPE_CHECKING
from jinja2 import Environment, FileSystemLoader, select_autoescape
from typing import List
from dcs.mission import Mission
from game import db
from .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
from theater import ControlPoint
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from .radios import RadioFrequency
from .runways import RunwayData
if TYPE_CHECKING:
from game import Game
@dataclass
class CommInfo:
@@ -29,33 +24,19 @@ class CommInfo:
freq: RadioFrequency
class FrontLineInfo:
def __init__(self, front_line: FrontLine):
self.front_line: FrontLine = front_line
self.player_base: ControlPoint = front_line.control_point_a
self.enemy_base: ControlPoint = front_line.control_point_b
self.player_zero: bool = self.player_base.base.total_armor == 0
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
self.combat_stances = CombatStance
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, game: Game) -> None:
def __init__(self, mission: Mission) -> None:
self.mission = mission
self.game = game
self.awacs: List[AwacsInfo] = []
self.comms: List[CommInfo] = []
self.flights: List[FlightData] = []
self.jtacs: List[JtacInfo] = []
self.tankers: List[TankerInfo] = []
self.frontlines: List[FrontLineInfo] = []
self.dynamic_runways: List[RunwayData] = []
def add_awacs(self, awacs: AwacsInfo) -> None:
"""Adds an AWACS/GCI to the mission.
@@ -98,13 +79,20 @@ class MissionInfoGenerator:
"""
self.tankers.append(tanker)
def add_frontline(self, frontline: FrontLineInfo) -> None:
"""Adds a frontline to the briefing
def generate(self) -> None:
"""Generates the mission information."""
raise NotImplementedError
Arguments:
frontline: Frontline conflict information
"""
self.frontlines.append(frontline)
class BriefingGenerator(MissionInfoGenerator):
def __init__(self, mission: Mission, conflict: Conflict, game):
super().__init__(mission)
self.conflict = conflict
self.game = game
self.title = ""
self.description = ""
self.dynamic_runways: List[RunwayData] = []
def add_dynamic_runway(self, runway: RunwayData) -> None:
"""Adds a dynamically generated runway to the briefing.
@@ -114,51 +102,150 @@ class MissionInfoGenerator:
"""
self.dynamic_runways.append(runway)
def generate(self) -> None:
"""Generates the mission information."""
raise NotImplementedError
def add_flight_description(self, flight: FlightData):
assert flight.client_units
aircraft = flight.aircraft_type
flight_unit_name = db.unit_type_name(aircraft)
self.description += "-" * 50 + "\n"
self.description += f"{flight_unit_name} x {flight.size}\n\n"
class BriefingGenerator(MissionInfoGenerator):
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"
def __init__(self, mission: Mission, game: Game):
super().__init__(mission, game)
self.allied_flights_by_departure: Dict[str, List[FlightData]] = {}
env = Environment(
loader=FileSystemLoader("resources/briefing/templates"),
autoescape=select_autoescape(
disabled_extensions=("",),
default_for_string=True,
default=True,
),
trim_blocks=True,
lstrip_blocks=True,
)
self.template = env.get_template("briefingtemplate_EN.j2")
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)
delay = datetime.timedelta(seconds=flight.departure_delay)
self.description += (
f"{flight.flight_type.name} {flight_unit_name} x {flight.size}, "
f"departing in {delay}\n"
)
def generate(self) -> None:
"""Generate the mission briefing
"""
self._generate_frontline_info()
self.generate_allied_flights_by_departure()
self.mission.set_description_text(self.template.render(vars(self)))
self.mission.add_picture_blue(os.path.abspath(
"./resources/ui/splash_screen.png"))
def generate(self):
self.description = ""
def _generate_frontline_info(self) -> None:
"""Build FrontLineInfo objects from FrontLine type and append to briefing.
"""
for front_line in self.game.theater.conflicts(from_player=True):
self.add_frontline(FrontLineInfo(front_line))
self.description += "DCS Liberation turn #" + str(self.game.turn) + "\n"
self.description += "=" * 15 + "\n\n"
# TODO: This should determine if runway is friendly through a method more robust than the existing string match
def generate_allied_flights_by_departure(self) -> None:
"""Create iterable to display allied flights grouped by departure airfield.
"""
self.description += (
"Most briefing information, including communications and flight "
"plan information, can be found on your kneeboard.\n\n"
)
self.generate_ongoing_war_text()
self.description += "\n"*2
self.description += "Your flights:" + "\n"
self.description += "=" * 15 + "\n\n"
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"
allied_flights_by_departure = defaultdict(list)
for flight in self.flights:
if not flight.client_units and flight.friendly:
name = flight.departure.airfield_name
if name in self.allied_flights_by_departure: # where else can we get this?
self.allied_flights_by_departure[name].append(flight)
else:
self.allied_flights_by_departure[name] = [flight]
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.comms:
self.description += "\n\nComms Frequencies:\n"
self.description += "=" * 15 + "\n"
for comm_info in self.comms:
self.description += f"{comm_info.name}: {comm_info.freq}\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.jtacs:
self.description += f"{jtac.region} -- Code : {jtac.code}\n"
self.mission.set_description_text(self.description)
self.mission.add_picture_blue(os.path.abspath(
"./resources/ui/splash_screen.png"))
def generate_ongoing_war_text(self):
self.description += "Current situation:\n"
self.description += "=" * 15 + "\n\n"
conflict_number = 0
for front_line in self.game.theater.conflicts(from_player=True):
conflict_number = conflict_number + 1
player_base = front_line.control_point_a
enemy_base = front_line.control_point_b
has_numerical_superiority = player_base.base.total_armor > enemy_base.base.total_armor
self.description += self.__random_frontline_sentence(player_base.name, enemy_base.name)
if enemy_base.id in player_base.stances.keys():
stance = player_base.stances[enemy_base.id]
if player_base.base.total_armor == 0:
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.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"
elif has_numerical_superiority:
self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
elif stance == CombatStance.ELIMINATION:
if has_numerical_superiority:
self.description += "On this location, our ground forces will focus on the destruction of enemy assets, before attempting to make progress toward " + enemy_base.name + ". "
self.description += "The enemy is already outnumbered, and this maneuver might draw a final blow to their forces.\n"
elif has_numerical_superiority:
self.description += "On this location, our ground forces will try an audacious assault against enemies in superior numbers. The operation is risky, and the enemy might counter attack.\n"
elif stance == CombatStance.BREAKTHROUGH:
if has_numerical_superiority:
self.description += "On this location, our ground forces will focus on progression toward " + enemy_base.name + ".\n"
elif has_numerical_superiority:
self.description += "On this location, our ground forces have been ordered to rush toward " + enemy_base.name + ". Wish them luck... We are also expecting a counter attack.\n"
elif stance in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
if has_numerical_superiority:
self.description += "On this location, our ground forces will hold position. We are not expecting an enemy assault.\n"
elif has_numerical_superiority:
self.description += "On this location, our ground forces have been ordered to hold still, and defend against enemy attacks. An enemy assault might be iminent.\n"
if conflict_number == 0:
self.description += "There are currently no fights on the ground.\n"
self.description += "\n\n"
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 {} 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 {}. ",
]
return random.choice(templates).format(player_base_name, enemy_base_name)

View File

@@ -144,16 +144,6 @@ class Conflict:
position = middle_point.point_from_heading(attack_heading, strength_delta * attack_distance / 2 - FRONTLINE_MIN_CP_DISTANCE)
return position, _opposite_heading(attack_heading)
@classmethod
def flight_frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
"""Returns the frontline vector without regard for exclusion zones, used in CAS flight plan"""
frontline = cls.frontline_position(theater, from_cp, to_cp)
center_position, heading = frontline
left_position = center_position.point_from_heading(_heading_sum(heading, -90), int(FRONTLINE_LENGTH/2))
right_position = center_position.point_from_heading(_heading_sum(heading, 90), int(FRONTLINE_LENGTH/2))
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
@classmethod
def frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:

View File

@@ -1,9 +1,13 @@
import random
from gen.sam.group_generator import ShipGroupGenerator
from gen.sam.group_generator import GroupGenerator
class CarrierGroupGenerator(ShipGroupGenerator):
class CarrierGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(CarrierGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
@@ -23,4 +27,4 @@ class CarrierGroupGenerator(ShipGroupGenerator):
self.add_unit(dd_type, "DD3", self.position.x + 4500, self.position.y + 8500, self.heading)
self.add_unit(dd_type, "DD4", self.position.x + 4500, self.position.y - 8500, self.heading)
self.get_generated_group().points[0].speed = 20
self.get_generated_group().points[0].speed = 20

View File

@@ -1,26 +1,15 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
Type_052C_Destroyer,
Type_052B_Destroyer,
Type_054A_Frigate,
CGN_1144_2_Pyotr_Velikiy,
)
from game.factions.faction import Faction
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
from gen.sam.group_generator import GroupGenerator
from dcs.ships import *
class ChineseNavyGroupGenerator(ShipGroupGenerator):
class ChineseNavyGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(ChineseNavyGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
@@ -49,5 +38,5 @@ class ChineseNavyGroupGenerator(ShipGroupGenerator):
class Type54GroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)

View File

@@ -1,21 +1,14 @@
from __future__ import annotations
from typing import TYPE_CHECKING
import random
from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import ShipGroupGenerator
from dcs.unittype import ShipType
from dcs.ships import Oliver_Hazzard_Perry_class, USS_Arleigh_Burke_IIa
if TYPE_CHECKING:
from game.game import Game
from gen.sam.group_generator import GroupGenerator
from dcs.ships import *
class DDGroupGenerator(ShipGroupGenerator):
class DDGroupGenerator(GroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction, ddtype: ShipType):
super(DDGroupGenerator, self).__init__(game, ground_object, faction)
def __init__(self, game, ground_object, faction, ddtype):
super(DDGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
self.ddtype = ddtype
def generate(self):
@@ -25,10 +18,10 @@ class DDGroupGenerator(ShipGroupGenerator):
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(OliverHazardPerryGroupGenerator, self).__init__(game, ground_object, faction, Oliver_Hazzard_Perry_class)
class ArleighBurkeGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)

View File

@@ -1,9 +1,13 @@
import random
from gen.sam.group_generator import ShipGroupGenerator
from gen.sam.group_generator import GroupGenerator
class LHAGroupGenerator(ShipGroupGenerator):
class LHAGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(LHAGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
@@ -18,4 +22,4 @@ class LHAGroupGenerator(ShipGroupGenerator):
self.add_unit(dd_type, "DD1", self.position.x + 1250, self.position.y + 1450, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 1250, self.position.y - 1450, self.heading)
self.get_generated_group().points[0].speed = 20
self.get_generated_group().points[0].speed = 20

View File

@@ -1,29 +1,15 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
FFL_1124_4_Grisha,
FSG_1241_1MP_Molniya,
FFG_11540_Neustrashimy,
FF_1135M_Rezky,
CG_1164_Moskva,
CGN_1144_2_Pyotr_Velikiy,
SSK_877,
SSK_641B
)
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject
from gen.sam.group_generator import GroupGenerator
from dcs.ships import *
if TYPE_CHECKING:
from game.game import Game
class RussianNavyGroupGenerator(GroupGenerator):
class RussianNavyGroupGenerator(ShipGroupGenerator):
def __init__(self, game, ground_object, faction):
super(RussianNavyGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
@@ -53,20 +39,21 @@ class RussianNavyGroupGenerator(ShipGroupGenerator):
class GrishaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(GrishaGroupGenerator, self).__init__(game, ground_object, faction, FFL_1124_4_Grisha)
class MolniyaGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(MolniyaGroupGenerator, self).__init__(game, ground_object, faction, FSG_1241_1MP_Molniya)
class KiloSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
def __init__(self, game, ground_object, faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)

View File

@@ -2,14 +2,18 @@ import random
from dcs.ships import Schnellboot_type_S130
from gen.sam.group_generator import ShipGroupGenerator
from gen.sam.group_generator import GroupGenerator
class SchnellbootGroupGenerator(ShipGroupGenerator):
class SchnellbootGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(SchnellbootGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
for i in range(random.randint(2, 4)):
self.add_unit(Schnellboot_type_S130, "Schnellboot" + str(i), self.position.x + i * random.randint(100, 250), self.position.y + (random.randint(100, 200)-100), self.heading)
self.get_generated_group().points[0].speed = 20
self.get_generated_group().points[0].speed = 20

View File

@@ -12,7 +12,6 @@ from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator
SHIP_MAP = {
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
"WW2LSTGroupGenerator": WW2LSTGroupGenerator,
@@ -46,7 +45,7 @@ def generate_ship_group(game, ground_object, faction_name: str):
return None
def generate_carrier_group(faction: str, game, ground_object):
def generate_carrier_group(faction:str, game, ground_object):
"""
This generate a carrier group
:param parentCp: The parent control point
@@ -59,7 +58,7 @@ def generate_carrier_group(faction: str, game, ground_object):
return generator.get_generated_group()
def generate_lha_group(faction: str, game, ground_object):
def generate_lha_group(faction:str, game, ground_object):
"""
This generate a lha carrier group
:param parentCp: The parent control point
@@ -69,4 +68,4 @@ def generate_lha_group(faction: str, game, ground_object):
"""
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()
return generator.get_generated_group()
return generator.get_generated_group()

View File

@@ -2,10 +2,14 @@ import random
from dcs.ships import Uboat_VIIC_U_flak
from gen.sam.group_generator import ShipGroupGenerator
from gen.sam.group_generator import GroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator):
class UBoatGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(UBoatGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):

View File

@@ -2,10 +2,14 @@ import random
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
from gen.sam.group_generator import ShipGroupGenerator
from gen.sam.group_generator import GroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator):
class WW2LSTGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(WW2LSTGroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):

View File

@@ -1,10 +1,9 @@
from __future__ import annotations
import logging
import operator
import random
import operator
from dataclasses import dataclass
from datetime import timedelta
from typing import Iterator, List, Optional, Set, TYPE_CHECKING, Tuple, Type
from dcs.unittype import FlyingType, UnitType
@@ -13,7 +12,7 @@ from game import db
from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information
from game.utils import nm_to_meter
from gen import Conflict
from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen.flights.ai_flight_planner_db import (
CAP_CAPABLE,
@@ -40,7 +39,6 @@ from theater import (
FrontLine,
MissionTarget,
TheaterGroundObject,
SamGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
@@ -243,15 +241,12 @@ class ObjectiveFinder:
found_targets: Set[str] = set()
for cp in self.enemy_control_points():
for ground_object in cp.ground_objects:
if not isinstance(ground_object, SamGroundObject):
continue
if ground_object.is_dead:
continue
if ground_object.name in found_targets:
continue
if ground_object.dcs_identifier != "AA":
continue
if not self.object_has_radar(ground_object):
continue
@@ -291,8 +286,6 @@ class ObjectiveFinder:
found_targets: Set[str] = set()
for enemy_cp in self.enemy_control_points():
for ground_object in enemy_cp.ground_objects:
if ground_object.is_dead:
continue
if ground_object.name in found_targets:
continue
ranges: List[int] = []
@@ -490,11 +483,11 @@ class CoalitionMissionPlanner:
def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int,
margin: int) -> Iterator[timedelta]:
interval = (latest - earliest) // count
margin: int) -> Iterator[int]:
interval = latest // count
for time in range(earliest, latest, interval):
error = random.randint(-margin, margin)
yield timedelta(minutes=max(0, time + error))
yield max(0, time + error)
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
@@ -519,7 +512,7 @@ class CoalitionMissionPlanner:
# airfields to hit grounded aircraft, since they're more likely
# to be present. Runway and air started aircraft will be
# delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) + tot
package.time_over_target = next(start_time) * 60 + tot
def message(self, title, text) -> None:
"""Emits a planning message to the player.

View File

@@ -27,7 +27,6 @@ from dcs.planes import (
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
@@ -105,7 +104,6 @@ INTERCEPT_CAPABLE = [
Mirage_2000_5,
Rafale_M,
F_14A_135_GR,
F_14B,
F_15C,
@@ -137,7 +135,6 @@ CAP_CAPABLE = [
F_86F_Sabre,
F_4E,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
@@ -186,7 +183,6 @@ CAP_PREFERRED = [
Mirage_2000_5,
F_86F_Sabre,
F_14A_135_GR,
F_14B,
F_15C,
@@ -230,7 +226,6 @@ CAS_CAPABLE = [
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,
@@ -395,7 +390,6 @@ STRIKE_CAPABLE = [
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,

View File

@@ -1,19 +1,17 @@
from __future__ import annotations
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING
from typing import Dict, Iterable, List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
from dcs.unittype import FlyingType
from dcs.unittype import UnitType
from game import db
from theater.controlpoint import ControlPoint, MissionTarget
if TYPE_CHECKING:
from gen.ato import Package
from gen.flights.flightplan import FlightPlan
class FlightType(Enum):
@@ -60,7 +58,17 @@ class FlightWaypointType(Enum):
SPLIT = 17
LOITER = 18
INGRESS_ESCORT = 19
INGRESS_DEAD = 20
class PredefinedWaypointCategory(Enum):
NOT_PREDEFINED = 0
ALLY_CP = 1
ENEMY_CP = 2
FRONTLINE = 3
ENEMY_BUILDING = 4
ENEMY_UNIT = 5
ALLY_BUILDING = 6
ALLY_UNIT = 7
class FlightWaypoint:
@@ -84,16 +92,19 @@ class FlightWaypoint:
self.name = ""
self.description = ""
self.targets: List[MissionTarget] = []
self.targetGroup: Optional[MissionTarget] = None
self.obj_name = ""
self.pretty_name = ""
self.category: PredefinedWaypointCategory = PredefinedWaypointCategory.NOT_PREDEFINED
self.only_for_player = False
self.data = None
# These are set very late by the air conflict generator (part of mission
# generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI.
self.tot: Optional[timedelta] = None
self.departure_time: Optional[timedelta] = None
self.tot: Optional[int] = None
self.departure_time: Optional[int] = None
@property
def position(self) -> Point:
@@ -127,8 +138,13 @@ class FlightWaypoint:
class Flight:
count: int = 0
client_count: int = 0
use_custom_loadout = False
preset_loadout_name = ""
group = False # Contains DCS Mission group data after mission has been generated
def __init__(self, package: Package, unit_type: FlyingType, count: int,
def __init__(self, package: Package, unit_type: UnitType, count: int,
from_cp: ControlPoint, flight_type: FlightType,
start_type: str) -> None:
self.package = package
@@ -136,27 +152,24 @@ class Flight:
self.count = count
self.from_cp = from_cp
self.flight_type = flight_type
# TODO: Replace with FlightPlan.
self.points: List[FlightWaypoint] = []
self.targets: List[MissionTarget] = []
self.loadout: Dict[str, str] = {}
self.start_type = start_type
self.use_custom_loadout = False
self.client_count = 0
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan(
package=package,
flight=self,
custom_waypoints=[]
)
@property
def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:]
# Late activation delay in seconds from mission start. This is not
# the same as the flight's takeoff time. Takeoff time depends on the
# mission's TOT and the other flights in the package. Takeoff time is
# determined by AirConflictGenerator.
self.scheduled_in = 0
def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
+ " (" + str(len(self.points)) + " wpt)"
def waypoint_with_type(
self,
types: Iterable[FlightWaypointType]) -> Optional[FlightWaypoint]:
for waypoint in self.points:
if waypoint.waypoint_type in types:
return waypoint
return None

File diff suppressed because it is too large Load Diff

View File

@@ -2,20 +2,61 @@ from __future__ import annotations
import logging
import math
from datetime import timedelta
from typing import Optional, TYPE_CHECKING
from dataclasses import dataclass
from typing import Iterable, Optional
from dcs.mapping import Point
from dcs.unittype import FlyingType
from game.utils import meter_to_nm
from gen.flights.flight import Flight
from gen.ato import Package
from gen.flights.flight import (
Flight,
FlightType,
FlightWaypoint,
FlightWaypointType,
)
if TYPE_CHECKING:
from gen.ato import Package
CAP_DURATION = 30 # Minutes
INGRESS_TYPES = {
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
}
class GroundSpeed:
@staticmethod
def mission_speed(package: Package) -> int:
speeds = set()
for flight in package.flights:
# Find a waypoint that matches the mission start waypoint and use
# that for the altitude of the mission. That may not be true for the
# whole mission, but it's probably good enough for now.
waypoint = flight.waypoint_with_type({
FlightWaypointType.INGRESS_CAS,
FlightWaypointType.INGRESS_ESCORT,
FlightWaypointType.INGRESS_SEAD,
FlightWaypointType.INGRESS_STRIKE,
FlightWaypointType.PATROL_TRACK,
})
if waypoint is None:
logging.error(f"Could not find ingress point for {flight}.")
if flight.points:
logging.warning(
"Using first waypoint for mission altitude.")
waypoint = flight.points[0]
else:
logging.warning(
"Flight has no waypoints. Assuming mission altitude "
"of 25000 feet.")
waypoint = FlightWaypoint(FlightWaypointType.NAV, 0, 0,
25000)
speeds.add(GroundSpeed.for_flight(flight, waypoint.alt))
return min(speeds)
@classmethod
def for_flight(cls, flight: Flight, altitude: int) -> int:
@@ -80,77 +121,52 @@ class GroundSpeed:
class TravelTime:
@staticmethod
def between_points(a: Point, b: Point, speed: float) -> timedelta:
def between_points(a: Point, b: Point, speed: float) -> int:
error_factor = 1.1
distance = meter_to_nm(a.distance_to_point(b))
return timedelta(hours=distance / speed * error_factor)
hours = distance / speed
seconds = hours * 3600
return int(seconds * error_factor)
class TotEstimator:
# An extra five minutes given as wiggle room. Expected to be spent at the
# hold point performing any last minute configuration.
HOLD_TIME = timedelta(minutes=5)
HOLD_TIME = 5 * 60
def __init__(self, package: Package) -> None:
self.package = package
self.timing = PackageWaypointTiming.for_package(package)
def mission_start_time(self, flight: Flight) -> timedelta:
def mission_start_time(self, flight: Flight) -> int:
takeoff_time = self.takeoff_time_for_flight(flight)
if takeoff_time is None:
# Could not determine takeoff time, probably due to a custom flight
# plan. Start immediately.
return timedelta()
startup_time = self.estimate_startup(flight)
ground_ops_time = self.estimate_ground_ops(flight)
start_time = takeoff_time - startup_time - ground_ops_time
# In case FP math has given us some barely below zero time, round to
# zero.
if math.isclose(start_time.total_seconds(), 0):
return timedelta()
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round down so *barely* above zero start times are just zero.
return timedelta(seconds=math.floor(start_time.total_seconds()))
return takeoff_time - startup_time - ground_ops_time
def takeoff_time_for_flight(self, flight: Flight) -> Optional[timedelta]:
travel_time = self.travel_time_to_rendezvous_or_target(flight)
def takeoff_time_for_flight(self, flight: Flight) -> int:
stop_types = {FlightWaypointType.JOIN, FlightWaypointType.PATROL_TRACK}
travel_time = self.estimate_waypoints_to_target(flight, stop_types)
if travel_time is None:
from gen.flights.flightplan import CustomFlightPlan
if not isinstance(flight.flight_plan, CustomFlightPlan):
logging.warning(
"Found no rendezvous or target point. Cannot estimate "
f"takeoff time takeoff time for {flight}.")
return None
logging.warning("Found no join point or patrol point. Cannot "
f"estimate takeoff time takeoff time for {flight}")
# Takeoff immediately.
return 0
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
tot = flight.flight_plan.tot_for_waypoint(
flight.flight_plan.join)
if tot is None:
logging.warning(
"Could not determine the TOT of the join point. Takeoff "
f"time for {flight} will be immediate.")
return None
# BARCAP flights do not coordinate with the rest of the package on join
# or ingress points.
if flight.flight_type == FlightType.BARCAP:
start_time = self.timing.race_track_start(flight)
else:
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME
start_time = self.timing.join
return start_time - travel_time - self.HOLD_TIME
def earliest_tot(self) -> timedelta:
earliest_tot = max((
def earliest_tot(self) -> int:
return max((
self.earliest_tot_for_flight(f) for f in self.package.flights
)) + self.HOLD_TIME
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
def earliest_tot_for_flight(self, flight: Flight) -> int:
"""Estimate fastest time from mission start to the target position.
For BARCAP flights, this is time to race track start. This ensures that
@@ -166,47 +182,211 @@ class TotEstimator:
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
"""
time_to_target = self.travel_time_to_target(flight)
if time_to_target is None:
logging.warning(f"Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return timedelta()
startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target
if flight.flight_type == FlightType.BARCAP:
time_to_target = self.estimate_waypoints_to_target(flight, {
FlightWaypointType.PATROL_TRACK
})
if time_to_target is None:
logging.warning(
f"Found no race track. Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return 0
else:
time_to_ingress = self.estimate_waypoints_to_target(
flight, INGRESS_TYPES
)
if time_to_ingress is None:
logging.warning(
f"Found no ingress types. Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return 0
assert self.package.waypoints is not None
time_to_target = time_to_ingress + TravelTime.between_points(
self.package.waypoints.ingress, self.package.target.position,
GroundSpeed.mission_speed(self.package))
return sum([
self.estimate_startup(flight),
self.estimate_ground_ops(flight),
time_to_target,
])
@staticmethod
def estimate_startup(flight: Flight) -> timedelta:
def estimate_startup(flight: Flight) -> int:
if flight.start_type == "Cold":
if flight.client_count:
return timedelta(minutes=10)
return 10 * 60
else:
# The AI doesn't seem to have a real startup procedure.
return timedelta(minutes=2)
return timedelta()
return 2 * 60
return 0
@staticmethod
def estimate_ground_ops(flight: Flight) -> timedelta:
def estimate_ground_ops(flight: Flight) -> int:
if flight.start_type in ("Runway", "In Flight"):
return timedelta()
return 0
if flight.from_cp.is_fleet:
return timedelta(minutes=2)
return 2 * 60
else:
return timedelta(minutes=5)
return 5 * 60
@staticmethod
def travel_time_to_target(flight: Flight) -> Optional[timedelta]:
if flight.flight_plan is None:
return None
return flight.flight_plan.travel_time_to_target
def estimate_waypoints_to_target(
self, flight: Flight,
stop_types: Iterable[FlightWaypointType]) -> Optional[int]:
total = 0
# TODO: This is AGL. We want MSL.
previous_altitude = 0
previous_position = flight.from_cp.position
for waypoint in flight.points:
position = Point(waypoint.x, waypoint.y)
total += TravelTime.between_points(
previous_position, position,
self.speed_to_waypoint(flight, waypoint, previous_altitude)
)
previous_position = position
previous_altitude = waypoint.alt
if waypoint.waypoint_type in stop_types:
return total
@staticmethod
def travel_time_to_rendezvous_or_target(
flight: Flight) -> Optional[timedelta]:
if flight.flight_plan is None:
return None
from gen.flights.flightplan import FormationFlightPlan
if isinstance(flight.flight_plan, FormationFlightPlan):
return flight.flight_plan.travel_time_to_rendezvous
return flight.flight_plan.travel_time_to_target
return None
def speed_to_waypoint(self, flight: Flight, waypoint: FlightWaypoint,
from_altitude: int) -> int:
# TODO: Adjust if AGL.
# We don't have an exact heightmap, but we should probably be performing
# *some* adjustment for NTTR since the minimum altitude of the map is
# near 2000 ft MSL.
alt_for_speed = min(from_altitude, waypoint.alt)
pre_join = (FlightWaypointType.LOITER, FlightWaypointType.JOIN)
if waypoint.waypoint_type == FlightWaypointType.ASCEND_POINT:
# Flights that start airborne already have some altitude and a good
# amount of speed.
factor = 1.0 if flight.start_type == "In Flight" else 0.5
return int(GroundSpeed.for_flight(flight, alt_for_speed) * factor)
elif waypoint.waypoint_type in pre_join:
return GroundSpeed.for_flight(flight, alt_for_speed)
return GroundSpeed.mission_speed(self.package)
@dataclass(frozen=True)
class PackageWaypointTiming:
#: The package being scheduled.
package: Package
#: The package join time.
join: int
#: The ingress waypoint TOT.
ingress: int
#: The egress waypoint TOT.
egress: int
#: The package split time.
split: int
@property
def target(self) -> int:
"""The package time over target."""
assert self.package.time_over_target is not None
return self.package.time_over_target
def race_track_start(self, flight: Flight) -> int:
if flight.flight_type == FlightType.BARCAP:
return self.target
else:
# The only other type that (currently) uses race tracks is TARCAP,
# which is sort of in need of cleanup. TARCAP is only valid on front
# lines and they participate in join points and patrol between the
# ingress and egress points rather than on a race track actually
# pointed at the enemy.
return self.ingress
def race_track_end(self, flight: Flight) -> int:
if flight.flight_type == FlightType.BARCAP:
return self.target + CAP_DURATION * 60
else:
# For TARCAP. See the explanation in race_track_start.
return self.egress
def push_time(self, flight: Flight, hold_point: FlightWaypoint) -> int:
assert self.package.waypoints is not None
return self.join - TravelTime.between_points(
Point(hold_point.x, hold_point.y),
self.package.waypoints.join,
GroundSpeed.for_flight(flight, hold_point.alt)
)
def tot_for_waypoint(self, flight: Flight,
waypoint: FlightWaypoint) -> Optional[int]:
target_types = (
FlightWaypointType.TARGET_GROUP_LOC,
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
if waypoint.waypoint_type == FlightWaypointType.JOIN:
return self.join
elif waypoint.waypoint_type in INGRESS_TYPES:
return self.ingress
elif waypoint.waypoint_type in target_types:
return self.target
elif waypoint.waypoint_type == FlightWaypointType.EGRESS:
return self.egress
elif waypoint.waypoint_type == FlightWaypointType.SPLIT:
return self.split
elif waypoint.waypoint_type == FlightWaypointType.PATROL_TRACK:
return self.race_track_start(flight)
return None
def depart_time_for_waypoint(self, waypoint: FlightWaypoint,
flight: Flight) -> Optional[int]:
if waypoint.waypoint_type == FlightWaypointType.LOITER:
return self.push_time(flight, waypoint)
elif waypoint.waypoint_type == FlightWaypointType.PATROL:
return self.race_track_end(flight)
return None
@classmethod
def for_package(cls, package: Package) -> PackageWaypointTiming:
assert package.waypoints is not None
# TODO: Plan similar altitudes for the in-country leg of the mission.
# Waypoint altitudes for a given flight *shouldn't* differ too much
# between the join and split points, so we don't need speeds for each
# leg individually since they should all be fairly similar. This doesn't
# hold too well right now since nothing is stopping each waypoint from
# jumping 20k feet each time, but that's a huge waste of energy we
# should be avoiding anyway.
if not package.flights:
raise ValueError("Cannot plan TOT for package with no flights")
group_ground_speed = GroundSpeed.mission_speed(package)
ingress = package.time_over_target - TravelTime.between_points(
package.waypoints.ingress,
package.target.position,
group_ground_speed
)
join = ingress - TravelTime.between_points(
package.waypoints.join,
package.waypoints.ingress,
group_ground_speed
)
egress = package.time_over_target + TravelTime.between_points(
package.target.position,
package.waypoints.egress,
group_ground_speed
)
split = egress + TravelTime.between_points(
package.waypoints.egress,
package.waypoints.split,
group_ground_speed
)
return cls(package, join, ingress, egress, split)

View File

@@ -1,62 +1,81 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import List, Optional, Tuple, Union
from typing import List, Optional, Union
from dcs.mapping import Point
from dcs.unit import Unit
from game.data.doctrine import Doctrine
from game.utils import nm_to_meter
from game.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True)
class StrikeTarget:
name: str
target: Union[TheaterGroundObject, Unit]
from ..runways import RunwayAssigner
class WaypointBuilder:
def __init__(self, conditions: Conditions, flight: Flight,
doctrine: Doctrine,
targets: Optional[List[StrikeTarget]] = None) -> None:
doctrine: Doctrine) -> None:
self.conditions = conditions
self.flight = flight
self.doctrine = doctrine
self.targets = targets
self.waypoints: List[FlightWaypoint] = []
self.ingress_point: Optional[FlightWaypoint] = None
@property
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
@staticmethod
def takeoff(departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
def build(self) -> List[FlightWaypoint]:
return self.waypoints
Note that the takeoff waypoint will automatically be created by pydcs
when we create the group, but creating our own before generation makes
the planning code simpler.
def ascent(self, departure: ControlPoint) -> None:
"""Create ascent waypoint for the given departure airfield or carrier.
Args:
departure: Departure airfield or carrier.
"""
position = departure.position
heading = RunwayAssigner(self.conditions).takeoff_heading(departure)
position = departure.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
FlightWaypointType.ASCEND_POINT,
position.x,
position.y,
0
500 if self.is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "TAKEOFF"
waypoint.name = "ASCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
return waypoint
waypoint.description = "Ascend"
waypoint.pretty_name = "Ascend"
self.waypoints.append(waypoint)
@staticmethod
def land(arrival: ControlPoint) -> FlightWaypoint:
def descent(self, arrival: ControlPoint) -> None:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
landing_heading = RunwayAssigner(self.conditions).landing_heading(
arrival)
heading = (landing_heading + 180) % 360
position = arrival.position.point_from_heading(
heading, nm_to_meter(5)
)
waypoint = FlightWaypoint(
FlightWaypointType.DESCENT_POINT,
position.x,
position.y,
300 if self.is_helo else self.doctrine.pattern_altitude
)
waypoint.name = "DESCEND"
waypoint.alt_type = "RADIO"
waypoint.description = "Descend to pattern altitude"
waypoint.pretty_name = "Descend"
self.waypoints.append(waypoint)
def land(self, arrival: ControlPoint) -> None:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
@@ -73,9 +92,9 @@ class WaypointBuilder:
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
return waypoint
self.waypoints.append(waypoint)
def hold(self, position: Point) -> FlightWaypoint:
def hold(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.LOITER,
position.x,
@@ -85,9 +104,9 @@ class WaypointBuilder:
waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time"
waypoint.name = "HOLD"
return waypoint
self.waypoints.append(waypoint)
def join(self, position: Point) -> FlightWaypoint:
def join(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.JOIN,
position.x,
@@ -97,9 +116,9 @@ class WaypointBuilder:
waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package"
waypoint.name = "JOIN"
return waypoint
self.waypoints.append(waypoint)
def split(self, position: Point) -> FlightWaypoint:
def split(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.SPLIT,
position.x,
@@ -109,35 +128,25 @@ class WaypointBuilder:
waypoint.pretty_name = "Split"
waypoint.description = "Depart from package"
waypoint.name = "SPLIT"
return waypoint
self.waypoints.append(waypoint)
def ingress_cas(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_CAS, position,
objective)
def ingress_cas(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_CAS, position, objective)
def ingress_escort(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_ESCORT, position,
objective)
def ingress_dead(self, position:Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_DEAD, position,
objective)
def ingress_escort(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_ESCORT, position, objective)
def ingress_sead(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
objective)
def ingress_sead(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_SEAD, position, objective)
def ingress_strike(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
objective)
def ingress_strike(self, position: Point, objective: MissionTarget) -> None:
self._ingress(FlightWaypointType.INGRESS_STRIKE, position, objective)
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
objective: MissionTarget) -> None:
if self.ingress_point is not None:
raise RuntimeError("A flight plan can have only one ingress point.")
waypoint = FlightWaypoint(
ingress_type,
position.x,
@@ -147,11 +156,10 @@ class WaypointBuilder:
waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name
waypoint.name = "INGRESS"
# TODO: This seems wrong, but it's what was there before.
waypoint.targets.append(objective)
return waypoint
self.waypoints.append(waypoint)
self.ingress_point = waypoint
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
def egress(self, position: Point, target: MissionTarget) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
@@ -161,46 +169,68 @@ class WaypointBuilder:
waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name
waypoint.name = "EGRESS"
return waypoint
self.waypoints.append(waypoint)
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}")
def dead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE {name}", location)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = location
def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}")
def sead_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE {name}", location)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = location
def strike_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}")
def strike_point(self, target: Union[TheaterGroundObject, Unit], name: str,
location: MissionTarget) -> None:
self._target_point(target, name, f"STRIKE {name}", location)
def _target_point(self, target: Union[TheaterGroundObject, Unit], name: str,
description: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
)
@staticmethod
def _target_point(target: StrikeTarget, description: str) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_POINT,
target.target.position.x,
target.target.position.y,
target.position.x,
target.position.y,
0
)
waypoint.description = description
waypoint.pretty_name = description
waypoint.name = target.name
waypoint.alt_type = "RADIO"
waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True
return waypoint
self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location)
def strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"STRIKE {target.name}", target)
def sead_area(self, target: MissionTarget) -> None:
self._target_area(f"SEAD on {target.name}", target)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = target
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"SEAD on {target.name}", target)
def dead_area(self, target: MissionTarget) -> None:
self._target_area(f"DEAD on {target.name}", target)
# TODO: Seems fishy.
if self.ingress_point is not None:
self.ingress_point.targetGroup = target
def dead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"DEAD on {target.name}", target)
def _target_area(self, name: str, location: MissionTarget) -> None:
if self.ingress_point is None:
raise RuntimeError(
"An ingress point must be added before target points."
)
@staticmethod
def _target_area(name: str, location: MissionTarget) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
location.position.x,
@@ -210,14 +240,15 @@ class WaypointBuilder:
waypoint.description = name
waypoint.pretty_name = name
waypoint.name = name
waypoint.alt_type = "RADIO"
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True
return waypoint
self.waypoints.append(waypoint)
# TODO: This seems wrong, but it's what was there before.
self.ingress_point.targets.append(location)
def cas(self, position: Point) -> FlightWaypoint:
def cas(self, position: Point) -> None:
waypoint = FlightWaypoint(
FlightWaypointType.CAS,
position.x,
@@ -228,10 +259,9 @@ class WaypointBuilder:
waypoint.description = "Provide CAS"
waypoint.name = "CAS"
waypoint.pretty_name = "CAS"
return waypoint
self.waypoints.append(waypoint)
@staticmethod
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
def race_track_start(self, position: Point, altitude: int) -> None:
"""Creates a racetrack start waypoint.
Args:
@@ -247,10 +277,9 @@ class WaypointBuilder:
waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point"
waypoint.pretty_name = "Race-track start"
return waypoint
self.waypoints.append(waypoint)
@staticmethod
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
def race_track_end(self, position: Point, altitude: int) -> None:
"""Creates a racetrack end waypoint.
Args:
@@ -266,10 +295,9 @@ class WaypointBuilder:
waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end"
return waypoint
self.waypoints.append(waypoint)
def race_track(self, start: Point, end: Point,
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
def race_track(self, start: Point, end: Point, altitude: int) -> None:
"""Creates two waypoint for a racetrack orbit.
Args:
@@ -277,11 +305,20 @@ class WaypointBuilder:
end: The ending racetrack waypoint.
altitude: The racetrack altitude.
"""
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
self.race_track_start(start, altitude)
self.race_track_end(end, altitude)
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
def rtb(self, arrival: ControlPoint) -> None:
"""Creates descent ant landing waypoints for the given control point.
Args:
arrival: Arrival airfield or carrier.
"""
self.descent(arrival)
self.land(arrival)
def escort(self, ingress: Point, target: MissionTarget,
egress: Point) -> None:
"""Creates the waypoints needed to escort the package.
Args:
@@ -295,8 +332,7 @@ class WaypointBuilder:
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target)
self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress, target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
@@ -307,6 +343,6 @@ class WaypointBuilder:
waypoint.name = "TARGET"
waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area"
self.waypoints.append(waypoint)
egress = self.egress(egress, target)
return ingress, waypoint, egress
self.egress(egress, target)

View File

@@ -27,14 +27,13 @@ TYPE_TANKS = [
Armor.MT_Pz_Kpfw_V_Panther_Ausf_G,
Armor.MT_Pz_Kpfw_IV_Ausf_H,
Armor.HT_Pz_Kpfw_VI_Tiger_I,
Armor.HT_Pz_Kpfw_VI_Ausf__B_Tiger_II,
Armor.HT_Pz_Kpfw_VI_Ausf__B__Tiger_II,
Armor.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV,
Armor.CT_Centaur_IV,
Armor.ST_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
@@ -74,15 +73,14 @@ TYPE_IFV = [
Armor.IFV_Marder,
Armor.IFV_MCV_80,
Armor.IFV_LAV_25,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# WW2
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
Armor.Daimler_Armoured_Car,
# Mods
frenchpack.ERC_90,

View File

@@ -1,18 +1,8 @@
"""Generators for creating the groups for ground objectives.
The classes in this file are responsible for creating the vehicle groups, ship
groups, statics, missile sites, and AA sites for the mission. Each of these
objectives is defined in the Theater by a TheaterGroundObject. These classes
create the pydcs groups and statics for those areas and add them to the mission.
"""
from __future__ import annotations
import logging
import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING
from typing import Dict, Iterator
from dcs import Mission
from dcs.country import Country
from dcs.statics import fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
@@ -20,339 +10,22 @@ from dcs.task import (
EPLRS,
OptAlarmState,
)
from dcs.unit import Ship, Vehicle, Unit
from dcs.unitgroup import Group, ShipGroup, StaticGroup
from dcs.unittype import StaticType, UnitType
from dcs.unit import Ship, Vehicle
from dcs.unitgroup import StaticGroup
from game import db
from game.data.building_data import FORTIFICATION_UNITS, FORTIFICATION_UNITS_ID
from game.db import unit_type_from_name
from theater import ControlPoint, TheaterGroundObject
from theater.theatergroundobject import (
BuildingGroundObject, CarrierGroundObject,
GenericCarrierGroundObject,
LhaGroundObject, ShipGroundObject,
)
from .conflictgen import Conflict
from .radios import RadioFrequency, RadioRegistry
from .radios import RadioRegistry
from .runways import RunwayData
from .tacan import TacanBand, TacanChannel, TacanRegistry
if TYPE_CHECKING:
from game import Game
from .tacan import TacanBand, TacanRegistry
FARP_FRONTLINE_DISTANCE = 10000
AA_CP_MIN_DISTANCE = 40000
class GenericGroundObjectGenerator:
"""An unspecialized ground object generator.
Currently used only for SAM and missile (V1/V2) sites.
"""
def __init__(self, ground_object: TheaterGroundObject, country: Country,
game: Game, mission: Mission) -> None:
self.ground_object = ground_object
self.country = country
self.game = game
self.m = mission
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
for group in self.ground_object.groups:
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized unit type: {group.units[0].type}")
vg = self.m.vehicle_group(self.country, group.name, unit_type,
position=group.position,
heading=group.units[0].heading)
vg.units[0].name = self.m.string(group.units[0].name)
vg.units[0].player_can_drive = True
for i, u in enumerate(group.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(),
self.m.string(u.name), u.type)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
vehicle.player_can_drive = True
vg.add_unit(vehicle)
self.enable_eplrs(vg, unit_type)
self.set_alarm_state(vg)
@staticmethod
def enable_eplrs(group: Group, unit_type: UnitType) -> None:
if hasattr(unit_type, 'eplrs'):
if unit_type.eplrs:
group.points[0].tasks.append(EPLRS(group.id))
def set_alarm_state(self, group: Group) -> None:
if self.game.settings.perf_red_alert_state:
group.points[0].tasks.append(OptAlarmState(2))
else:
group.points[0].tasks.append(OptAlarmState(1))
class BuildingSiteGenerator(GenericGroundObjectGenerator):
"""Generator for building sites.
Building sites are the primary type of non-airbase objective locations that
appear on the map. They come in a handful of variants each with different
types of buildings and ground units.
"""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
if self.ground_object.dcs_identifier in warehouse_map:
static_type = warehouse_map[self.ground_object.dcs_identifier]
self.generate_static(static_type)
elif self.ground_object.dcs_identifier in fortification_map:
static_type = fortification_map[self.ground_object.dcs_identifier]
self.generate_static(static_type)
elif self.ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
for f in FORTIFICATION_UNITS:
if f.id == self.ground_object.dcs_identifier:
unit_type = f
self.generate_vehicle_group(unit_type)
break
else:
logging.error(
f"{self.ground_object.dcs_identifier} not found in static maps")
def generate_vehicle_group(self, unit_type: UnitType) -> None:
if not self.ground_object.is_dead:
self.m.vehicle_group(
country=self.country,
name=self.ground_object.group_name,
_type=unit_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
)
def generate_static(self, static_type: StaticType) -> None:
self.m.static_group(
country=self.country,
name=self.ground_object.group_name,
_type=static_type,
position=self.ground_object.position,
heading=self.ground_object.heading,
dead=self.ground_object.is_dead,
)
class GenericCarrierGenerator(GenericGroundObjectGenerator):
"""Base type for carrier group generation.
Used by both CV(N) groups and LHA groups.
"""
def __init__(self, ground_object: GenericCarrierGroundObject,
control_point: ControlPoint, country: Country, game: Game,
mission: Mission, radio_registry: RadioRegistry,
tacan_registry: TacanRegistry, icls_alloc: Iterator[int],
runways: Dict[str, RunwayData]) -> None:
super().__init__(ground_object, country, game, mission)
self.ground_object = ground_object
self.control_point = control_point
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.icls_alloc = icls_alloc
self.runways = runways
def generate(self) -> None:
# TODO: Require single group?
for group in self.ground_object.groups:
if not group.units:
logging.warning(
f"Found empty carrier group in {self.control_point}")
continue
atc = self.radio_registry.alloc_uhf()
ship_group = self.configure_carrier(group, atc)
for unit in group.units[1:]:
ship_group.add_unit(self.create_ship(unit, atc))
tacan = self.tacan_registry.alloc_for_band(TacanBand.X)
tacan_callsign = self.tacan_callsign()
icls = next(self.icls_alloc)
brc = self.steam_into_wind(ship_group)
self.activate_beacons(ship_group, tacan, tacan_callsign, icls)
self.add_runway_data(brc or 0, atc, tacan, tacan_callsign, icls)
def get_carrier_type(self, group: Group) -> UnitType:
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized carrier name: {group.units[0].type}")
return unit_type
def configure_carrier(self, group: Group,
atc_channel: RadioFrequency) -> ShipGroup:
unit_type = self.get_carrier_type(group)
ship_group = self.m.ship_group(self.country, group.name, unit_type,
position=group.position,
heading=group.units[0].heading)
ship_group.set_frequency(atc_channel.hertz)
ship_group.units[0].name = self.m.string(group.units[0].name)
return ship_group
def create_ship(self, unit: Unit, atc_channel: RadioFrequency) -> Ship:
ship = Ship(self.m.next_unit_id(),
self.m.string(unit.name),
unit_type_from_name(unit.type))
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
# TODO: Verify.
ship.set_frequency(atc_channel.hertz)
return ship
def steam_into_wind(self, group: ShipGroup) -> Optional[int]:
brc = self.m.weather.wind_at_ground.direction + 180
for attempt in range(5):
point = group.points[0].position.point_from_heading(
brc, 100000 - attempt * 20000)
if self.game.theater.is_in_sea(point):
group.add_waypoint(point)
return brc
return None
def tacan_callsign(self) -> str:
raise NotImplementedError
@staticmethod
def activate_beacons(group: ShipGroup, tacan: TacanChannel,
callsign: str, icls: int) -> None:
group.points[0].tasks.append(ActivateBeaconCommand(
channel=tacan.number,
modechannel=tacan.band.value,
callsign=callsign,
unit_id=group.units[0].id,
aa=False
))
group.points[0].tasks.append(ActivateICLSCommand(
icls, unit_id=group.units[0].id
))
def add_runway_data(self, brc: int, atc: RadioFrequency,
tacan: TacanChannel, callsign: str, icls: int) -> None:
# 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[self.control_point.name] = RunwayData(
self.control_point.name,
brc,
"N/A",
atc=atc,
tacan=tacan,
tacan_callsign=callsign,
icls=icls,
)
class CarrierGenerator(GenericCarrierGenerator):
"""Generator for CV(N) groups."""
def get_carrier_type(self, group: Group) -> UnitType:
unit_type = super().get_carrier_type(group)
if self.game.settings.supercarrier:
unit_type = db.upgrade_to_supercarrier(unit_type,
self.control_point.name)
return unit_type
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"STE",
"CVN",
"CVH",
"CCV",
"ACC",
"ARC",
"GER",
"ABR",
"LIN",
"TRU",
])
class LhaGenerator(GenericCarrierGenerator):
"""Generator for LHA groups."""
def tacan_callsign(self) -> str:
# TODO: Assign these properly.
return random.choice([
"LHD",
"LHA",
"LHB",
"LHC",
"LHD",
"LDS",
])
class ShipObjectGenerator(GenericGroundObjectGenerator):
"""Generator for non-carrier naval groups."""
def generate(self) -> None:
if self.game.position_culled(self.ground_object.position):
return
for group in self.ground_object.groups:
if not group.units:
logging.warning(f"Found empty group in {self.ground_object}")
continue
unit_type = unit_type_from_name(group.units[0].type)
if unit_type is None:
raise RuntimeError(
f"Unrecognized unit type: {group.units[0].type}")
self.generate_group(group, unit_type)
def generate_group(self, group_def: Group, unit_type: UnitType):
group = self.m.ship_group(self.country, group_def.name, unit_type,
position=group_def.position,
heading=group_def.units[0].heading)
group.units[0].name = self.m.string(group_def.units[0].name)
# TODO: Skipping the first unit looks like copy pasta from the carrier.
for unit in group_def.units[1:]:
unit_type = unit_type_from_name(unit.type)
ship = Ship(self.m.next_unit_id(),
self.m.string(unit.name), unit_type)
ship.position.x = unit.position.x
ship.position.y = unit.position.y
ship.heading = unit.heading
group.add_unit(ship)
self.set_alarm_state(group)
class GroundObjectsGenerator:
"""Creates DCS groups and statics for the theater during mission generation.
Most of the work of group/static generation is delegated to the other
generator classes. This class is responsible for finding each of the
locations for spawning ground objects, determining their types, and creating
the appropriate generators.
"""
FARP_CAPACITY = 4
def __init__(self, mission: Mission, conflict: Conflict, game,
@@ -388,34 +61,188 @@ class GroundObjectsGenerator:
)
def generate(self):
for cp in self.game.theater.controlpoints:
if cp.captured:
country_name = self.game.player_country
country = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
country = self.game.enemy_country
side = self.m.country(country)
for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(ground_object, country,
self.game, self.m)
elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(ground_object, country,
self.game, self.m)
if ground_object.dcs_identifier == "AA":
if self.game.position_culled(ground_object.position):
continue
for g in ground_object.groups:
if len(g.units) > 0:
utype = unit_type_from_name(g.units[0].type)
if not ground_object.sea_object:
vg = self.m.vehicle_group(side, g.name, utype, position=g.position, heading=g.units[0].heading)
vg.units[0].name = self.m.string(g.units[0].name)
vg.units[0].player_can_drive = True
for i, u in enumerate(g.units):
if i > 0:
vehicle = Vehicle(self.m.next_unit_id(), self.m.string(u.name), u.type)
vehicle.position.x = u.position.x
vehicle.position.y = u.position.y
vehicle.heading = u.heading
vehicle.player_can_drive = True
vg.add_unit(vehicle)
if hasattr(utype, 'eplrs'):
if utype.eplrs:
vg.points[0].tasks.append(EPLRS(vg.id))
else:
vg = self.m.ship_group(side, g.name, utype, position=g.position,
heading=g.units[0].heading)
vg.units[0].name = self.m.string(g.units[0].name)
for i, u in enumerate(g.units):
utype = unit_type_from_name(u.type)
if i > 0:
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), utype)
ship.position.x = u.position.x
ship.position.y = u.position.y
ship.heading = u.heading
vg.add_unit(ship)
if self.game.settings.perf_red_alert_state:
vg.points[0].tasks.append(OptAlarmState(2))
else:
vg.points[0].tasks.append(OptAlarmState(1))
elif ground_object.dcs_identifier in ["CARRIER", "LHA"]:
for g in ground_object.groups:
if len(g.units) > 0:
utype = unit_type_from_name(g.units[0].type)
if ground_object.dcs_identifier == "CARRIER" and self.game.settings.supercarrier == True:
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):
if i > 0:
ship = Ship(self.m.next_unit_id(), self.m.string(u.name), unit_type_from_name(u.type))
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)
found_carrier_destination = False
attempt = 0
brc = self.m.weather.wind_at_ground.direction + 180
while not found_carrier_destination and attempt < 5:
point = sg.points[0].position.point_from_heading(brc, 100000-attempt*20000)
if self.game.theater.is_in_sea(point):
found_carrier_destination = True
sg.add_waypoint(point)
else:
attempt = attempt + 1
# Set UP TACAN and ICLS
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,
brc,
"N/A",
atc=atc_channel,
tacan=tacan,
tacan_callsign=tacan_callsign,
icls=icls_channel,
)
else:
generator = GenericGroundObjectGenerator(ground_object,
country, self.game,
self.m)
generator.generate()
if self.game.position_culled(ground_object.position):
continue
static_type = None
if ground_object.dcs_identifier in warehouse_map:
static_type = warehouse_map[ground_object.dcs_identifier]
elif ground_object.dcs_identifier in fortification_map:
static_type = fortification_map[ground_object.dcs_identifier]
elif ground_object.dcs_identifier in FORTIFICATION_UNITS_ID:
for f in FORTIFICATION_UNITS:
if f.id == ground_object.dcs_identifier:
unit_type = f
break
else:
print("Didn't find {} in static _map(s)!".format(ground_object.dcs_identifier))
continue
if static_type is None:
if not ground_object.is_dead:
group = self.m.vehicle_group(
country=side,
name=ground_object.string_identifier,
_type=unit_type,
position=ground_object.position,
heading=ground_object.heading,
)
logging.info("generated {}object identifier {} with mission id {}".format(
"dead " if ground_object.is_dead else "", group.name, group.id))
else:
group = self.m.static_group(
country=side,
name=ground_object.string_identifier,
_type=static_type,
position=ground_object.position,
heading=ground_object.heading,
dead=ground_object.is_dead,
)
logging.info("generated {}object identifier {} with mission id {}".format("dead " if ground_object.is_dead else "", group.name, group.id))

View File

@@ -26,7 +26,7 @@ import datetime
from collections import defaultdict
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Optional, Tuple, TYPE_CHECKING
from typing import Dict, List, Optional, Tuple
from PIL import Image, ImageDraw, ImageFont
from dcs.mission import Mission
@@ -42,8 +42,7 @@ from .flights.flight import FlightWaypoint, FlightWaypointType
from .radios import RadioFrequency
from .runways import RunwayData
if TYPE_CHECKING:
from game import Game
class KneeboardPageWriter:
"""Creates kneeboard images."""
@@ -158,10 +157,10 @@ class FlightPlanBuilder:
self._format_time(waypoint.waypoint.departure_time),
])
def _format_time(self, time: Optional[datetime.timedelta]) -> str:
def _format_time(self, time: Optional[int]) -> str:
if time is None:
return ""
local_time = self.start_time + time
local_time = self.start_time + datetime.timedelta(seconds=time)
return local_time.strftime(f"%H:%M:%S")
def _waypoint_distance(self, waypoint: FlightWaypoint) -> str:
@@ -190,7 +189,7 @@ class FlightPlanBuilder:
distance = meter_to_nm(self.last_waypoint.position.distance_to_point(
waypoint.position
))
duration = (waypoint.tot - last_time).total_seconds() / 3600
duration = (waypoint.tot - last_time) / 3600
return f"{int(distance / duration)} kt"
def build(self) -> List[List[str]]:
@@ -311,8 +310,8 @@ class BriefingPage(KneeboardPage):
class KneeboardGenerator(MissionInfoGenerator):
"""Creates kneeboard pages for each client flight in the mission."""
def __init__(self, mission: Mission, game: "Game") -> None:
super().__init__(mission, game)
def __init__(self, mission: Mission) -> None:
super().__init__(mission)
def generate(self) -> None:
"""Generates a kneeboard per client flight."""

View File

@@ -1,22 +0,0 @@
from dataclasses import dataclass, field
from typing import List
from gen.locations.preset_locations import PresetLocation
@dataclass
class PresetControlPointLocations:
"""A repository of preset locations for a given control point"""
# List of possible ashore locations to generate objects (Represented in miz file by an APC_AAV_7)
ashore_locations: List[PresetLocation] = field(default_factory=list)
# List of possible offshore locations to generate ship groups (Represented in miz file by an Oliver Hazard Perry)
offshore_locations: List[PresetLocation] = field(default_factory=list)
# Possible antiship missiles sites locations (Represented in miz file by Iranian Silkworm missiles)
antiship_locations: List[PresetLocation] = field(default_factory=list)
# List of possible powerplants locations (Represented in miz file by static Workshop A object, USA)
powerplant_locations: List[PresetLocation] = field(default_factory=list)

View File

@@ -1,59 +0,0 @@
from pathlib import Path
from typing import List
from dcs import Mission, ships
from dcs.vehicles import MissilesSS
from gen.locations.preset_control_point_locations import PresetControlPointLocations
from gen.locations.preset_locations import PresetLocation
class PresetLocationFinder:
@staticmethod
def compute_possible_locations(terrain_name: str, cp_name: str) -> PresetControlPointLocations:
"""
Extract the list of preset locations from miz data
:param terrain_name: Terrain/Map name
:param cp_name: Control Point / Airbase name
:return:
"""
miz_file = Path("./resources/mizdata/", terrain_name.lower(), cp_name + ".miz")
offshore_locations: List[PresetLocation] = []
ashore_locations: List[PresetLocation] = []
powerplants_locations: List[PresetLocation] = []
antiship_locations: List[PresetLocation] = []
if miz_file.exists():
m = Mission()
m.load_file(miz_file.absolute())
for vehicle_group in m.country("USA").vehicle_group:
if len(vehicle_group.units) > 0:
ashore_locations.append(PresetLocation(vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name))
for ship_group in m.country("USA").ship_group:
if len(ship_group.units) > 0 and ship_group.units[0].type == ships.Oliver_Hazzard_Perry_class.id:
offshore_locations.append(PresetLocation(ship_group.position,
ship_group.units[0].heading,
ship_group.name))
for static_group in m.country("USA").static_group:
if len(static_group.units) > 0:
powerplants_locations.append(PresetLocation(static_group.position,
static_group.units[0].heading,
static_group.name))
if m.country("Iran") is not None:
for vehicle_group in m.country("Iran").vehicle_group:
if len(vehicle_group.units) > 0 and vehicle_group.units[0].type == MissilesSS.SS_N_2_Silkworm.id:
antiship_locations.append(PresetLocation(vehicle_group.position,
vehicle_group.units[0].heading,
vehicle_group.name))
return PresetControlPointLocations(ashore_locations, offshore_locations,
antiship_locations, powerplants_locations)

View File

@@ -1,15 +0,0 @@
from dataclasses import dataclass
from dcs import Point
@dataclass
class PresetLocation:
"""A preset location"""
position: Point
heading: int
id: str
def __str__(self):
return "-" * 10 + "X: {}\n Y: {}\nHdg: {}°\nId: {}".format(self.position.x, self.position.y, self.heading,
self.id) + "-" * 10

View File

@@ -1,12 +1,10 @@
import logging
import random
from game import db
from gen.missiles.scud_site import ScudGenerator
from gen.missiles.v1_group import V1GroupGenerator
MISSILES_MAP = {
"V1GroupGenerator": V1GroupGenerator,
"ScudGenerator": ScudGenerator
}

View File

@@ -1,30 +0,0 @@
import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from gen.sam.group_generator import GroupGenerator
class ScudGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(ScudGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
# Scuds
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(MissilesSS.SRBM_SS_1C_Scud_B_9K72_LN_9P117M, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
# Commander
self.add_unit(Unarmed.Transport_UAZ_469, "Kubel#0", self.position.x - 35, self.position.y - 20,
self.heading)
# Shorad
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "SHILKA#0", self.position.x - 55, self.position.y - 38,
self.heading)
self.add_unit(AirDefence.SAM_SA_9_Strela_1_9P31, "STRELA#0",
self.position.x + 200, self.position.y + 15, 90)

View File

@@ -126,25 +126,6 @@ RADIOS: List[Radio] = [
Radio("R&S M3AR VHF", MHz(120), MHz(174), step=MHz(1)),
Radio("R&S M3AR UHF", MHz(225), MHz(400), step=MHz(1)),
# MiG-15bis
Radio("RSI-6K HF", MHz(3, 750), MHz(5), step=kHz(25)),
# MiG-19P
Radio("RSIU-4V", MHz(100), MHz(150), step=MHz(1)),
# MiG-21bis
Radio("RSIU-5V", MHz(118), MHz(140), step=MHz(1)),
# Ka-50
# Note: Also capable of 100MHz-150MHz, but we can't model gaps.
Radio("R-800L1", MHz(220), MHz(400), step=kHz(25)),
Radio("R-828", MHz(20), MHz(60), step=kHz(25)),
# UH-1H
Radio("AN/ARC-51BX", MHz(225), MHz(400), step=kHz(50)),
Radio("AN/ARC-131", MHz(30), MHz(76), step=kHz(50)),
Radio("AN/ARC-134", MHz(116), MHz(150), step=kHz(25)),
]

View File

@@ -1,29 +0,0 @@
import random
from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator
class Flak18Generator(GroupGenerator):
"""
This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
"""
name = "WW2 Flak Site"
price = 40
def generate(self):
spacing = random.randint(30, 60)
index = 0
for i in range(3):
for j in range(2):
index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.heading)
# Add a commander truck
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#", self.position.x - 35, self.position.y - 20, self.heading)

View File

@@ -1,34 +0,0 @@
import random
from dcs.vehicles import AirDefence, Unarmed, Armor
from gen.sam.group_generator import GroupGenerator
class AllyWW2FlakGenerator(GroupGenerator):
"""
This generate an ally flak artillery group
"""
name = "WW2 Ally Flak Site"
price = 140
def generate(self):
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AA_gun_QF_3_7, "AA#" + str(i), position[0], position[1], position[2])
positions = self.get_circular_position(8, launcher_distance=100, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M1_37mm, "AA#" + str(4 + i), position[0], position[1], position[2])
positions = self.get_circular_position(8, launcher_distance=150, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_M45_Quadmount, "AA#" + str(12 + i), position[0], position[1], position[2])
# Add a commander truck
self.add_unit(Unarmed.Willys_MB, "CMD#1", self.position.x, self.position.y - 20, random.randint(0, 360))
self.add_unit(Armor.M30_Cargo_Carrier, "LOG#1", self.position.x, self.position.y + 20, random.randint(0, 360))
self.add_unit(Armor.M4_Tractor, "LOG#2", self.position.x + 20, self.position.y, random.randint(0, 360))
self.add_unit(Unarmed.Bedford_MWD, "LOG#3", self.position.x - 20, self.position.y, random.randint(0, 360))

View File

@@ -1,72 +0,0 @@
import random
from dcs.vehicles import AirDefence, Unarmed
from gen.sam.group_generator import GroupGenerator
class EarlyColdWarFlakGenerator(GroupGenerator):
"""
This generator attempt to mimic an early cold-war era flak AAA site.
The Flak 18 88mm is used as the main long range gun and 2 Bofors 40mm guns provide short range protection.
This does not include search lights and telemeter computer (Kdo.G 40) because these are paid units only available in WW2 asset pack
"""
name = "Early Cold War Flak Site"
price = 58
def generate(self):
spacing = random.randint(30, 60)
index = 0
# Long range guns
for i in range(3):
for j in range(2):
index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.heading)
# Short range guns
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
self.position.x - 40, self.position.y - 40, self.heading + 180),
self.add_unit(AirDefence.AAA_Bofors_40mm, "SHO#1",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
# Add a truck
self.add_unit(Unarmed.Transport_KAMAZ_43101, "Truck#", self.position.x - 60, self.position.y - 20, self.heading)
class ColdWarFlakGenerator(GroupGenerator):
"""
This generator attempt to mimic a cold-war era flak AAA site.
The Flak 18 88mm is used as the main long range gun while 2 Zu-23 guns provide short range protection.
The site is also fitted with a P-19 radar for early detection.
"""
name = "Cold War Flak Site"
price = 72
def generate(self):
spacing = random.randint(30, 60)
index = 0
# Long range guns
for i in range(3):
for j in range(2):
index = index + 1
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AAA#" + str(index),
self.position.x + spacing * i + random.randint(1, 5),
self.position.y + spacing * j + random.randint(1, 5), self.heading)
# Short range guns
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
self.position.x - 40, self.position.y - 40, self.heading + 180),
self.add_unit(AirDefence.AAA_ZU_23_Closed, "SHO#1",
self.position.x + spacing * 2 + 40, self.position.y + spacing + 40, self.heading),
# Add a P19 Radar for EWR
self.add_unit(AirDefence.SAM_SR_P_19, "SR#0", self.position.x - 60, self.position.y - 20, self.heading)

View File

@@ -1,98 +0,0 @@
from dcs.vehicles import AirDefence
from dcs.unittype import VehicleType
from gen.sam.group_generator import GroupGenerator
class EwrGenerator(GroupGenerator):
@property
def unit_type(self) -> VehicleType:
raise NotImplementedError
def generate(self) -> None:
self.add_unit(self.unit_type, "EWR", self.position.x, self.position.y,
self.heading)
class BoxSpringGenerator(EwrGenerator):
"""1L13 "Box Spring" EWR."""
unit_type = AirDefence.EWR_1L13
class TallRackGenerator(EwrGenerator):
"""55G6 "Tall Rack" EWR."""
unit_type = AirDefence.EWR_55G6
class DogEarGenerator(EwrGenerator):
"""9S80M1 "Dog Ear" EWR.
This is the SA-8 search radar, but used as an early warning radar.
"""
unit_type = AirDefence.CP_9S80M1_Sborka
class RolandEwrGenerator(EwrGenerator):
"""Roland EWR.
This is the Roland search radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_Roland_EWR
class FlatFaceGenerator(EwrGenerator):
"""P-19 "Flat Face" EWR.
This is the SA-3 search radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_SR_P_19
class PatriotEwrGenerator(EwrGenerator):
"""Patriot EWR.
This is the Patriot search/track radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_Patriot_STR_AN_MPQ_53
class BigBirdGenerator(EwrGenerator):
"""64H6E "Big Bird" EWR.
This is the SA-10 track radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_SA_10_S_300PS_SR_64H6E
class SnowDriftGenerator(EwrGenerator):
"""9S18M1 "Snow Drift" EWR.
This is the SA-11 search radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_SA_11_Buk_SR_9S18M1
class StraightFlushGenerator(EwrGenerator):
"""1S91 "Straight Flush" EWR.
This is the SA-6 search/track radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_SA_6_Kub_STR_9S91
class HawkEwrGenerator(EwrGenerator):
"""Hawk EWR.
This is the Hawk search radar, but used as an early warning radar.
"""
unit_type = AirDefence.SAM_Hawk_SR_AN_MPQ_50

View File

@@ -1,39 +0,0 @@
import random
from dcs.vehicles import AirDefence, Unarmed, Infantry
from gen.sam.group_generator import GroupGenerator
class FreyaGenerator(GroupGenerator):
"""
This generate a German flak artillery group using only free units, thus not requiring the WW2 asset pack
"""
name = "Freya EWR Site"
price = 60
def generate(self):
# TODO : would be better with the Concrete structure that is supposed to protect it
self.add_unit(AirDefence.EWR_FuMG_401_Freya_LZ, "EWR#1", self.position.x, self.position.y, self.heading)
positions = self.get_circular_position(4, launcher_distance=50, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Flak_Vierling_38, "AA#" + str(i), position[0], position[1], position[2])
positions = self.get_circular_position(4, launcher_distance=100, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_8_8cm_Flak_18, "AA#" + str(4+i), position[0], position[1], position[2])
# Command/Logi
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#1", self.position.x - 20, self.position.y - 20, self.heading)
self.add_unit(Unarmed.Sd_Kfz_7, "Sdkfz#1", self.position.x + 20, self.position.y + 22, self.heading)
self.add_unit(Unarmed.Sd_Kfz_2, "Sdkfz#2", self.position.x - 22, self.position.y + 20, self.heading)
# Maschinensatz_33 and Kdo.g 40 Telemeter
self.add_unit(AirDefence.Maschinensatz_33, "Energy#1", self.position.x + 20, self.position.y - 20, self.heading)
self.add_unit(AirDefence.AAA_Kdo_G_40, "Telemeter#1", self.position.x + 20, self.position.y - 10, self.heading)
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#1", self.position.x + 20, self.position.y - 14, self.heading)
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#2", self.position.x + 20, self.position.y - 22, self.heading)
self.add_unit(Infantry.Infantry_Mauser_98, "Inf#3", self.position.x + 20, self.position.y - 24, self.heading + 45)

View File

@@ -1,15 +1,23 @@
from abc import ABC
import random
from game import Game
from dcs.vehicles import AirDefence
from game import db
from gen.sam.group_generator import GroupGenerator
from theater.theatergroundobject import SamGroundObject
class GenericSamGroupGenerator(GroupGenerator, ABC):
class GenericSamGroupGenerator(GroupGenerator):
"""
This is the base for all SAM group generators
"""
def __init__(self, game, ground_object, faction):
self.faction = faction
super(GenericSamGroupGenerator, self).__init__(game, ground_object)
def __init__(self, game: Game, ground_object: SamGroundObject) -> None:
ground_object.skynet_capable = True
super().__init__(game, ground_object)
@property
def groupNamePrefix(self) -> str:
# prefix the SAM site for use with the Skynet IADS plugin
if self.faction == self.game.player_name: # this is the player faction
return "BLUE SAM "
else:
return "RED SAM "

View File

@@ -1,47 +1,38 @@
from __future__ import annotations
import math
import random
from typing import TYPE_CHECKING, Optional
from dcs import unitgroup
from dcs.point import PointAction
from dcs.unit import Vehicle, Ship
from dcs.unittype import VehicleType
from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
from dcs.unit import Vehicle
# TODO: Generate a group description rather than a pydcs group.
# It appears that all of this work gets redone at miz generation time (see
# groundobjectsgen for an example). We can do less work and include the data we
# care about in the format we want if we just generate our own group description
# types rather than pydcs groups.
class GroupGenerator:
class GroupGenerator():
def __init__(self, game: Game, ground_object: TheaterGroundObject) -> None:
def __init__(self, game, ground_object):
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(),
self.go.group_name)
self.vg = unitgroup.VehicleGroup(self.game.next_group_id(), self.groupNamePrefix + self.go.group_identifier)
wp = self.vg.add_waypoint(self.position, PointAction.OffRoad, 0)
wp.ETA_locked = True
@property
def groupNamePrefix(self) -> str:
return ""
def generate(self):
raise NotImplementedError
def get_generated_group(self) -> unitgroup.VehicleGroup:
def get_generated_group(self):
return self.vg
def add_unit(self, unit_type: VehicleType, name: str, pos_x: float,
pos_y: float, heading: int) -> Vehicle:
def add_unit(self, unit_type, name, pos_x, pos_y, heading):
nn = "cgroup|" + str(self.go.cp_id) + '|' + str(self.go.group_id) + '|' + str(self.go.group_identifier) + "|" + name
unit = Vehicle(self.game.next_unit_id(),
f"{self.go.group_name}|{name}", unit_type.id)
nn, unit_type.id)
unit.position.x = pos_x
unit.position.y = pos_y
unit.heading = heading
@@ -83,25 +74,3 @@ class GroupGenerator:
current_offset += outer_offset
return positions
class ShipGroupGenerator(GroupGenerator):
"""Abstract class for other ship generator classes"""
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
self.game = game
self.go = ground_object
self.position = ground_object.position
self.heading = random.randint(0, 359)
self.faction = faction
self.vg = unitgroup.ShipGroup(self.game.next_group_id(),
self.go.group_name)
wp = self.vg.add_waypoint(self.position, 0)
wp.ETA_locked = True
def add_unit(self, unit_type, name, pos_x, pos_y, heading) -> Ship:
unit = Ship(self.game.next_unit_id(),
f"{self.go.group_name}|{name}", unit_type)
unit.position.x = pos_x
unit.position.y = pos_y
unit.heading = heading
self.vg.add_unit(unit)
return unit

View File

@@ -1,31 +1,15 @@
import random
from typing import List, Optional, Type
from typing import List, Type
from dcs.unittype import UnitType
from dcs.vehicles import AirDefence
from dcs.unitgroup import VehicleGroup
from game import Game, db
from game import db
from gen.sam.aaa_bofors import BoforsGenerator
from gen.sam.aaa_flak import FlakGenerator
from gen.sam.aaa_flak18 import Flak18Generator
from gen.sam.aaa_ww2_ally_flak import AllyWW2FlakGenerator
from gen.sam.aaa_zu23_insurgent import ZU23InsurgentGenerator
from gen.sam.cold_war_flak import EarlyColdWarFlakGenerator, ColdWarFlakGenerator
from gen.sam.ewrs import (
BigBirdGenerator,
BoxSpringGenerator,
DogEarGenerator,
FlatFaceGenerator,
HawkEwrGenerator,
PatriotEwrGenerator,
RolandEwrGenerator,
SnowDriftGenerator,
StraightFlushGenerator,
TallRackGenerator,
)
from gen.sam.group_generator import GroupGenerator
from gen.sam.genericsam_group_generator import GenericSamGroupGenerator
from gen.sam.sam_avenger import AvengerGenerator
from gen.sam.sam_chaparral import ChaparralGenerator
from gen.sam.sam_gepard import GepardGenerator
@@ -50,9 +34,6 @@ from gen.sam.sam_zsu23 import ZSU23Generator
from gen.sam.sam_zu23 import ZU23Generator
from gen.sam.sam_zu23_ural import ZU23UralGenerator
from gen.sam.sam_zu23_ural_insurgent import ZU23UralInsurgentGenerator
from gen.sam.freya_ewr import FreyaGenerator
from theater import TheaterGroundObject
from theater.theatergroundobject import SamGroundObject
SAM_MAP = {
"HawkGenerator": HawkGenerator,
@@ -81,12 +62,7 @@ SAM_MAP = {
"SA13Generator": SA13Generator,
"SA15Generator": SA15Generator,
"SA19Generator": SA19Generator,
"HQ7Generator": HQ7Generator,
"Flak18Generator": Flak18Generator,
"ColdWarFlakGenerator": ColdWarFlakGenerator,
"EarlyColdWarFlakGenerator": EarlyColdWarFlakGenerator,
"FreyaGenerator": FreyaGenerator,
"AllyWW2FlakGenerator": AllyWW2FlakGenerator
"HQ7Generator": HQ7Generator
}
SAM_PRICES = {
@@ -123,74 +99,32 @@ SAM_PRICES = {
AirDefence.HQ_7_Self_Propelled_LN: 35
}
EWR_MAP = {
"BoxSpringGenerator": BoxSpringGenerator,
"TallRackGenerator": TallRackGenerator,
"DogEarGenerator": DogEarGenerator,
"RolandEwrGenerator": RolandEwrGenerator,
"FlatFaceGenerator": FlatFaceGenerator,
"PatriotEwrGenerator": PatriotEwrGenerator,
"BigBirdGenerator": BigBirdGenerator,
"SnowDriftGenerator": SnowDriftGenerator,
"StraightFlushGenerator": StraightFlushGenerator,
"HawkEwrGenerator": HawkEwrGenerator,
}
def get_faction_possible_sams_generator(faction: str) -> List[Type[GroupGenerator]]:
def get_faction_possible_sams_generator(faction: str) -> List[Type[GenericSamGroupGenerator]]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for
"""
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP]
return [SAM_MAP[s] for s in db.FACTIONS[faction].sams if s in SAM_MAP.keys()]
def get_faction_possible_ewrs_generator(faction: str) -> List[Type[GroupGenerator]]:
"""
Return the list of possible SAM generator for the given faction
:param faction: Faction name to search units for
"""
return [EWR_MAP[s] for s in db.FACTIONS[faction].ewrs if s in EWR_MAP]
def generate_anti_air_group(game: Game, ground_object: TheaterGroundObject,
faction: str) -> Optional[VehicleGroup]:
def generate_anti_air_group(game, parent_cp, ground_object, faction:str):
"""
This generate a SAM group
:param game: The Game.
:param ground_object: The ground object which will own the sam group.
:param faction: Owner faction.
:return: The generated group, or None if one could not be generated.
:param parentCp: The parent control point
:param ground_object: The ground object which will own the sam group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
"""
possible_sams_generators = get_faction_possible_sams_generator(faction)
if len(possible_sams_generators) > 0:
sam_generator_class = random.choice(possible_sams_generators)
generator = sam_generator_class(game, ground_object)
generator = sam_generator_class(game, ground_object, faction)
generator.generate()
return generator.get_generated_group()
return None
def generate_ewr_group(game: Game, ground_object: TheaterGroundObject,
faction: str) -> Optional[VehicleGroup]:
"""Generates an early warning radar group.
:param game: The Game.
:param ground_object: The ground object which will own the EWR group.
:param faction: Owner faction.
:return: The generated group, or None if one could not be generated.
"""
generators = get_faction_possible_ewrs_generator(faction)
if len(generators) > 0:
generator_class = random.choice(generators)
generator = generator_class(game, ground_object)
generator.generate()
return generator.get_generated_group()
return None
def generate_shorad_group(game: Game, ground_object: SamGroundObject,
faction_name: str) -> Optional[VehicleGroup]:
def generate_shorad_group(game, parent_cp, ground_object, faction_name: str):
faction = db.FACTIONS[faction_name]
if len(faction.shorads) > 0:
@@ -199,4 +133,9 @@ def generate_shorad_group(game: Game, ground_object: SamGroundObject,
generator.generate()
return generator.get_generated_group()
else:
return generate_anti_air_group(game, ground_object, faction_name)
return generate_anti_air_group(game, parent_cp, ground_object, faction_name)

View File

@@ -28,6 +28,6 @@ class PatriotGenerator(GenericSamGroupGenerator):
# Short range protection for high value site
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
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

@@ -31,7 +31,7 @@ class SA10Generator(GenericSamGroupGenerator):
# 2 different launcher type (C & D)
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(num_launchers, launcher_distance=100, coverage=360)
positions = self.get_circular_position(num_launchers, launcher_distance=120, coverage=360)
for i, position in enumerate(positions):
if i%2 == 0:
self.add_unit(AirDefence.SAM_SA_10_S_300PS_LN_5P85C, "LN#" + str(i), position[0], position[1], position[2])
@@ -41,12 +41,12 @@ class SA10Generator(GenericSamGroupGenerator):
# Then let's add short range protection to this high value site
# Sa-13 Strela are great for that
num_launchers = random.randint(2, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=140, coverage=360)
positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SAM_SA_13_Strela_10M3_9A35M3, "IR#" + str(i), position[0], position[1], position[2])
# And even some AA
num_launchers = random.randint(6, 8)
positions = self.get_circular_position(num_launchers, launcher_distance=210, coverage=360)
positions = self.get_circular_position(num_launchers, launcher_distance=350, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.SPAAA_ZSU_23_4_Shilka, "AA#" + str(i), position[0], position[1], position[2])

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
from dcs.action import MarkToAll
from dcs.condition import TimeAfter
from dcs.mission import Mission
@@ -7,7 +5,7 @@ from dcs.task import Option
from dcs.translation import String
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Skill
from dcs.unitgroup import FlyingGroup
from .conflictgen import Conflict
PUSH_TRIGGER_SIZE = 3000
@@ -75,9 +73,8 @@ class TriggersGenerator:
continue
for country in coalition.countries.values():
flying_groups = country.plane_group + country.helicopter_group # type: FlyingGroup
for flying_group in flying_groups:
for plane_unit in flying_group.units:
for plane_group in country.plane_group:
for plane_unit in plane_group.units:
if plane_unit.skill != Skill.Client and plane_unit.skill != Skill.Player:
plane_unit.skill = Skill(skill_level[0])

2
plugin/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from .luaplugin import LuaPlugin
from .manager import LuaPluginManager

208
plugin/luaplugin.py Normal file
View File

@@ -0,0 +1,208 @@
import json
from pathlib import Path
from typing import List, Optional
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QCheckBox, QGridLayout, QGroupBox, QLabel
class LuaPluginWorkOrder:
def __init__(self, parent, filename: str, mnemonic: str,
disable: bool) -> None:
self.filename = filename
self.mnemonic = mnemonic
self.disable = disable
self.parent = parent
def work(self, operation):
if self.disable:
operation.bypass_plugin_script(self.mnemonic)
else:
operation.inject_plugin_script(self.parent.mnemonic, self.filename,
self.mnemonic)
class LuaPluginSpecificOption:
def __init__(self, parent, mnemonic: str, nameInUI: str,
defaultValue: bool) -> None:
self.mnemonic = mnemonic
self.nameInUI = nameInUI
self.defaultValue = defaultValue
self.parent = parent
class LuaPlugin:
NAME_IN_SETTINGS_BASE:str = "plugins."
def __init__(self, jsonFilename: str) -> None:
self.mnemonic: Optional[str] = None
self.skipUI: bool = False
self.nameInUI: Optional[str] = None
self.nameInSettings: Optional[str] = None
self.defaultValue: bool = False
self.specificOptions: List[LuaPluginSpecificOption] = []
self.scriptsWorkOrders: List[LuaPluginWorkOrder] = []
self.configurationWorkOrders: List[LuaPluginWorkOrder] = []
self.initFromJson(jsonFilename)
self.enabled = self.defaultValue
self.settings = None
def initFromJson(self, jsonFilename:str):
jsonFile:Path = Path(jsonFilename)
if jsonFile.exists():
jsonData = json.loads(jsonFile.read_text())
self.mnemonic = jsonData.get("mnemonic")
self.skipUI = jsonData.get("skipUI", False)
self.nameInUI = jsonData.get("nameInUI")
assert self.mnemonic is not None
self.nameInSettings = LuaPlugin.NAME_IN_SETTINGS_BASE + self.mnemonic
self.defaultValue = jsonData.get("defaultValue", False)
self.specificOptions = []
for jsonSpecificOption in jsonData.get("specificOptions"):
mnemonic = jsonSpecificOption.get("mnemonic")
nameInUI = jsonSpecificOption.get("nameInUI", mnemonic)
defaultValue = jsonSpecificOption.get("defaultValue")
self.specificOptions.append(LuaPluginSpecificOption(self, mnemonic, nameInUI, defaultValue))
self.scriptsWorkOrders = []
for jsonWorkOrder in jsonData.get("scriptsWorkOrders"):
file = jsonWorkOrder.get("file")
mnemonic = jsonWorkOrder.get("mnemonic")
disable = jsonWorkOrder.get("disable", False)
self.scriptsWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable))
self.configurationWorkOrders = []
for jsonWorkOrder in jsonData.get("configurationWorkOrders"):
file = jsonWorkOrder.get("file")
mnemonic = jsonWorkOrder.get("mnemonic")
disable = jsonWorkOrder.get("disable", False)
self.configurationWorkOrders.append(LuaPluginWorkOrder(self, file, mnemonic, disable))
def setupUI(self, settingsWindow, row:int):
# set the game settings
self.setSettings(settingsWindow.game.settings)
if not self.skipUI:
assert self.nameInSettings is not None
assert self.settings is not None
# create the plugin choice checkbox interface
self.uiWidget: QCheckBox = QCheckBox()
self.uiWidget.setChecked(self.isEnabled())
self.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow))
settingsWindow.pluginsGroupLayout.addWidget(QLabel(self.nameInUI), row, 0)
settingsWindow.pluginsGroupLayout.addWidget(self.uiWidget, row, 1, Qt.AlignRight)
# if needed, create the plugin options special page
if settingsWindow.pluginsOptionsPageLayout and self.specificOptions != None:
self.optionsGroup: QGroupBox = QGroupBox(self.nameInUI)
optionsGroupLayout = QGridLayout();
optionsGroupLayout.setAlignment(Qt.AlignTop)
self.optionsGroup.setLayout(optionsGroupLayout)
settingsWindow.pluginsOptionsPageLayout.addWidget(self.optionsGroup)
# browse each option in the specific options list
row = 0
for specificOption in self.specificOptions:
assert specificOption.mnemonic is not None
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
if not nameInSettings in self.settings.plugins:
self.settings.plugins[nameInSettings] = specificOption.defaultValue
specificOption.uiWidget = QCheckBox()
specificOption.uiWidget.setChecked(self.settings.plugins[nameInSettings])
#specificOption.uiWidget.setEnabled(False)
specificOption.uiWidget.toggled.connect(lambda: self.applySetting(settingsWindow))
optionsGroupLayout.addWidget(QLabel(specificOption.nameInUI), row, 0)
optionsGroupLayout.addWidget(specificOption.uiWidget, row, 1, Qt.AlignRight)
row += 1
# disable or enable the UI in the plugins special page
self.enableOptionsGroup()
def enableOptionsGroup(self):
if self.optionsGroup:
self.optionsGroup.setEnabled(self.isEnabled())
def setSettings(self, settings):
self.settings = settings
# ensure the setting exist
if not self.nameInSettings in self.settings.plugins:
self.settings.plugins[self.nameInSettings] = self.defaultValue
# do the same for each option in the specific options list
for specificOption in self.specificOptions:
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
if not nameInSettings in self.settings.plugins:
self.settings.plugins[nameInSettings] = specificOption.defaultValue
def applySetting(self, settingsWindow):
# apply the main setting
self.settings.plugins[self.nameInSettings] = self.uiWidget.isChecked()
self.enabled = self.settings.plugins[self.nameInSettings]
# do the same for each option in the specific options list
for specificOption in self.specificOptions:
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
self.settings.plugins[nameInSettings] = specificOption.uiWidget.isChecked()
# disable or enable the UI in the plugins special page
self.enableOptionsGroup()
def injectScripts(self, operation):
# set the game settings
self.setSettings(operation.game.settings)
# execute the work order
if self.scriptsWorkOrders != None:
for workOrder in self.scriptsWorkOrders:
workOrder.work(operation)
# serves for subclasses
return self.isEnabled()
def injectConfiguration(self, operation):
# set the game settings
self.setSettings(operation.game.settings)
# inject the plugin options
if len(self.specificOptions) > 0:
defineAllOptions = ""
for specificOption in self.specificOptions:
nameInSettings = self.nameInSettings + "." + specificOption.mnemonic
value = "true" if self.settings.plugins[nameInSettings] else "false"
defineAllOptions += f" dcsLiberation.plugins.{self.mnemonic}.{specificOption.mnemonic} = {value} \n"
lua = f"-- {self.mnemonic} plugin configuration.\n"
lua += "\n"
lua += "if dcsLiberation then\n"
lua += " if not dcsLiberation.plugins then \n"
lua += " dcsLiberation.plugins = {}\n"
lua += " end\n"
lua += f" dcsLiberation.plugins.{self.mnemonic} = {{}}\n"
lua += defineAllOptions
lua += "end"
operation.inject_lua_trigger(lua, f"{self.mnemonic} plugin configuration")
# execute the work order
if self.configurationWorkOrders != None:
for workOrder in self.configurationWorkOrders:
workOrder.work(operation)
# serves for subclasses
return self.isEnabled()
def isEnabled(self) -> bool:
if not self.settings:
return False
self.setSettings(self.settings) # create the necessary settings keys if needed
return self.settings != None and self.settings.plugins[self.nameInSettings]
def hasUI(self) -> bool:
return not self.skipUI

43
plugin/manager.py Normal file
View File

@@ -0,0 +1,43 @@
from .luaplugin import LuaPlugin
from typing import List
import glob
from pathlib import Path
import json
import logging
class LuaPluginManager():
PLUGINS_RESOURCE_PATH = Path("resources/plugins")
PLUGINS_LIST_FILENAME = "plugins.json"
PLUGINS_JSON_FILENAME = "plugin.json"
__plugins = None
def __init__(self):
if not LuaPluginManager.__plugins:
LuaPluginManager.__plugins= []
jsonFile:Path = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, LuaPluginManager.PLUGINS_LIST_FILENAME)
if jsonFile.exists():
logging.info(f"Reading plugins list from {jsonFile}")
jsonData = json.loads(jsonFile.read_text())
for plugin in jsonData:
jsonPluginFolder = Path(LuaPluginManager.PLUGINS_RESOURCE_PATH, plugin)
jsonPluginFile = Path(jsonPluginFolder, LuaPluginManager.PLUGINS_JSON_FILENAME)
if jsonPluginFile.exists():
logging.info(f"Reading plugin {plugin} from {jsonPluginFile}")
plugin = LuaPlugin(jsonPluginFile)
LuaPluginManager.__plugins.append(plugin)
else:
logging.error(f"Missing configuration file {jsonPluginFile} for plugin {plugin}")
else:
logging.error(f"Missing plugins list file {jsonFile}")
def getPlugins(self):
return LuaPluginManager.__plugins
def getPlugin(self, pluginName):
for plugin in LuaPluginManager.__plugins:
if plugin.mnemonic == pluginName:
return plugin
return None

2
pydcs

Submodule pydcs updated: 2883be31c2...c12733a471

View File

@@ -1,169 +0,0 @@
from dcs import unittype
class SAM_SA_20_S_300PMU1_TR_30N6E(unittype.VehicleType):
id = "S-300PMU1 40B6M tr"
name = "SAM SA-20 S-300PMU1 TR 30N6E"
detection_range = 160000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_20_S_300PMU1_TR_30N6E_truck(unittype.VehicleType):
id = "S-300PMU1 30N6E tr"
name = "SAM SA-20 S-300PMU1 TR 30N6E(truck)"
detection_range = 160000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_20_S_300PMU1_SR_5N66E(unittype.VehicleType):
id = "S-300PMU1 40B6MD sr"
name = "SAM SA-20 S-300PMU1 SR 5N66E"
detection_range = 120000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_20_S_300PMU1_SR_64N6E(unittype.VehicleType):
id = "S-300PMU1 64N6E sr"
name = "SAM SA-20 S-300PMU1 SR 64N6E"
detection_range = 300000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_23_S_300VM_9S15M2_SR(unittype.VehicleType):
id = "S-300VM 9S15M2 sr"
name = "SAM SA-23 S-300VM 9S15M2 SR"
detection_range = 320000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_23_S_300VM_9S19M2_SR(unittype.VehicleType):
id = "S-300VM 9S19M2 sr"
name = "SAM SA-23 S-300VM 9S19M2 SR"
detection_range = 310000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_23_S_300VM_9S32ME_TR(unittype.VehicleType):
id = "S-300VM 9S32ME tr"
name = "SAM SA-23 S-300VM 9S32ME TR"
detection_range = 230000
threat_range = 0
air_weapon_dist = 0
class SAM_SA_20_S_300PMU1_LN_5P85CE(unittype.VehicleType):
id = "S-300PMU1 5P85CE ln"
name = "SAM SA-20 S-300PMU1 LN 5P85CE"
detection_range = 0
threat_range = 150000
air_weapon_dist = 150000
class SAM_SA_20_S_300PMU1_LN_5P85DE(unittype.VehicleType):
id = "S-300PMU1 5P85DE ln"
name = "SAM SA-20 S-300PMU1 LN 5P85DE"
detection_range = 0
threat_range = 150000
air_weapon_dist = 150000
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE(unittype.VehicleType):
id = "S-300PS 5P85CE ln"
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85CE"
detection_range = 0
threat_range = 90000
air_weapon_dist = 90000
class SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE(unittype.VehicleType):
id = "S-300PS 5P85DE ln"
name = "SAM SA-10 (5V55RUD) S-300PS LN 5P85DE"
detection_range = 0
threat_range = 90000
air_weapon_dist = 90000
class SAM_SA_23_S_300VM_9A83ME_LN(unittype.VehicleType):
id = "S-300VM 9A83ME ln"
name = "SAM SA-23 S-300VM 9A83ME LN"
detection_range = 0
threat_range = 90000
air_weapon_dist = 90000
class SAM_SA_23_S_300VM_9A82ME_LN(unittype.VehicleType):
id = "S-300VM 9A82ME ln"
name = "SAM SA-23 S-300VM 9A82ME LN"
detection_range = 0
threat_range = 200000
air_weapon_dist = 200000
class SAM_SA_17_Buk_M1_2_LN_9A310M1_2(unittype.VehicleType):
id = "SA-17 Buk M1-2 LN 9A310M1-2"
name = "SAM SA-17 Buk M1-2 LN 9A310M1-2"
detection_range = 120000
threat_range = 50000
air_weapon_dist = 50000
class SAM_SA_2__V759__LN_SM_90(unittype.VehicleType):
id = "S_75M_Volhov_V759"
name = "SAM SA-2 (V759) LN SM-90"
detection_range = 0
threat_range = 50000
air_weapon_dist = 50000
class SAM_HQ_2_LN_SM_90(unittype.VehicleType):
id = "HQ_2_Guideline_LN"
name = "SAM HQ-2 LN SM-90"
detection_range = 0
threat_range = 50000
air_weapon_dist = 50000
class SAM_SA_3__V_601P__LN_5P73(unittype.VehicleType):
id = "5p73 V-601P ln"
name = "SAM SA-3 (V-601P) LN 5P73"
detection_range = 0
threat_range = 18000
air_weapon_dist = 18000
class SAM_SA_20_S_300PMU1_CP_54K6(unittype.VehicleType):
id = "S-300PMU1 54K6 cp"
name = "SAM SA-20 S-300PMU1 CP 54K6"
detection_range = 0
threat_range = 0
air_weapon_dist = 0
class SAM_SA_23_S_300VM_9S457ME_CP(unittype.VehicleType):
id = "S-300VM 9S457ME cp"
name = "SAM SA-23 S-300VM 9S457ME CP"
detection_range = 0
threat_range = 0
air_weapon_dist = 0
class SAM_SA_24_Igla_S_manpad(unittype.VehicleType):
id = "SA-24 Igla-S manpad"
name = "SAM SA-24 Igla-S manpad"
detection_range = 5000
threat_range = 6000
air_weapon_dist = 6000
class SAM_SA_14_Strela_3_manpad(unittype.VehicleType):
id = "SA-14 Strela-3 manpad"
name = "SAM SA-14 Strela-3 manpad"
detection_range = 5000
threat_range = 4500
air_weapon_dist = 4500

View File

@@ -1,5 +1,4 @@
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.highdigitsams import highdigitsams
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_M, Rafale_A_S
from pydcs_extensions.su57.su57 import Su_57
@@ -40,26 +39,5 @@ MODDED_VEHICLES = [
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E,
highdigitsams.SAM_SA_20_S_300PMU1_TR_30N6E_truck,
highdigitsams.SAM_SA_20_S_300PMU1_SR_5N66E,
highdigitsams.SAM_SA_20_S_300PMU1_SR_64N6E,
highdigitsams.SAM_SA_23_S_300VM_9S15M2_SR,
highdigitsams.SAM_SA_23_S_300VM_9S19M2_SR,
highdigitsams.SAM_SA_23_S_300VM_9S32ME_TR,
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85CE,
highdigitsams.SAM_SA_20_S_300PMU1_LN_5P85DE,
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85CE,
highdigitsams.SAM_SA_10__5V55RUD__S_300PS_LN_5P85DE,
highdigitsams.SAM_SA_23_S_300VM_9A83ME_LN,
highdigitsams.SAM_SA_23_S_300VM_9A82ME_LN,
highdigitsams.SAM_SA_17_Buk_M1_2_LN_9A310M1_2,
highdigitsams.SAM_SA_2__V759__LN_SM_90,
highdigitsams.SAM_HQ_2_LN_SM_90,
highdigitsams.SAM_SA_3__V_601P__LN_5P73,
highdigitsams.SAM_SA_20_S_300PMU1_CP_54K6,
highdigitsams.SAM_SA_23_S_300VM_9S457ME_CP,
highdigitsams.SAM_SA_24_Igla_S_manpad,
highdigitsams.SAM_SA_14_Strela_3_manpad
frenchpack.DIM__KAMIKAZE
]

View File

@@ -34,13 +34,12 @@ class Dialog:
cls.game_model = game_model
@classmethod
def open_new_package_dialog(cls, mission_target: MissionTarget, parent=None):
def open_new_package_dialog(cls, mission_target: MissionTarget):
"""Opens the dialog to create a new package with the given target."""
cls.new_package_dialog = QNewPackageDialog(
cls.game_model,
cls.game_model.ato_model,
mission_target,
parent=parent
mission_target
)
cls.new_package_dialog.show()
@@ -56,12 +55,11 @@ class Dialog:
@classmethod
def open_edit_flight_dialog(cls, package_model: PackageModel,
flight: Flight, parent=None) -> None:
flight: Flight) -> None:
"""Opens the dialog to edit the given flight."""
cls.edit_flight_dialog = QEditFlightDialog(
cls.game_model,
package_model.package,
flight,
parent=parent
flight
)
cls.edit_flight_dialog.show()

View File

@@ -7,7 +7,7 @@ from PySide2 import QtWidgets
from PySide2.QtGui import QPixmap
from PySide2.QtWidgets import QApplication, QSplashScreen
from game import db, persistency, VERSION
from game import persistency
from qt_ui import (
liberation_install,
liberation_theme,
@@ -20,11 +20,9 @@ from qt_ui.windows.preferences.QLiberationFirstStartWindow import \
QLiberationFirstStartWindow
# Logging setup
logging_config.init_logging(VERSION)
logging_config.init_logging(uiconstants.VERSION_STRING)
if __name__ == "__main__":
# Load eagerly to catch errors early.
db.FACTIONS.initialize()
os.environ["QT_AUTO_SCREEN_SCALE_FACTOR"] = "1" # Potential fix for 4K screens
app = QApplication(sys.argv)

View File

@@ -125,8 +125,7 @@ class PackageModel(QAbstractListModel):
count = flight.count
name = db.unit_type_name(flight.unit_type)
estimator = TotEstimator(self.package)
delay = datetime.timedelta(
seconds=int(estimator.mission_start_time(flight).total_seconds()))
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
origin = flight.from_cp.name
return f"[{task}] {count} x {name} from {origin} in {delay}"
@@ -163,7 +162,7 @@ class PackageModel(QAbstractListModel):
"""Returns the flight located at the given index."""
return self.package.flights[index.row()]
def update_tot(self, tot: datetime.timedelta) -> None:
def update_tot(self, tot: int) -> None:
self.package.time_over_target = tot
self.layoutChanged.emit()
@@ -217,8 +216,6 @@ class AtoModel(QAbstractListModel):
self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount())
self.ato.add_package(package)
self.endInsertRows()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
def delete_package_at_index(self, index: QModelIndex) -> None:
"""Removes the package at the given index from the ATO."""
@@ -233,8 +230,6 @@ class AtoModel(QAbstractListModel):
for flight in package.flights:
self.game.aircraft_inventory.return_from_flight(flight)
self.endRemoveRows()
# noinspection PyUnresolvedReferences
self.client_slots_changed.emit()
def package_at_index(self, index: QModelIndex) -> Package:
"""Returns the package at the given index."""

View File

@@ -1,12 +1,12 @@
import os
from typing import Dict
from pathlib import Path
from PySide2.QtGui import QColor, QFont, QPixmap
from theater.theatergroundobject import CATEGORY_MAP
from .liberation_theme import get_theme_icons
VERSION_STRING = "2.2.0-preview"
URLS : Dict[str, str] = {
"Manual": "https://github.com/khopa/dcs_liberation/wiki",

View File

@@ -48,14 +48,12 @@ class QTopPanel(QFrame):
self.passTurnButton.setIcon(CONST.ICONS["PassTurn"])
self.passTurnButton.setProperty("style", "btn-primary")
self.passTurnButton.clicked.connect(self.passTurn)
if not self.game:
self.passTurnButton.setEnabled(False)
self.proceedButton = QPushButton("Take off")
self.proceedButton.setIcon(CONST.ICONS["Proceed"])
self.proceedButton.setProperty("style", "start-button")
self.proceedButton.clicked.connect(self.launch_mission)
if not self.game or self.game.turn == 0:
if self.game and self.game.turn == 0:
self.proceedButton.setEnabled(False)
self.factionsInfos = QFactionsInfos(self.game)
@@ -103,8 +101,6 @@ class QTopPanel(QFrame):
self.budgetBox.setGame(game)
self.factionsInfos.setGame(game)
self.passTurnButton.setEnabled(True)
if game and game.turn == 0:
self.proceedButton.setEnabled(False)
else:
@@ -130,7 +126,7 @@ class QTopPanel(QFrame):
continue
estimator = TotEstimator(package)
for flight in package.flights:
if estimator.mission_start_time(flight).total_seconds() < 0:
if estimator.mission_start_time(flight) < 0:
packages.append(package)
break
return packages

View File

@@ -1,4 +1,5 @@
"""Widgets for displaying air tasking orders."""
import datetime
import logging
from contextlib import contextmanager
from typing import ContextManager, Optional
@@ -21,7 +22,6 @@ from PySide2.QtWidgets import (
QAction,
QGroupBox,
QHBoxLayout,
QLabel,
QListView,
QMenu,
QPushButton,
@@ -64,7 +64,7 @@ class FlightDelegate(QStyledItemDelegate):
count = flight.count
name = db.unit_type_name(flight.unit_type)
estimator = TotEstimator(self.package)
delay = estimator.mission_start_time(flight)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
return f"[{task}] {count} x {name} in {delay}"
def second_row_text(self, index: QModelIndex) -> str:
@@ -194,8 +194,7 @@ class QFlightList(QListView):
def edit_flight(self, index: QModelIndex) -> None:
from qt_ui.dialogs import Dialog
Dialog.open_edit_flight_dialog(
self.package_model, self.package_model.flight_at_index(index),
parent=self.window()
self.package_model, self.package_model.flight_at_index(index)
)
def delete_flight(self, index: QModelIndex) -> None:
@@ -236,12 +235,6 @@ class QFlightPanel(QGroupBox):
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.tip = QLabel(
"To add flights to a package, edit the package by double clicking "
"it or pressing the edit button."
)
self.vbox.addWidget(self.tip)
self.flight_list = QFlightList(game_model, package_model)
self.vbox.addWidget(self.flight_list)
@@ -335,7 +328,10 @@ class PackageDelegate(QStyledItemDelegate):
def right_text(self, index: QModelIndex) -> str:
package = self.package(index)
return f"TOT T+{package.time_over_target}"
if package.time_over_target is None:
return ""
tot = datetime.timedelta(seconds=package.time_over_target)
return f"TOT T+{tot}"
def paint(self, painter: QPainter, option: QStyleOptionViewItem,
index: QModelIndex) -> None:
@@ -444,13 +440,6 @@ class QPackagePanel(QGroupBox):
self.vbox = QVBoxLayout()
self.setLayout(self.vbox)
self.tip = QLabel(
"To create a new package, right click the mission target on the "
"map. To target airbase objectives, use\n"
"the attack button in the airbase view."
)
self.vbox.addWidget(self.tip)
self.package_list = QPackageList(self.ato_model)
self.vbox.addWidget(self.package_list)

View File

@@ -95,7 +95,7 @@ class QFlightTypeComboBox(QComboBox):
yield from self.ENEMY_AIRBASE_MISSIONS
elif isinstance(self.target, TheaterGroundObject):
# TODO: Filter more based on the category.
friendly = self.target.control_point.captured
friendly = self.target.parent_control_point(self.theater).captured
if friendly:
yield from self.FRIENDLY_GROUND_OBJECT_MISSIONS
else:

View File

@@ -1,10 +1,9 @@
"""Combo box for selecting a departure airfield."""
from typing import Iterable
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QComboBox
from dcs.planes import PlaneType
from dcs.planes import PlaneType
from game.inventory import GlobalAircraftInventory
from theater.controlpoint import ControlPoint
@@ -16,8 +15,6 @@ class QOriginAirfieldSelector(QComboBox):
that have unassigned inventory of the given aircraft type.
"""
availability_changed = Signal(int)
def __init__(self, global_inventory: GlobalAircraftInventory,
origins: Iterable[ControlPoint],
aircraft: PlaneType) -> None:
@@ -26,7 +23,6 @@ class QOriginAirfieldSelector(QComboBox):
self.origins = list(origins)
self.aircraft = aircraft
self.rebuild_selector()
self.currentIndexChanged.connect(self.index_changed)
def change_aircraft(self, aircraft: PlaneType) -> None:
if self.aircraft == aircraft:
@@ -47,14 +43,5 @@ class QOriginAirfieldSelector(QComboBox):
@property
def available(self) -> int:
origin = self.currentData()
if origin is None:
return 0
inventory = self.global_inventory.for_control_point(origin)
return inventory.available(self.aircraft)
def index_changed(self, index: int) -> None:
origin = self.itemData(index)
if origin is None:
return
inventory = self.global_inventory.for_control_point(origin)
self.availability_changed.emit(inventory.available(self.aircraft))

View File

@@ -1,8 +1,9 @@
from PySide2.QtCore import QSortFilterProxyModel, Qt, QModelIndex
from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QComboBox, QCompleter
from game import Game
from gen import BuildingGroundObject, Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint
from gen import Conflict, FlightWaypointType
from gen.flights.flight import FlightWaypoint, PredefinedWaypointCategory
from qt_ui.widgets.combos.QFilteredComboBox import QFilteredComboBox
from theater import ControlPointType
@@ -44,6 +45,7 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
i = 0
def add_model_item(i, model, name, wpt):
print(name)
item = QStandardItem(name)
model.setItem(i, 0, item)
self.wpts.append(wpt)
@@ -64,13 +66,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.alt_type = "RADIO"
wpt.pretty_name = wpt.name
wpt.description = "Frontline"
wpt.data = [cp, ecp]
wpt.category = PredefinedWaypointCategory.FRONTLINE
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_targets:
for cp in self.game.theater.controlpoints:
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 isinstance(ground_object, BuildingGroundObject):
if not ground_object.is_dead and not ground_object.dcs_identifier == "AA":
wpt = FlightWaypoint(
FlightWaypointType.CUSTOM,
ground_object.position.x,
@@ -78,14 +82,17 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
0
)
wpt.alt_type = "RADIO"
wpt.name = ground_object.waypoint_name
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
if cp.captured:
wpt.description = "Friendly Building"
wpt.category = PredefinedWaypointCategory.ALLY_BUILDING
else:
wpt.description = "Enemy Building"
wpt.category = PredefinedWaypointCategory.ENEMY_BUILDING
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_units:
@@ -105,12 +112,15 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
wpt.name = wpt.name = "[" + str(ground_object.obj_name) + "] : " + u.type + " #" + str(j)
wpt.pretty_name = wpt.name
wpt.targets.append(u)
wpt.data = u
wpt.obj_name = ground_object.obj_name
wpt.waypoint_type = FlightWaypointType.CUSTOM
if cp.captured:
wpt.description = "Friendly unit : " + u.type
wpt.category = PredefinedWaypointCategory.ALLY_UNIT
else:
wpt.description = "Enemy unit : " + u.type
wpt.category = PredefinedWaypointCategory.ENEMY_UNIT
i = add_model_item(i, model, wpt.pretty_name, wpt)
if self.include_airbases:
@@ -124,10 +134,13 @@ class QPredefinedWaypointSelectionComboBox(QFilteredComboBox):
)
wpt.alt_type = "RADIO"
wpt.name = cp.name
wpt.data = cp
if cp.captured:
wpt.description = "Position of " + cp.name + " [Friendly Airbase]"
wpt.category = PredefinedWaypointCategory.ALLY_CP
else:
wpt.description = "Position of " + cp.name + " [Enemy Airbase]"
wpt.category = PredefinedWaypointCategory.ENEMY_CP
if cp.cptype == ControlPointType.AIRCRAFT_CARRIER_GROUP:
wpt.pretty_name = cp.name + " (Aircraft Carrier Group)"

View File

@@ -26,11 +26,13 @@ from dcs.mapping import point_from_heading
import qt_ui.uiconstants as CONST
from game import Game, db
from game.data.aaa_db import AAA_UNITS
from game.data.radar_db import UNITS_WITH_RADAR
from game.utils import meter_to_feet
from game.weather import TimeOfDay
from gen import Conflict
from gen import Conflict, PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint, FlightWaypointType
from gen.flights.flightplan import FlightPlan
from qt_ui.displayoptions import DisplayOptions
from qt_ui.models import GameModel
from qt_ui.widgets.map.QFrontLine import QFrontLine
@@ -39,11 +41,6 @@ from qt_ui.widgets.map.QMapControlPoint import QMapControlPoint
from qt_ui.widgets.map.QMapGroundObject import QMapGroundObject
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from theater import ControlPoint, FrontLine
from theater.theatergroundobject import (
EwrGroundObject,
MissileSiteGroundObject,
TheaterGroundObject,
)
class QLiberationMap(QGraphicsView):
@@ -124,8 +121,8 @@ class QLiberationMap(QGraphicsView):
def setGame(self, game: Optional[Game]):
self.game = game
logging.debug("Reloading Map Canvas")
if self.game is not None:
logging.debug("Reloading Map Canvas")
self.reload_scene()
"""
@@ -166,28 +163,6 @@ class QLiberationMap(QGraphicsView):
self.reload_scene()
"""
@staticmethod
def aa_ranges(ground_object: TheaterGroundObject) -> Tuple[int, int]:
detection_range = 0
threat_range = 0
for g in ground_object.groups:
for u in g.units:
unit = db.unit_type_from_name(u.type)
if unit is None:
logging.error(f"Unknown unit type {u.type}")
continue
# Some units in pydcs have detection_range and threat_range
# defined, but explicitly set to None.
unit_detection_range = getattr(unit, "detection_range", None)
if unit_detection_range is not None:
detection_range = max(detection_range, unit_detection_range)
unit_threat_range = getattr(unit, "threat_range", None)
if unit_threat_range is not None:
threat_range = max(threat_range, unit_threat_range)
return detection_range, threat_range
def reload_scene(self):
scene = self.scene()
@@ -240,34 +215,41 @@ class QLiberationMap(QGraphicsView):
buildings = self.game.theater.find_ground_objects_by_obj_name(ground_object.obj_name)
scene.addItem(QMapGroundObject(self, go_pos[0], go_pos[1], 14, 12, cp, ground_object, self.game, buildings))
is_missile = isinstance(ground_object, MissileSiteGroundObject)
is_aa = ground_object.category == "aa" and not is_missile
is_ewr = isinstance(ground_object, EwrGroundObject)
is_display_type = is_aa or is_ewr
is_aa = ground_object.category == "aa"
should_display = ((DisplayOptions.sam_ranges and cp.captured)
or
(DisplayOptions.enemy_sam_ranges and not cp.captured))
if is_display_type and should_display:
detection_range, threat_range = self.aa_ranges(
ground_object
)
if threat_range:
if is_aa and should_display:
threat_range = 0
detection_range = 0
can_fire = False
if ground_object.groups:
for g in ground_object.groups:
for u in g.units:
unit = db.unit_type_from_name(u.type)
if unit in UNITS_WITH_RADAR or unit in AAA_UNITS:
can_fire = True
if unit.detection_range > detection_range:
detection_range = unit.detection_range
if unit.threat_range > threat_range:
threat_range = unit.threat_range
if can_fire:
threat_pos = self._transform_point(Point(ground_object.position.x+threat_range,
ground_object.position.y+threat_range))
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
ground_object.position.y+detection_range))
threat_radius = Point(*go_pos).distance_to_point(Point(*threat_pos))
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
# Add detection range circle
if DisplayOptions.detection_range:
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
detection_radius, detection_radius, self.detection_pen(cp.captured))
# Add threat range circle
scene.addEllipse(go_pos[0] - threat_radius / 2 + 7, go_pos[1] - threat_radius / 2 + 6,
threat_radius, threat_radius, self.threat_pen(cp.captured))
if detection_range:
# Add detection range circle
detection_pos = self._transform_point(Point(ground_object.position.x+detection_range,
ground_object.position.y+detection_range))
detection_radius = Point(*go_pos).distance_to_point(Point(*detection_pos))
if DisplayOptions.detection_range:
scene.addEllipse(go_pos[0] - detection_radius/2 + 7, go_pos[1] - detection_radius/2 + 6,
detection_radius, detection_radius, self.detection_pen(cp.captured))
added_objects.append(ground_object.obj_name)
for cp in self.game.theater.enemy_points():
@@ -312,10 +294,11 @@ class QLiberationMap(QGraphicsView):
selected = (p_idx, f_idx) == self.selected_flight
if DisplayOptions.flight_paths.only_selected and not selected:
continue
self.draw_flight_plan(scene, flight, selected)
self.draw_flight_plan(scene, package_model.package, flight,
selected)
def draw_flight_plan(self, scene: QGraphicsScene, flight: Flight,
selected: bool) -> None:
def draw_flight_plan(self, scene: QGraphicsScene, package: Package,
flight: Flight, selected: bool) -> None:
is_player = flight.from_cp.captured
pos = self._transform_point(flight.from_cp.position)
@@ -327,7 +310,7 @@ class QLiberationMap(QGraphicsView):
FlightWaypointType.TARGET_POINT,
FlightWaypointType.TARGET_SHIP,
)
for idx, point in enumerate(flight.flight_plan.waypoints[1:]):
for idx, point in enumerate(flight.points):
new_pos = self._transform_point(Point(point.x, point.y))
self.draw_flight_path(scene, prev_pos, new_pos, is_player,
selected)
@@ -338,8 +321,8 @@ class QLiberationMap(QGraphicsView):
# Don't draw dozens of targets over each other.
continue
drew_target = True
self.draw_waypoint_info(scene, idx + 1, point, new_pos,
flight.flight_plan)
self.draw_waypoint_info(scene, idx + 1, point, new_pos, package,
flight)
prev_pos = tuple(new_pos)
self.draw_flight_path(scene, prev_pos, pos, is_player, selected)
@@ -354,21 +337,21 @@ class QLiberationMap(QGraphicsView):
def draw_waypoint_info(self, scene: QGraphicsScene, number: int,
waypoint: FlightWaypoint, position: Tuple[int, int],
flight_plan: FlightPlan) -> None:
package: Package, flight: Flight) -> None:
timing = PackageWaypointTiming.for_package(package)
altitude = meter_to_feet(waypoint.alt)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
prefix = "TOT"
time = flight_plan.tot_for_waypoint(waypoint)
time = timing.tot_for_waypoint(flight, waypoint)
if time is None:
prefix = "Depart"
time = flight_plan.depart_time_for_waypoint(waypoint)
time = timing.depart_time_for_waypoint(waypoint, flight)
if time is None:
tot = ""
else:
time = datetime.timedelta(seconds=int(time.total_seconds()))
tot = f"{prefix} T+{time}"
tot = f"{prefix} T+{datetime.timedelta(seconds=time)}"
pen = QPen(QColor("black"), 0.3)
brush = QColor("white")

View File

@@ -90,10 +90,3 @@ class QMapControlPoint(QMapObject):
# Reinitialized ground planners and the like.
self.game_model.game.initialize_turn()
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
def open_new_package_dialog(self) -> None:
"""Extends the default packagedialog to redirect to base menu for red air base."""
if not self.control_point.captured:
self.on_click()
else:
super(QMapControlPoint, self).open_new_package_dialog()

View File

@@ -1,7 +1,7 @@
import logging
import traceback
import sys
import webbrowser
from typing import Optional
from typing import Optional, Union
from PySide2.QtCore import Qt
from PySide2.QtGui import QCloseEvent, QIcon
@@ -10,14 +10,14 @@ from PySide2.QtWidgets import (
QActionGroup, QDesktopWidget,
QFileDialog,
QMainWindow,
QMessageBox,
QMenu, QMessageBox,
QSplitter,
QVBoxLayout,
QWidget,
)
import qt_ui.uiconstants as CONST
from game import Game, VERSION, persistency
from game import Game, persistency
from qt_ui.dialogs import Dialog
from qt_ui.displayoptions import DisplayGroup, DisplayOptions, DisplayRule
from qt_ui.models import GameModel
@@ -41,12 +41,13 @@ class QLiberationWindow(QMainWindow):
self.game: Optional[Game] = None
self.game_model = GameModel()
Dialog.set_game(self.game_model)
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.info_panel = QInfoPanel(self.game)
self.liberation_map = QLiberationMap(self.game_model)
self.ato_panel = None
self.info_panel = None
self.liberation_map = None
self.setGame(persistency.restore_game())
self.setGeometry(300, 100, 270, 100)
self.setWindowTitle(f"DCS Liberation - v{VERSION}")
self.setWindowTitle("DCS Liberation - v" + CONST.VERSION_STRING)
self.setWindowIcon(QIcon("./resources/icon.png"))
self.statusBar().showMessage('Ready')
@@ -55,24 +56,24 @@ class QLiberationWindow(QMainWindow):
self.initMenuBar()
self.initToolbar()
self.connectSignals()
self.onGameGenerated(self.game)
screen = QDesktopWidget().screenGeometry()
self.setGeometry(0, 0, screen.width(), screen.height())
self.setWindowState(Qt.WindowMaximized)
self.onGameGenerated(persistency.restore_game())
def initUi(self):
self.ato_panel = QAirTaskingOrderPanel(self.game_model)
self.liberation_map = QLiberationMap(self.game_model)
self.info_panel = QInfoPanel(self.game)
hbox = QSplitter(Qt.Horizontal)
vbox = QSplitter(Qt.Vertical)
hbox.addWidget(self.ato_panel)
hbox.addWidget(vbox)
vbox.addWidget(self.liberation_map)
vbox.addWidget(self.info_panel)
# Will make the ATO sidebar as small as necessary to fit the content. In
# practice this means it is sized by the hints in the panel.
hbox.setSizes([1, 10000000])
hbox.setSizes([100, 600])
vbox.setSizes([600, 100])
vbox = QVBoxLayout()
@@ -190,7 +191,8 @@ class QLiberationWindow(QMainWindow):
filter="*.liberation")
if file is not None:
game = persistency.load_game(file[0])
GameUpdateSignal.get_instance().updateGame(game)
self.setGame(game)
GameUpdateSignal.get_instance().updateGame(self.game)
def saveGame(self):
logging.info("Saving game")
@@ -213,40 +215,26 @@ class QLiberationWindow(QMainWindow):
GameUpdateSignal.get_instance().updateGame(self.game)
def setGame(self, game: Optional[Game]):
try:
if game is not None:
game.on_load()
self.game = game
if self.info_panel is not None:
self.info_panel.setGame(game)
self.game_model.set(self.game)
if self.liberation_map is not None:
self.liberation_map.setGame(game)
except AttributeError:
logging.exception("Incompatible save game")
QMessageBox.critical(
self,
"Could not load save game",
"The save game you have loaded is incompatible with this "
"version of DCS Liberation.\n"
"\n"
f"{traceback.format_exc()}",
QMessageBox.Ok
)
GameUpdateSignal.get_instance().updateGame(None)
if game is not None:
game.on_load()
self.game = game
if self.info_panel is not None:
self.info_panel.setGame(game)
self.game_model.set(self.game)
if self.liberation_map is not None:
self.liberation_map.setGame(game)
def showAboutDialog(self):
text = "<h3>DCS Liberation " + VERSION + "</h3>" + \
text = "<h3>DCS Liberation " + CONST.VERSION_STRING + "</h3>" + \
"<b>Source code :</b> https://github.com/khopa/dcs_liberation" + \
"<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, ColonelPanic, Roach, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
"shdwp, Khopa, ColonelPanic, Wrycu, calvinmorrow, JohanAberg, Deus, root0fall, Captain Cody, steveveepee, pedromagueija, parithon, bwRavencl, davidp57" + \
"<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/>"\
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"\
"<b>Walder </b> <i>for the Skynet-IADS script</i><br/>"
"<b>Ciribob </b> <i>for the JTACAutoLase.lua script</i><br/>"
about = QMessageBox()
about.setWindowTitle("About DCS Liberation")
about.setIcon(QMessageBox.Icon.Information)

View File

@@ -13,9 +13,8 @@ from PySide2.QtWidgets import (
QLabel,
QMessageBox,
QPushButton,
QTextBrowser,
QTextEdit,
)
from jinja2 import Environment, FileSystemLoader, select_autoescape
from game.debriefing import Debriefing, wait_for_debriefing
from game.game import Event, Game, logging
@@ -66,21 +65,27 @@ class QWaitingForMissionResultWindow(QDialog):
self.layout.addWidget(header, 0, 0)
self.gridLayout = QGridLayout()
TEXT = "" + \
"<b>You are clear for takeoff</b>" + \
"" + \
"<h2>For Singleplayer :</h2>\n" + \
"In DCS, open the Mission Editor, and load the file : \n" + \
"<i>liberation_nextturn</i>\n" + \
"<p>Then once the mission is loaded in ME, in menu \"Flight\",\n" + \
"click on FLY Mission to launch.</p>\n" + \
"" + \
"<h2>For Multiplayer :</h2>" + \
"In DCS, open the Mission Editor, and load the file : " + \
"<i>liberation_nextturn</i>" + \
"<p>Click on File/Save. Then exit the mission editor, and go to Multiplayer.</p>" + \
"<p>Then host a server with the mission, and tell your friends to join !</p>" + \
"<i>(The step in the mission editor is important, and fix a game breaking bug.)</i>" + \
"<h2>Finishing</h2>" + \
"<p>Once you have played the mission, click on the \"Accept Results\" button.</p>" + \
"<p>If DCS Liberation does not detect mission end, use the manually submit button, and choose the state.json file.</p>"
jinja = Environment(
loader=FileSystemLoader("resources/ui/templates"),
autoescape=select_autoescape(
disabled_extensions=("",),
default_for_string=True,
default=True,
),
trim_blocks=True,
lstrip_blocks=True,
)
self.instructions_text = QTextBrowser()
self.instructions_text.setHtml(
jinja.get_template("mission_start_EN.j2").render())
self.instructions_text.setOpenExternalLinks(True)
self.instructions_text = QTextEdit(TEXT)
self.instructions_text.setReadOnly(True)
self.gridLayout.addWidget(self.instructions_text, 1, 0)
progress = QLabel("")

View File

@@ -16,11 +16,11 @@ class QBaseMenuTabs(QTabWidget):
if cp:
if not cp.captured:
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
if not cp.is_carrier:
self.base_defenses_hq = QBaseDefensesHQ(cp, game_model.game)
self.addTab(self.base_defenses_hq, "Base Defenses")
self.intel = QIntelInfo(cp, game_model.game)
self.addTab(self.intel, "Intel")
else:
if cp.has_runway():
self.airfield_command = QAirfieldCommand(cp, game_model)

View File

@@ -6,7 +6,6 @@ from PySide2.QtWidgets import (
QGridLayout,
QHBoxLayout,
QLabel,
QMessageBox,
QScrollArea,
QVBoxLayout,
QWidget,
@@ -89,21 +88,7 @@ class QAircraftRecruitmentMenu(QFrame, QRecruitBehaviour):
super().buy(unit_type)
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)
def sell(self, unit_type: UnitType):
# Don't need to remove aircraft from the inventory if we're canceling
# orders.
if self.deliveryEvent.units.get(unit_type, 0) <= 0:
global_inventory = self.game_model.game.aircraft_inventory
inventory = global_inventory.for_control_point(self.cp)
try:
inventory.remove_aircraft(unit_type, 1)
except ValueError:
QMessageBox.critical(
self, "Could not sell aircraft",
f"Attempted to sell one {unit_type.id} at {self.cp.name} "
"but none are available. Are all aircraft currently "
"assigned to a mission?", QMessageBox.Ok)
return
def sell(self, unit_type):
super().sell(unit_type)
self.hangar_status.update_label(self.total_units, self.cp.available_aircraft_slots)

View File

@@ -1,7 +1,6 @@
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QGridLayout, QLabel, QGroupBox, QPushButton, QVBoxLayout
from qt_ui.dialogs import Dialog
from qt_ui.uiconstants import VEHICLES_ICONS
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from theater import ControlPoint, TheaterGroundObject
@@ -24,20 +23,13 @@ class QBaseDefenseGroupInfo(QGroupBox):
def init_ui(self):
self.buildLayout()
self.main_layout.addLayout(self.unit_layout)
if not self.cp.captured and not self.ground_object.is_dead:
attack_button = QPushButton("Attack")
attack_button.setProperty("style", "btn-danger")
attack_button.setMaximumWidth(180)
attack_button.clicked.connect(self.onAttack)
self.main_layout.addWidget(attack_button, 0, Qt.AlignLeft)
manage_button = QPushButton("Manage")
manage_button.setProperty("style", "btn-success")
manage_button.setMaximumWidth(180)
manage_button.clicked.connect(self.onManage)
if self.cp.captured:
manage_button = QPushButton("Manage")
manage_button.setProperty("style", "btn-success")
manage_button.setMaximumWidth(180)
manage_button.clicked.connect(self.onManage)
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
self.main_layout.addLayout(self.unit_layout)
self.main_layout.addWidget(manage_button, 0, Qt.AlignLeft)
self.setLayout(self.main_layout)
@@ -74,9 +66,6 @@ class QBaseDefenseGroupInfo(QGroupBox):
self.setLayout(self.main_layout)
def onAttack(self):
Dialog.open_new_package_dialog(self.ground_object, parent=self.window())
def onManage(self):
self.edition_menu = QGroundObjectMenu(self.window(), self.ground_object, self.buildings, self.cp, self.game)

View File

@@ -14,8 +14,8 @@ class QGroundForcesStrategySelector(QComboBox):
self.cp.stances[enemy_cp.id] = CombatStance.DEFENSIVE
for i, stance in enumerate(CombatStance):
self.addItem(stance.name, userData=stance)
if self.cp.stances[enemy_cp.id] == stance:
self.addItem(stance.name, userData=stance.value)
if self.cp.stances[enemy_cp.id] == stance.value:
self.setCurrentIndex(i)
self.currentTextChanged.connect(self.on_change)

View File

@@ -15,8 +15,8 @@ from qt_ui.windows.mission.flight.QFlightPlanner import QFlightPlanner
class QEditFlightDialog(QDialog):
"""Dialog window for editing flight plans and loadouts."""
def __init__(self, game_model: GameModel, package: Package, flight: Flight, parent=None) -> None:
super().__init__(parent=parent)
def __init__(self, game_model: GameModel, package: Package, flight: Flight) -> None:
super().__init__()
self.game_model = game_model

View File

@@ -22,7 +22,7 @@ class QFlightItem(QStandardItem):
self.setIcon(icon)
self.setEditable(False)
estimator = TotEstimator(self.package)
delay = estimator.mission_start_time(flight)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
self.setText("["+str(self.flight.flight_type.name[:6])+"] "
+ str(self.flight.count) + " x " + db.unit_type_name(self.flight.unit_type)
+ " in " + str(delay))

View File

@@ -1,6 +1,5 @@
"""Dialogs for creating and editing ATO packages."""
import logging
from datetime import timedelta
from typing import Optional
from PySide2.QtCore import QItemSelection, QTime, Signal
@@ -36,8 +35,8 @@ class QPackageDialog(QDialog):
#: Emitted when a change is made to the package.
package_changed = Signal()
def __init__(self, game_model: GameModel, model: PackageModel, parent=None) -> None:
super().__init__(parent)
def __init__(self, game_model: GameModel, model: PackageModel) -> None:
super().__init__()
self.game_model = game_model
self.package_model = model
self.add_flight_dialog: Optional[QFlightCreator] = None
@@ -79,7 +78,7 @@ class QPackageDialog(QDialog):
self.tot_spinner.timeChanged.connect(self.save_tot)
self.tot_column.addWidget(self.tot_spinner)
self.reset_tot_button = QPushButton("ASAP")
self.reset_tot_button = QPushButton("Reset TOT")
self.reset_tot_button.setToolTip(
"Sets the package TOT to the earliest time that all flights can "
"arrive at the target."
@@ -119,7 +118,7 @@ class QPackageDialog(QDialog):
return self.game_model.game
def tot_qtime(self) -> QTime:
delay = int(self.package_model.package.time_over_target.total_seconds())
delay = self.package_model.package.time_over_target
hours = delay // 3600
minutes = delay // 60 % 60
seconds = delay % 60
@@ -138,11 +137,11 @@ class QPackageDialog(QDialog):
def save_tot(self) -> None:
time = self.tot_spinner.time()
seconds = time.hour() * 3600 + time.minute() * 60 + time.second()
self.package_model.update_tot(timedelta(seconds=seconds))
self.package_model.update_tot(seconds)
def reset_tot(self) -> None:
if not list(self.package_model.flights):
self.package_model.update_tot(timedelta())
self.package_model.update_tot(0)
else:
self.package_model.update_tot(
TotEstimator(self.package_model.package).earliest_tot())
@@ -156,8 +155,7 @@ class QPackageDialog(QDialog):
def on_add_flight(self) -> None:
"""Opens the new flight dialog."""
self.add_flight_dialog = QFlightCreator(self.game,
self.package_model.package,
parent=self.window())
self.package_model.package)
self.add_flight_dialog.created.connect(self.add_flight)
self.add_flight_dialog.show()
@@ -190,8 +188,8 @@ class QNewPackageDialog(QPackageDialog):
"""
def __init__(self, game_model: GameModel, model: AtoModel,
target: MissionTarget, parent=None) -> None:
super().__init__(game_model, PackageModel(Package(target)), parent=parent)
target: MissionTarget) -> None:
super().__init__(game_model, PackageModel(Package(target)))
self.ato_model = model
self.save_button = QPushButton("Save")

View File

@@ -3,7 +3,6 @@ from typing import Optional
from PySide2.QtCore import Qt, Signal
from PySide2.QtWidgets import (
QDialog,
QMessageBox,
QPushButton,
QVBoxLayout,
)
@@ -24,8 +23,8 @@ from theater import ControlPoint
class QFlightCreator(QDialog):
created = Signal(Flight)
def __init__(self, game: Game, package: Package, parent=None) -> None:
super().__init__(parent=parent)
def __init__(self, game: Game, package: Package) -> None:
super().__init__()
self.game = game
self.package = package
@@ -54,11 +53,11 @@ class QFlightCreator(QDialog):
[cp for cp in game.theater.controlpoints if cp.captured],
self.aircraft_selector.currentData()
)
self.airfield_selector.availability_changed.connect(self.update_max_size)
self.aircraft_selector.currentIndexChanged.connect(self.update_max_size)
layout.addLayout(QLabeledWidget("Airfield:", self.airfield_selector))
self.flight_size_spinner = QFlightSizeSpinner()
self.update_max_size(self.airfield_selector.available)
self.update_max_size()
layout.addLayout(QLabeledWidget("Size:", self.flight_size_spinner))
self.client_slots_spinner = QFlightSizeSpinner(
@@ -91,15 +90,12 @@ class QFlightCreator(QDialog):
return f"{origin.name} has no {aircraft.id} available."
if size > available:
return f"{origin.name} has only {available} {aircraft.id} available."
if size <= 0:
return f"Flight must have at least one aircraft."
return None
def create_flight(self) -> None:
error = self.verify_form()
if error is not None:
QMessageBox.critical(self, "Could not create flight", error,
QMessageBox.Ok)
self.error_box("Could not create flight", error)
return
task = self.task_selector.currentData()
@@ -112,6 +108,7 @@ class QFlightCreator(QDialog):
else:
start_type = "Warm"
flight = Flight(self.package, aircraft, size, origin, task, start_type)
flight.scheduled_in = self.package.delay
flight.client_count = self.client_slots_spinner.value()
# noinspection PyUnresolvedReferences
@@ -122,8 +119,7 @@ class QFlightCreator(QDialog):
new_aircraft = self.aircraft_selector.itemData(index)
self.airfield_selector.change_aircraft(new_aircraft)
def update_max_size(self, available: int) -> None:
self.flight_size_spinner.setMaximum(min(available, 4))
if self.flight_size_spinner.maximum() >= 2:
if self.flight_size_spinner.value() < 2:
self.flight_size_spinner.setValue(2)
def update_max_size(self) -> None:
self.flight_size_spinner.setMaximum(
min(self.airfield_selector.available, 4)
)

View File

@@ -19,7 +19,7 @@ class QFlightDepartureDisplay(QGroupBox):
layout.addLayout(departure_row)
estimator = TotEstimator(package)
delay = estimator.mission_start_time(flight)
delay = datetime.timedelta(seconds=estimator.mission_start_time(flight))
departure_row.addWidget(QLabel(
f"Departing from <b>{flight.from_cp.name}</b>"

View File

@@ -1,5 +1,3 @@
import logging
from PySide2.QtCore import Signal
from PySide2.QtWidgets import QLabel, QHBoxLayout, QGroupBox, QSpinBox, QGridLayout
@@ -12,27 +10,30 @@ class QFlightSlotEditor(QGroupBox):
super(QFlightSlotEditor, self).__init__("Slots")
self.flight = flight
self.game = game
self.inventory = self.game.aircraft_inventory.for_control_point(
inventory = self.game.aircraft_inventory.for_control_point(
flight.from_cp
)
available = self.inventory.available(self.flight.unit_type)
max_count = self.flight.count + available
if max_count > 4:
max_count = 4
self.available = inventory.all_aircraft
if self.flight.unit_type not in self.available:
max = self.flight.count
else:
max = self.flight.count + self.available[self.flight.unit_type]
if max > 4:
max = 4
layout = QGridLayout()
self.aircraft_count = QLabel("Aircraft count :")
self.aircraft_count_spinner = QSpinBox()
self.aircraft_count_spinner.setMinimum(1)
self.aircraft_count_spinner.setMaximum(max_count)
self.aircraft_count_spinner.setMaximum(max)
self.aircraft_count_spinner.setValue(flight.count)
self.aircraft_count_spinner.valueChanged.connect(self._changed_aircraft_count)
self.client_count = QLabel("Client slots count :")
self.client_count_spinner = QSpinBox()
self.client_count_spinner.setMinimum(0)
self.client_count_spinner.setMaximum(max_count)
self.client_count_spinner.setMaximum(max)
self.client_count_spinner.setValue(flight.client_count)
self.client_count_spinner.valueChanged.connect(self._changed_client_count)
@@ -49,23 +50,9 @@ class QFlightSlotEditor(QGroupBox):
self.setLayout(layout)
def _changed_aircraft_count(self):
self.game.aircraft_inventory.return_from_flight(self.flight)
old_count = self.flight.count
self.flight.count = int(self.aircraft_count_spinner.value())
try:
self.game.aircraft_inventory.claim_for_flight(self.flight)
except ValueError:
# The UI should have prevented this, but if we ran out of aircraft
# then roll back the inventory change.
difference = self.flight.count - old_count
available = self.inventory.available(self.flight.unit_type)
logging.error(
f"Could not add {difference} additional aircraft to "
f"{self.flight} because {self.flight.from_cp} has only "
f"{available} {self.flight.unit_type} remaining")
self.flight.count = old_count
self.game.aircraft_inventory.claim_for_flight(self.flight)
self.changed.emit()
# TODO check if enough aircraft are available
def _changed_client_count(self):
self.flight.client_count = int(self.client_count_spinner.value())

View File

@@ -1,11 +1,12 @@
import datetime
import itertools
from datetime import timedelta
from PySide2.QtCore import QItemSelectionModel, QPoint
from PySide2.QtGui import QStandardItem, QStandardItemModel
from PySide2.QtWidgets import QHeaderView, QTableView
from game.utils import meter_to_feet
from gen.aircraft import PackageWaypointTiming
from gen.ato import Package
from gen.flights.flight import Flight, FlightWaypoint
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import \
@@ -42,6 +43,8 @@ class QFlightWaypointList(QTableView):
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
timing = PackageWaypointTiming.for_package(self.package)
# The first waypoint is set up by pydcs at mission generation time, so
# we need to add that waypoint manually.
takeoff = FlightWaypoint(self.flight.from_cp.position.x,
@@ -52,12 +55,13 @@ class QFlightWaypointList(QTableView):
waypoints = itertools.chain([takeoff], self.flight.points)
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.add_waypoint_row(row, self.flight, waypoint, timing)
self.selectionModel().setCurrentIndex(self.indexAt(QPoint(1, 1)),
QItemSelectionModel.Select)
def add_waypoint_row(self, row: int, flight: Flight,
waypoint: FlightWaypoint) -> None:
waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> None:
self.model.insertRow(self.model.rowCount())
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
@@ -68,19 +72,18 @@ class QFlightWaypointList(QTableView):
altitude_item.setEditable(False)
self.model.setItem(row, 1, altitude_item)
tot = self.tot_text(flight, waypoint)
tot = self.tot_text(flight, waypoint, timing)
tot_item = QStandardItem(tot)
tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item)
@staticmethod
def tot_text(flight: Flight, waypoint: FlightWaypoint) -> str:
def tot_text(self, flight: Flight, waypoint: FlightWaypoint,
timing: PackageWaypointTiming) -> str:
prefix = ""
time = flight.flight_plan.tot_for_waypoint(waypoint)
time = timing.tot_for_waypoint(flight, waypoint)
if time is None:
prefix = "Depart "
time = flight.flight_plan.depart_time_for_waypoint(waypoint)
time = timing.depart_time_for_waypoint(waypoint, self.flight)
if time is None:
return ""
time = timedelta(seconds=int(time.total_seconds()))
return f"{prefix}T+{time}"
return f"{prefix}T+{datetime.timedelta(seconds=time)}"

View File

@@ -1,4 +1,4 @@
from typing import Iterable, List, Optional
from typing import List, Optional
from PySide2.QtCore import Signal
from PySide2.QtWidgets import (
@@ -12,18 +12,13 @@ from PySide2.QtWidgets import (
from game import Game
from gen.ato import Package
from gen.flights.flight import Flight, FlightType, FlightWaypoint
from gen.flights.flightplan import (
CustomFlightPlan,
FlightPlanBuilder,
StrikeFlightPlan,
)
from gen.flights.flight import Flight, FlightType
from gen.flights.flightplan import FlightPlanBuilder
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointList import \
QFlightWaypointList
from qt_ui.windows.mission.flight.waypoints \
.QPredefinedWaypointSelectionWindow import \
from qt_ui.windows.mission.flight.waypoints.QPredefinedWaypointSelectionWindow import \
QPredefinedWaypointSelectionWindow
from theater import FrontLine
from theater import ControlPoint, FrontLine
class QFlightWaypointTab(QFrame):
@@ -38,6 +33,8 @@ class QFlightWaypointTab(QFrame):
self.planner = FlightPlanBuilder(self.game, package, is_player=True)
self.flight_waypoint_list: Optional[QFlightWaypointList] = None
self.ascend_waypoint: Optional[QPushButton] = None
self.descend_waypoint: Optional[QPushButton] = None
self.rtb_waypoint: Optional[QPushButton] = None
self.delete_selected: Optional[QPushButton] = None
self.open_fast_waypoint_button: Optional[QPushButton] = None
@@ -62,7 +59,6 @@ class QFlightWaypointTab(QFrame):
recreate_types = [
FlightType.CAS,
FlightType.CAP,
FlightType.DEAD,
FlightType.ESCORT,
FlightType.SEAD,
FlightType.STRIKE
@@ -80,6 +76,14 @@ class QFlightWaypointTab(QFrame):
rlayout.addWidget(QLabel("<strong>Advanced : </strong>"))
rlayout.addWidget(QLabel("<small>Do not use for AI flights</small>"))
self.ascend_waypoint = QPushButton("Add Ascend Waypoint")
self.ascend_waypoint.clicked.connect(self.on_ascend_waypoint)
rlayout.addWidget(self.ascend_waypoint)
self.descend_waypoint = QPushButton("Add Descend Waypoint")
self.descend_waypoint.clicked.connect(self.on_descend_waypoint)
rlayout.addWidget(self.descend_waypoint)
self.rtb_waypoint = QPushButton("Add RTB Waypoint")
self.rtb_waypoint.clicked.connect(self.on_rtb_waypoint)
rlayout.addWidget(self.rtb_waypoint)
@@ -97,51 +101,35 @@ class QFlightWaypointTab(QFrame):
def on_delete_waypoint(self):
wpt = self.flight_waypoint_list.selectionModel().currentIndex().row()
if wpt > 0:
self.delete_waypoint(self.flight.flight_plan.waypoints[wpt])
del self.flight.points[wpt-1]
self.flight_waypoint_list.update_list()
self.on_change()
def delete_waypoint(self, waypoint: FlightWaypoint) -> None:
# Need to degrade to a custom flight plan and remove the waypoint.
# If the waypoint is a target waypoint and is not the last target
# waypoint, we don't need to degrade.
if isinstance(self.flight.flight_plan, StrikeFlightPlan):
is_target = waypoint in self.flight.flight_plan.targets
if is_target and len(self.flight.flight_plan.targets) > 1:
self.flight.flight_plan.targets.remove(waypoint)
return
self.degrade_to_custom_flight_plan()
self.flight.flight_plan.waypoints.remove(waypoint)
def on_fast_waypoint(self):
self.subwindow = QPredefinedWaypointSelectionWindow(self.game, self.flight, self.flight_waypoint_list)
self.subwindow.waypoints_added.connect(self.on_waypoints_added)
self.subwindow.finished.connect(self.on_change)
self.subwindow.show()
def on_waypoints_added(self, waypoints: Iterable[FlightWaypoint]) -> None:
if not waypoints:
return
self.degrade_to_custom_flight_plan()
self.flight.flight_plan.waypoints.extend(waypoints)
def on_ascend_waypoint(self):
ascend = self.planner.generate_ascend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(ascend)
self.flight_waypoint_list.update_list()
self.on_change()
def on_rtb_waypoint(self):
rtb = self.planner.generate_rtb_waypoint(self.flight,
self.flight.from_cp)
self.degrade_to_custom_flight_plan()
self.flight.flight_plan.waypoints.append(rtb)
self.flight.points.append(rtb)
self.flight_waypoint_list.update_list()
self.on_change()
def degrade_to_custom_flight_plan(self) -> None:
if not isinstance(self.flight.flight_plan, CustomFlightPlan):
self.flight.flight_plan = CustomFlightPlan(
package=self.flight.package,
flight=self.flight,
custom_waypoints=self.flight.flight_plan.waypoints
)
def on_descend_waypoint(self):
descend = self.planner.generate_descend_point(self.flight,
self.flight.from_cp)
self.flight.points.append(descend)
self.flight_waypoint_list.update_list()
self.on_change()
def confirm_recreate(self, task: FlightType) -> None:
result = QMessageBox.question(
@@ -161,7 +149,7 @@ class QFlightWaypointTab(QFrame):
if task == FlightType.CAP:
if isinstance(self.package.target, FrontLine):
task = FlightType.TARCAP
else:
elif isinstance(self.package.target, ControlPoint):
task = FlightType.BARCAP
self.flight.flight_type = task
self.planner.populate_flight_plan(self.flight)

View File

@@ -1,20 +1,11 @@
from PySide2.QtCore import Qt, Signal
from PySide2.QtWidgets import (
QCheckBox,
QDialog,
QHBoxLayout,
QLabel,
QPushButton,
QVBoxLayout,
)
from PySide2.QtCore import Qt
from PySide2.QtWidgets import QDialog, QLabel, QHBoxLayout, QVBoxLayout, QPushButton, QCheckBox
from game import Game
from gen.flights.flight import Flight
from qt_ui.uiconstants import EVENT_ICONS
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import \
QPredefinedWaypointSelectionComboBox
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import \
QFlightWaypointInfoBox
from qt_ui.widgets.combos.QPredefinedWaypointSelectionComboBox import QPredefinedWaypointSelectionComboBox
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointInfoBox import QFlightWaypointInfoBox
PREDEFINED_WAYPOINT_CATEGORIES = [
"Frontline (CAS AREA)",
@@ -26,8 +17,6 @@ PREDEFINED_WAYPOINT_CATEGORIES = [
class QPredefinedWaypointSelectionWindow(QDialog):
# List of FlightWaypoint
waypoints_added = Signal(list)
def __init__(self, game: Game, flight: Flight, flight_waypoint_list):
super(QPredefinedWaypointSelectionWindow, self).__init__()
@@ -55,6 +44,7 @@ class QPredefinedWaypointSelectionWindow(QDialog):
self.init_ui()
self.on_select_wpt_changed()
print("DONE")
def init_ui(self):
@@ -87,5 +77,12 @@ class QPredefinedWaypointSelectionWindow(QDialog):
self.add_button.setDisabled(False)
def add_waypoint(self):
self.waypoints_added.emit(self.selected_waypoints)
for wpt in self.selected_waypoints:
self.flight.points.append(wpt)
self.flight_waypoint_list.update_list()
self.close()

View File

@@ -20,7 +20,6 @@ class Campaign:
name: str
icon_name: str
authors: str
description: str
theater: ConflictTheater
@classmethod
@@ -30,7 +29,7 @@ class Campaign:
sanitized_theater = data["theater"].replace(" ", "")
return cls(data["name"], f"Terrain_{sanitized_theater}", data.get("authors", "???"),
data.get("description", ""), ConflictTheater.from_json(data))
ConflictTheater.from_json(data))
def load_campaigns() -> List[Campaign]:

View File

@@ -1,32 +1,25 @@
from __future__ import unicode_literals
import datetime
import logging
from typing import List, Optional
from PySide2 import QtGui, QtWidgets
from PySide2.QtCore import QItemSelectionModel, QPoint, Qt
from PySide2.QtWidgets import QVBoxLayout, QTextEdit
from jinja2 import Environment, FileSystemLoader, select_autoescape
from PySide2.QtWidgets import QVBoxLayout
from dcs.task import CAP, CAS
from game import db
import qt_ui.uiconstants as CONST
from game import Game, db
from game.settings import Settings
from gen import namegen
from qt_ui.windows.newgame.QCampaignList import (
Campaign,
QCampaignList,
load_campaigns,
)
from theater.start_generator import GameGenerator
from theater import ConflictTheater, start_generator
jinja_env = Environment(
loader=FileSystemLoader("resources/ui/templates"),
autoescape=select_autoescape(
disabled_extensions=("",),
default_for_string=True,
default=True,
),
trim_blocks=True,
lstrip_blocks=True,
)
class NewGameWizard(QtWidgets.QWizard):
def __init__(self, parent=None):
@@ -48,6 +41,7 @@ class NewGameWizard(QtWidgets.QWizard):
self.generatedGame = None
def accept(self):
logging.info("New Game Wizard accept")
logging.info("======================")
@@ -62,9 +56,7 @@ class NewGameWizard(QtWidgets.QWizard):
timePeriod = db.TIME_PERIODS[list(db.TIME_PERIODS.keys())[self.field("timePeriod")]]
midGame = self.field("midGame")
# QSlider forces integers, so we use 1 to 50 and divide by 10 to give
# 0.1 to 5.0.
multiplier = self.field("multiplier") / 10
multiplier = self.field("multiplier")
no_carrier = self.field("no_carrier")
no_lha = self.field("no_lha")
supercarrier = self.field("supercarrier")
@@ -84,13 +76,39 @@ class NewGameWizard(QtWidgets.QWizard):
settings.do_not_generate_player_navy = no_player_navy
settings.do_not_generate_enemy_navy = no_enemy_navy
generator = GameGenerator(player_name, enemy_name, conflictTheater,
settings, timePeriod, starting_money,
multiplier, midGame)
self.generatedGame = generator.generate()
self.generatedGame = self.start_new_game(player_name, enemy_name, conflictTheater, midGame, multiplier,
timePeriod, settings, starting_money)
super(NewGameWizard, self).accept()
def start_new_game(self, player_name: str, enemy_name: str, conflictTheater: ConflictTheater,
midgame: bool, multiplier: float, period: datetime, settings:Settings, starting_money: int):
# Reset name generator
namegen.reset()
start_generator.prepare_theater(conflictTheater, settings, midgame)
print("-- Starting New Game Generator")
print("Enemy name : " + enemy_name)
print("Player name : " + player_name)
print("Midgame : " + str(midgame))
start_generator.generate_initial_units(conflictTheater, enemy_name, True, multiplier)
print("-- Initial units generated")
game = Game(player_name=player_name,
enemy_name=enemy_name,
theater=conflictTheater,
start_date=period,
settings=settings)
print("-- Game Object generated")
start_generator.generate_groundobjects(conflictTheater, game)
game.budget = starting_money
game.settings.multiplier = multiplier
game.settings.sams = True
game.settings.version = CONST.VERSION_STRING
return game
class IntroPage(QtWidgets.QWizardPage):
def __init__(self, parent=None):
@@ -122,9 +140,7 @@ class FactionSelection(QtWidgets.QWizardPage):
# Factions selection
self.factionsGroup = QtWidgets.QGroupBox("Factions")
self.factionsGroupLayout = QtWidgets.QHBoxLayout()
self.blueGroupLayout = QtWidgets.QGridLayout()
self.redGroupLayout = QtWidgets.QGridLayout()
self.factionsGroupLayout = QtWidgets.QGridLayout()
blueFaction = QtWidgets.QLabel("<b>Player Faction :</b>")
self.blueFactionSelect = QtWidgets.QComboBox()
@@ -136,13 +152,6 @@ class FactionSelection(QtWidgets.QWizardPage):
self.redFactionSelect = QtWidgets.QComboBox()
redFaction.setBuddy(self.redFactionSelect)
# Faction description
self.blueFactionDescription = QTextEdit("")
self.blueFactionDescription.setReadOnly(True)
self.redFactionDescription = QTextEdit("")
self.redFactionDescription.setReadOnly(True)
# Setup default selected factions
for i, r in enumerate(db.FACTIONS):
self.redFactionSelect.addItem(r)
@@ -151,16 +160,20 @@ class FactionSelection(QtWidgets.QWizardPage):
if r == "USA 2005":
self.blueFactionSelect.setCurrentIndex(i)
self.blueGroupLayout.addWidget(blueFaction, 0, 0)
self.blueGroupLayout.addWidget(self.blueFactionSelect, 0, 1)
self.blueGroupLayout.addWidget(self.blueFactionDescription, 1, 0, 1, 2)
self.blueSideRecap = QtWidgets.QLabel("")
self.blueSideRecap.setFont(CONST.FONT_PRIMARY_I)
self.blueSideRecap.setWordWrap(True)
self.redGroupLayout.addWidget(redFaction, 0, 0)
self.redGroupLayout.addWidget(self.redFactionSelect, 0, 1)
self.redGroupLayout.addWidget(self.redFactionDescription, 1, 0, 1, 2)
self.redSideRecap = QtWidgets.QLabel("")
self.redSideRecap.setFont(CONST.FONT_PRIMARY_I)
self.redSideRecap.setWordWrap(True)
self.factionsGroupLayout.addLayout(self.blueGroupLayout)
self.factionsGroupLayout.addLayout(self.redGroupLayout)
self.factionsGroupLayout.addWidget(blueFaction, 0, 0)
self.factionsGroupLayout.addWidget(self.blueFactionSelect, 0, 1)
self.factionsGroupLayout.addWidget(self.blueSideRecap, 1, 0, 1, 2)
self.factionsGroupLayout.addWidget(redFaction, 2, 0)
self.factionsGroupLayout.addWidget(self.redFactionSelect, 2, 1)
self.factionsGroupLayout.addWidget(self.redSideRecap, 3, 0, 1, 2)
self.factionsGroup.setLayout(self.factionsGroupLayout)
# Create required mod layout
@@ -186,34 +199,39 @@ class FactionSelection(QtWidgets.QWizardPage):
def updateUnitRecap(self):
self.requiredMods.setText("<ul>")
red_faction = db.FACTIONS[self.redFactionSelect.currentText()]
blue_faction = db.FACTIONS[self.blueFactionSelect.currentText()]
template = jinja_env.get_template("factiontemplate_EN.j2")
red_units = red_faction.aircrafts
blue_units = blue_faction.aircrafts
blue_faction_txt = template.render({"faction": blue_faction})
red_faction_txt = template.render({"faction": red_faction})
blue_txt = ""
for u in blue_units:
if u in db.UNIT_BY_TASK[CAP] or u in db.UNIT_BY_TASK[CAS]:
blue_txt = blue_txt + u.id + ", "
blue_txt = blue_txt + "\n"
self.blueSideRecap.setText(blue_txt)
self.blueFactionDescription.setText(blue_faction_txt)
self.redFactionDescription.setText(red_faction_txt)
red_txt = ""
for u in red_units:
if u in db.UNIT_BY_TASK[CAP] or u in db.UNIT_BY_TASK[CAS]:
red_txt = red_txt + u.id + ", "
red_txt = red_txt + "\n"
self.redSideRecap.setText(red_txt)
# Compute mod requirements txt
self.requiredMods.setText("<ul>")
has_mod = False
if len(red_faction.requirements.keys()) > 0:
has_mod = True
for mod in red_faction.requirements.keys():
self.requiredMods.setText(
self.requiredMods.text() + "\n<li>" + mod + ": <a href=\"" + red_faction.requirements[mod] + "\">" +
red_faction.requirements[mod] + "</a></li>")
self.requiredMods.setText(self.requiredMods.text() + "\n<li>" + mod + ": <a href=\""+red_faction.requirements[mod]+"\">" + red_faction.requirements[mod] + "</a></li>")
if len(blue_faction.requirements.keys()) > 0:
has_mod = True
for mod in blue_faction.requirements.keys():
if mod not in red_faction.requirements.keys():
self.requiredMods.setText(
self.requiredMods.text() + "\n<li>" + mod + ": <a href=\"" + blue_faction.requirements[
mod] + "\">" + blue_faction.requirements[mod] + "</a></li>")
if not "requirements" in red_faction.keys() or mod not in red_faction.requirements.keys():
self.requiredMods.setText(self.requiredMods.text() + "\n<li>" + mod + ": <a href=\""+blue_faction.requirements[mod]+"\">" + blue_faction.requirements[mod] + "</a></li>")
if has_mod:
self.requiredMods.setText(self.requiredMods.text() + "</ul>\n\n")
@@ -237,16 +255,10 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
campaignList = QCampaignList(campaigns)
self.registerField("selectedCampaign", campaignList)
# Faction description
self.campaignMapDescription = QTextEdit("")
self.campaignMapDescription.setReadOnly(True)
def on_campaign_selected():
template = jinja_env.get_template("campaigntemplate_EN.j2")
index = campaignList.selectionModel().currentIndex().row()
campaign = campaignList.campaigns[index]
self.setField("selectedCampaign", campaign)
self.campaignMapDescription.setText(template.render({"campaign": campaign}))
campaignList.selectionModel().setCurrentIndex(campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows)
campaignList.selectionModel().selectionChanged.connect(on_campaign_selected)
@@ -282,9 +294,8 @@ class TheaterConfiguration(QtWidgets.QWizardPage):
layout = QtWidgets.QGridLayout()
layout.setColumnMinimumWidth(0, 20)
layout.addWidget(campaignList, 0, 0, 3, 1)
layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1)
layout.addWidget(mapSettingsGroup, 1, 1, 1, 1)
layout.addWidget(timeGroup, 2, 1, 1, 1)
layout.addWidget(mapSettingsGroup, 0, 1, 1, 1)
layout.addWidget(timeGroup, 1, 1, 1, 1)
self.setLayout(layout)
@@ -326,44 +337,6 @@ class BudgetInputs(QtWidgets.QGridLayout):
self.addWidget(self.starting_money, 1, 1)
class ForceMultiplierSpinner(QtWidgets.QSpinBox):
def __init__(self, minimum: Optional[int] = None,
maximum: Optional[int] = None,
initial: Optional[int] = None) -> None:
super().__init__()
if minimum is not None:
self.setMinimum(minimum)
if maximum is not None:
self.setMaximum(maximum)
if initial is not None:
self.setValue(initial)
def textFromValue(self, val: int) -> str:
return f"X {val / 10:.1f}"
class ForceMultiplierInputs(QtWidgets.QGridLayout):
def __init__(self) -> None:
super().__init__()
self.addWidget(QtWidgets.QLabel("Enemy forces multiplier"), 0, 0)
minimum = 1
maximum = 50
initial = 10
slider = QtWidgets.QSlider(Qt.Horizontal)
slider.setMinimum(minimum)
slider.setMaximum(maximum)
slider.setValue(initial)
self.multiplier = ForceMultiplierSpinner(minimum, maximum, initial)
slider.valueChanged.connect(lambda x: self.multiplier.setValue(x))
self.multiplier.valueChanged.connect(lambda x: slider.setValue(x))
self.addWidget(slider, 1, 0)
self.addWidget(self.multiplier, 1, 1)
class MiscOptions(QtWidgets.QWizardPage):
def __init__(self, parent=None):
super(MiscOptions, self).__init__(parent)
@@ -374,12 +347,14 @@ class MiscOptions(QtWidgets.QWizardPage):
QtGui.QPixmap('./resources/ui/wizard/logo1.png'))
midGame = QtWidgets.QCheckBox()
multiplier_inputs = ForceMultiplierInputs()
self.registerField('multiplier', multiplier_inputs.multiplier)
multiplier = QtWidgets.QSpinBox()
multiplier.setEnabled(False)
multiplier.setMinimum(1)
multiplier.setMaximum(5)
miscSettingsGroup = QtWidgets.QGroupBox("Misc Settings")
self.registerField('midGame', midGame)
self.registerField('multiplier', multiplier)
# Campaign settings
generatorSettingsGroup = QtWidgets.QGroupBox("Generator Settings")
@@ -389,7 +364,7 @@ class MiscOptions(QtWidgets.QWizardPage):
self.registerField('no_lha', no_lha)
supercarrier = QtWidgets.QCheckBox()
self.registerField('supercarrier', supercarrier)
no_player_navy = QtWidgets.QCheckBox()
no_player_navy= QtWidgets.QCheckBox()
self.registerField('no_player_navy', no_player_navy)
no_enemy_navy = QtWidgets.QCheckBox()
self.registerField('no_enemy_navy', no_enemy_navy)
@@ -397,7 +372,8 @@ class MiscOptions(QtWidgets.QWizardPage):
layout = QtWidgets.QGridLayout()
layout.addWidget(QtWidgets.QLabel("Start at mid game"), 1, 0)
layout.addWidget(midGame, 1, 1)
layout.addLayout(multiplier_inputs, 2, 0)
layout.addWidget(QtWidgets.QLabel("Ennemy forces multiplier [Disabled for Now]"), 2, 0)
layout.addWidget(multiplier, 2, 1)
miscSettingsGroup.setLayout(layout)
generatorLayout = QtWidgets.QGridLayout()

View File

@@ -26,8 +26,7 @@ from game.infos.information import Information
from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.finances.QFinancesMenu import QHorizontalSeparationLine
from qt_ui.windows.settings.plugins import PluginOptionsPage, PluginsPage
from plugin import LuaPluginManager
class CheatSettingsBox(QGroupBox):
def __init__(self, game: Game, apply_settings: Callable[[], None]) -> None:
@@ -98,21 +97,21 @@ class QSettingsWindow(QDialog):
self.categoryModel.appendRow(cheat)
self.right_layout.addWidget(self.cheatPage)
self.pluginsPage = PluginsPage()
plugins = QStandardItem("LUA Plugins")
plugins.setIcon(CONST.ICONS["Plugins"])
plugins.setEditable(False)
plugins.setSelectable(True)
self.categoryModel.appendRow(plugins)
self.right_layout.addWidget(self.pluginsPage)
self.pluginsOptionsPage = PluginOptionsPage()
pluginsOptions = QStandardItem("LUA Plugins Options")
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
pluginsOptions.setEditable(False)
pluginsOptions.setSelectable(True)
self.categoryModel.appendRow(pluginsOptions)
self.right_layout.addWidget(self.pluginsOptionsPage)
self.initPluginsLayout()
if self.pluginsPage:
plugins = QStandardItem("LUA Plugins")
plugins.setIcon(CONST.ICONS["Plugins"])
plugins.setEditable(False)
plugins.setSelectable(True)
self.categoryModel.appendRow(plugins)
self.right_layout.addWidget(self.pluginsPage)
if self.pluginsOptionsPage:
pluginsOptions = QStandardItem("LUA Plugins Options")
pluginsOptions.setIcon(CONST.ICONS["PluginsOptions"])
pluginsOptions.setEditable(False)
pluginsOptions.setSelectable(True)
self.categoryModel.appendRow(pluginsOptions)
self.right_layout.addWidget(self.pluginsOptionsPage)
self.categoryList.setSelectionBehavior(QAbstractItemView.SelectRows)
self.categoryList.setModel(self.categoryModel)
@@ -206,7 +205,7 @@ class QSettingsWindow(QDialog):
self.generatorPage.setLayout(self.generatorLayout)
self.gameplay = QGroupBox("Gameplay")
self.gameplayLayout = QGridLayout()
self.gameplayLayout = QGridLayout();
self.gameplayLayout.setAlignment(Qt.AlignTop)
self.gameplay.setLayout(self.gameplayLayout)
@@ -218,23 +217,10 @@ class QSettingsWindow(QDialog):
self.generate_marks.setChecked(self.game.settings.generate_marks)
self.generate_marks.toggled.connect(self.applySettings)
self.never_delay_players = QCheckBox()
self.never_delay_players.setChecked(
self.game.settings.never_delay_player_flights)
self.never_delay_players.toggled.connect(self.applySettings)
self.never_delay_players.setToolTip(
"When checked, player flights with a delayed start time will be "
"spawned immediately. AI wingmen may begin startup immediately."
)
self.gameplayLayout.addWidget(QLabel("Use Supercarrier Module"), 0, 0)
self.gameplayLayout.addWidget(self.supercarrier, 0, 1, Qt.AlignRight)
self.gameplayLayout.addWidget(QLabel("Put Objective Markers on Map"), 1, 0)
self.gameplayLayout.addWidget(self.generate_marks, 1, 1, Qt.AlignRight)
self.gameplayLayout.addWidget(
QLabel("Never delay player flights"), 2, 0)
self.gameplayLayout.addWidget(self.never_delay_players, 2, 1,
Qt.AlignRight)
self.performance = QGroupBox("Performance")
self.performanceLayout = QGridLayout()
@@ -331,6 +317,34 @@ class QSettingsWindow(QDialog):
self.moneyCheatBoxLayout.addWidget(btn, i/2, i%2)
self.cheatLayout.addWidget(self.moneyCheatBox, stretch=1)
def initPluginsLayout(self):
uiPrepared = False
row:int = 0
for plugin in LuaPluginManager().getPlugins():
if plugin.hasUI():
if not uiPrepared:
uiPrepared = True
self.pluginsOptionsPage = QWidget()
self.pluginsOptionsPageLayout = QVBoxLayout()
self.pluginsOptionsPageLayout.setAlignment(Qt.AlignTop)
self.pluginsOptionsPage.setLayout(self.pluginsOptionsPageLayout)
self.pluginsPage = QWidget()
self.pluginsPageLayout = QVBoxLayout()
self.pluginsPageLayout.setAlignment(Qt.AlignTop)
self.pluginsPage.setLayout(self.pluginsPageLayout)
self.pluginsGroup = QGroupBox("Plugins")
self.pluginsGroupLayout = QGridLayout();
self.pluginsGroupLayout.setAlignment(Qt.AlignTop)
self.pluginsGroup.setLayout(self.pluginsGroupLayout)
self.pluginsPageLayout.addWidget(self.pluginsGroup)
plugin.setupUI(self, row)
row = row + 1
def cheatLambda(self, amount):
return lambda: self.cheatMoney(amount)
@@ -352,7 +366,6 @@ class QSettingsWindow(QDialog):
self.game.settings.map_coalition_visibility = self.mapVisibiitySelection.currentData()
self.game.settings.external_views_allowed = self.ext_views.isChecked()
self.game.settings.generate_marks = self.generate_marks.isChecked()
self.game.settings.never_delay_player_flights = self.never_delay_players.isChecked()
self.game.settings.supercarrier = self.supercarrier.isChecked()

View File

@@ -1,71 +0,0 @@
from PySide2.QtCore import Qt
from PySide2.QtWidgets import (
QCheckBox,
QGridLayout,
QGroupBox,
QLabel, QVBoxLayout,
QWidget,
)
from game.plugins import LuaPlugin, LuaPluginManager
class PluginsBox(QGroupBox):
def __init__(self) -> None:
super().__init__("Plugins")
layout = QGridLayout()
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
for row, plugin in enumerate(LuaPluginManager.plugins()):
if not plugin.show_in_ui:
continue
layout.addWidget(QLabel(plugin.name), row, 0)
checkbox = QCheckBox()
checkbox.setChecked(plugin.enabled)
checkbox.toggled.connect(plugin.set_enabled)
layout.addWidget(checkbox, row, 1)
class PluginsPage(QWidget):
def __init__(self) -> None:
super().__init__()
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
layout.addWidget(PluginsBox())
class PluginOptionsBox(QGroupBox):
def __init__(self, plugin: LuaPlugin) -> None:
super().__init__(plugin.name)
layout = QGridLayout()
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
for row, option in enumerate(plugin.options):
layout.addWidget(QLabel(option.name), row, 0)
checkbox = QCheckBox()
checkbox.setChecked(option.enabled)
checkbox.toggled.connect(option.set_enabled)
layout.addWidget(checkbox, row, 1)
class PluginOptionsPage(QWidget):
def __init__(self) -> None:
super().__init__()
layout = QVBoxLayout()
layout.setAlignment(Qt.AlignTop)
self.setLayout(layout)
for plugin in LuaPluginManager.plugins():
if plugin.options:
layout.addWidget(PluginOptionsBox(plugin))

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