From 2ee604d2a4e7e1a2c7bbc4f1565005952f87678b Mon Sep 17 00:00:00 2001 From: Khopa Date: Mon, 16 Aug 2021 19:45:43 +0200 Subject: [PATCH 01/48] Fixed : Missing icons for E-2C Hawkeye --- resources/ui/units/aircrafts/icons/E-2C_24.jpg | Bin 0 -> 1219 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/ui/units/aircrafts/icons/E-2C_24.jpg diff --git a/resources/ui/units/aircrafts/icons/E-2C_24.jpg b/resources/ui/units/aircrafts/icons/E-2C_24.jpg new file mode 100644 index 0000000000000000000000000000000000000000..9cf5dd6574b9fa2fdcf8187529939f96ea21efe9 GIT binary patch literal 1219 zcmex=^(PF6}rMnOeST|r4lSw=>~TvNxu(8R<c1}I=;VrF4wW9Q)H;sz?% zD!{d!pzFb!U9xX3zTPI5o8roG<0MW4oqZMDikqloVbuf*=gfJ(V&YTRE(2~ znmD<{#3dx9RMpfqG__1j&CD$#!<}8*yUtyUp1Db>v)qtbU9_Xdbus^5l~cwI zKJJsF?x|l9{9{?web}m%{nep8hr(Q3g)O&s`@U7lp|hT+*}`QrgUmnL{eCZ9Y!Gt%O`Os>h_HJv5FX7-!+uK0KE z+q%j>rL0@e7{zSy7MbL6U;EvG8)kj;c0RwiZT>MUFOB4aRu6XNd-Jy@edE~pckWy7 zzw_S)9bOxCZuX;+(yw7>J^wBjU$FeFQKjOY$LpSWpDt?(Dl;=~Za$jMYjpf{Nogi? z@-&;CszBFL3)go&S7wFzthmM6soS1<@-g#*717QfCk(yL{`>r>ODk5wu2pMutVdg0 zdGZ+s4F(y;-TNwD)xH#Vo?vV_)9>Sn#1sD+m|aEf4zX5e3fQ;LF>`LS+Y|EfrSJmD ziu0G(iUhtW0R_45`v3p{ literal 0 HcmV?d00001 From 57e78d5c559396e899c0849ab6a1dd82b2e1f0be Mon Sep 17 00:00:00 2001 From: Khopa Date: Mon, 16 Aug 2021 19:47:00 +0200 Subject: [PATCH 02/48] Added squadrons for Syria & Israel --- resources/squadrons/AH-1X/IAF 160th Sqn.yaml | 11 +++++++++++ resources/squadrons/AH-64D/IAF 113th Sqn.yaml | 10 ++++++++++ resources/squadrons/F-4E/IAF 201th Sqn.yaml | 13 +++++++++++++ resources/squadrons/Mi-24/SAAF 765th Sqn.yaml | 11 +++++++++++ resources/squadrons/Mi-24/SAAF 766th Sqn.yaml | 11 +++++++++++ resources/squadrons/Mi-8/SAAF 253th Sqn.yaml | 11 +++++++++++ resources/squadrons/Mi-8/SAAF 255th Sqn.yaml | 11 +++++++++++ resources/squadrons/Mig-21/SAAF 679th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-21/SAAF 680th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-21/SAAF 825th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-21/SAAF 8th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-23/SAAF 678th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-25/SAAF 1st Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-29/SAAF 697th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Mig-29/SAAF 699th Sqn.yaml | 14 ++++++++++++++ resources/squadrons/Su-17/SAAF 677th Sqn.yaml | 15 +++++++++++++++ resources/squadrons/Su-17/SAAF 685th Sqn.yaml | 15 +++++++++++++++ resources/squadrons/Su-17/SAAF 827th Sqn.yaml | 15 +++++++++++++++ resources/squadrons/Su-24/SAAF 819th Sqn.yaml | 15 +++++++++++++++ resources/squadrons/sa342/SAAF 976th Sqn.yaml | 10 ++++++++++ resources/squadrons/sa342/SAAF 977th Sqn.yaml | 10 ++++++++++ resources/squadrons/sa342/SAAF 988th Sqn.yaml | 10 ++++++++++ resources/squadrons/sa342/SAAF 989th Sqn.yaml | 10 ++++++++++ 23 files changed, 290 insertions(+) create mode 100644 resources/squadrons/AH-1X/IAF 160th Sqn.yaml create mode 100644 resources/squadrons/AH-64D/IAF 113th Sqn.yaml create mode 100644 resources/squadrons/F-4E/IAF 201th Sqn.yaml create mode 100644 resources/squadrons/Mi-24/SAAF 765th Sqn.yaml create mode 100644 resources/squadrons/Mi-24/SAAF 766th Sqn.yaml create mode 100644 resources/squadrons/Mi-8/SAAF 253th Sqn.yaml create mode 100644 resources/squadrons/Mi-8/SAAF 255th Sqn.yaml create mode 100644 resources/squadrons/Mig-21/SAAF 679th Sqn.yaml create mode 100644 resources/squadrons/Mig-21/SAAF 680th Sqn.yaml create mode 100644 resources/squadrons/Mig-21/SAAF 825th Sqn.yaml create mode 100644 resources/squadrons/Mig-21/SAAF 8th Sqn.yaml create mode 100644 resources/squadrons/Mig-23/SAAF 678th Sqn.yaml create mode 100644 resources/squadrons/Mig-25/SAAF 1st Sqn.yaml create mode 100644 resources/squadrons/Mig-29/SAAF 697th Sqn.yaml create mode 100644 resources/squadrons/Mig-29/SAAF 699th Sqn.yaml create mode 100644 resources/squadrons/Su-17/SAAF 677th Sqn.yaml create mode 100644 resources/squadrons/Su-17/SAAF 685th Sqn.yaml create mode 100644 resources/squadrons/Su-17/SAAF 827th Sqn.yaml create mode 100644 resources/squadrons/Su-24/SAAF 819th Sqn.yaml create mode 100644 resources/squadrons/sa342/SAAF 976th Sqn.yaml create mode 100644 resources/squadrons/sa342/SAAF 977th Sqn.yaml create mode 100644 resources/squadrons/sa342/SAAF 988th Sqn.yaml create mode 100644 resources/squadrons/sa342/SAAF 989th Sqn.yaml diff --git a/resources/squadrons/AH-1X/IAF 160th Sqn.yaml b/resources/squadrons/AH-1X/IAF 160th Sqn.yaml new file mode 100644 index 00000000..736902a5 --- /dev/null +++ b/resources/squadrons/AH-1X/IAF 160th Sqn.yaml @@ -0,0 +1,11 @@ +--- +name: 160th Squadron +nickname: Northern Cobra Squadron +country: Israel +role: Attack Helicopter +aircraft: AH-1W SuperCobra +livery: ah-64_d_isr +mission_types: + - CAS + - BAI + diff --git a/resources/squadrons/AH-64D/IAF 113th Sqn.yaml b/resources/squadrons/AH-64D/IAF 113th Sqn.yaml new file mode 100644 index 00000000..28fcbe94 --- /dev/null +++ b/resources/squadrons/AH-64D/IAF 113th Sqn.yaml @@ -0,0 +1,10 @@ +--- +name: 113th Squadron +nickname: The Hornet Squadron +country: Israel +role: Attack Helicopter +aircraft: AH-64D Apache Longbow +livery: standard +mission_types: + - CAS + - BAI diff --git a/resources/squadrons/F-4E/IAF 201th Sqn.yaml b/resources/squadrons/F-4E/IAF 201th Sqn.yaml new file mode 100644 index 00000000..9354b912 --- /dev/null +++ b/resources/squadrons/F-4E/IAF 201th Sqn.yaml @@ -0,0 +1,13 @@ +--- +name: 201th Squadron +nickname: The One +country: Israel +role: Air Superiority Fighter +aircraft: F-4E Phantom II +livery: "af standard" +mission_types: + - BARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml b/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml new file mode 100644 index 00000000..84e5df0e --- /dev/null +++ b/resources/squadrons/Mi-24/SAAF 765th Sqn.yaml @@ -0,0 +1,11 @@ +--- +name: 765th Squadron +nickname: 765th +country: Syria +role: Attack Helicopter +aircraft: Mi-24P Hind-F +livery: "SyAAF" +mission_types: + - CAS + - BAI + - Transport diff --git a/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml b/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml new file mode 100644 index 00000000..65649243 --- /dev/null +++ b/resources/squadrons/Mi-24/SAAF 766th Sqn.yaml @@ -0,0 +1,11 @@ +--- +name: 766th Squadron +nickname: 766th +country: Syria +role: Attack Helicopter +aircraft: Mi-24V Hind-E +livery: "standard" +mission_types: + - CAS + - BAI + - Transport diff --git a/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml b/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml new file mode 100644 index 00000000..afa2ae50 --- /dev/null +++ b/resources/squadrons/Mi-8/SAAF 253th Sqn.yaml @@ -0,0 +1,11 @@ +--- +name: 253th Squadron +nickname: 253th +country: Syria +role: Transport Helicopter +aircraft: Mi-8MTV2 Hip +livery: "BP_RS01" +mission_types: + - Transport + - CAS + - BAI diff --git a/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml b/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml new file mode 100644 index 00000000..f1fbca2b --- /dev/null +++ b/resources/squadrons/Mi-8/SAAF 255th Sqn.yaml @@ -0,0 +1,11 @@ +--- +name: 255th Squadron +nickname: 255th +country: Syria +role: Transport Helicopter +aircraft: Mi-8MTV2 Hip +livery: "BP_RS01" +mission_types: + - Transport + - CAS + - BAI diff --git a/resources/squadrons/Mig-21/SAAF 679th Sqn.yaml b/resources/squadrons/Mig-21/SAAF 679th Sqn.yaml new file mode 100644 index 00000000..2e519123 --- /dev/null +++ b/resources/squadrons/Mig-21/SAAF 679th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 679th Squadron +nickname: 679th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-21bis Fishbed-N +livery: "Syria (2)" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-21/SAAF 680th Sqn.yaml b/resources/squadrons/Mig-21/SAAF 680th Sqn.yaml new file mode 100644 index 00000000..56b8987b --- /dev/null +++ b/resources/squadrons/Mig-21/SAAF 680th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 680th Squadron +nickname: 680th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-21bis Fishbed-N +livery: "Syria (2)" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-21/SAAF 825th Sqn.yaml b/resources/squadrons/Mig-21/SAAF 825th Sqn.yaml new file mode 100644 index 00000000..794e24c3 --- /dev/null +++ b/resources/squadrons/Mig-21/SAAF 825th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 825th Squadron +nickname: 825th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-21bis Fishbed-N +livery: "Syria (1)" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-21/SAAF 8th Sqn.yaml b/resources/squadrons/Mig-21/SAAF 8th Sqn.yaml new file mode 100644 index 00000000..1d29d2fa --- /dev/null +++ b/resources/squadrons/Mig-21/SAAF 8th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 8th Squadron +nickname: 8th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-21bis Fishbed-N +livery: "Syria (1)" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-23/SAAF 678th Sqn.yaml b/resources/squadrons/Mig-23/SAAF 678th Sqn.yaml new file mode 100644 index 00000000..39fde573 --- /dev/null +++ b/resources/squadrons/Mig-23/SAAF 678th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 678th Squadron +nickname: 678th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-23MLD Flogger-K +livery: "af standard-3 (worn-out)" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-25/SAAF 1st Sqn.yaml b/resources/squadrons/Mig-25/SAAF 1st Sqn.yaml new file mode 100644 index 00000000..0eb4e6e7 --- /dev/null +++ b/resources/squadrons/Mig-25/SAAF 1st Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 1st Squadron +nickname: 1st +country: Syria +role: Air Superiority Fighter +aircraft: MiG-25PD Foxbat-E +livery: "af standard" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-29/SAAF 697th Sqn.yaml b/resources/squadrons/Mig-29/SAAF 697th Sqn.yaml new file mode 100644 index 00000000..e0208144 --- /dev/null +++ b/resources/squadrons/Mig-29/SAAF 697th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 697th Squadron +nickname: 697th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-29S Fulcrum-C +livery: "ERAF" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-29/SAAF 699th Sqn.yaml b/resources/squadrons/Mig-29/SAAF 699th Sqn.yaml new file mode 100644 index 00000000..5229f239 --- /dev/null +++ b/resources/squadrons/Mig-29/SAAF 699th Sqn.yaml @@ -0,0 +1,14 @@ +--- +name: 699th Squadron +nickname: 699th +country: Syria +role: Air Superiority Fighter +aircraft: MiG-29S Fulcrum-C +livery: "ERAF" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Su-17/SAAF 677th Sqn.yaml b/resources/squadrons/Su-17/SAAF 677th Sqn.yaml new file mode 100644 index 00000000..81ee48e7 --- /dev/null +++ b/resources/squadrons/Su-17/SAAF 677th Sqn.yaml @@ -0,0 +1,15 @@ +--- +name: 677th Squadron +nickname: 677th +country: Syria +role: Bomber +aircraft: Su-17M4 Fitter-K +livery: "af standard (worn-out)" +mission_types: + - BAI + - CAS + - DEAD + - SEAD + - OCA/Aircraft + - OCA/Runway + - Strike diff --git a/resources/squadrons/Su-17/SAAF 685th Sqn.yaml b/resources/squadrons/Su-17/SAAF 685th Sqn.yaml new file mode 100644 index 00000000..e26abc6e --- /dev/null +++ b/resources/squadrons/Su-17/SAAF 685th Sqn.yaml @@ -0,0 +1,15 @@ +--- +name: 685th Squadron +nickname: 685th +country: Syria +role: Bomber +aircraft: Su-17M4 Fitter-K +livery: "af standard (worn-out)" +mission_types: + - BAI + - CAS + - DEAD + - SEAD + - OCA/Aircraft + - OCA/Runway + - Strike diff --git a/resources/squadrons/Su-17/SAAF 827th Sqn.yaml b/resources/squadrons/Su-17/SAAF 827th Sqn.yaml new file mode 100644 index 00000000..413b813f --- /dev/null +++ b/resources/squadrons/Su-17/SAAF 827th Sqn.yaml @@ -0,0 +1,15 @@ +--- +name: 827th Squadron +nickname: 827th +country: Syria +role: Bomber +aircraft: Su-17M4 Fitter-K +livery: "af standard (worn-out)" +mission_types: + - BAI + - CAS + - DEAD + - SEAD + - OCA/Aircraft + - OCA/Runway + - Strike diff --git a/resources/squadrons/Su-24/SAAF 819th Sqn.yaml b/resources/squadrons/Su-24/SAAF 819th Sqn.yaml new file mode 100644 index 00000000..3e539d1d --- /dev/null +++ b/resources/squadrons/Su-24/SAAF 819th Sqn.yaml @@ -0,0 +1,15 @@ +--- +name: 819th Squadron +nickname: 819th +country: Syria +role: Bomber +aircraft: Su-24M Fencer-D +livery: "Syrian Air Force" +mission_types: + - BAI + - CAS + - DEAD + - SEAD + - OCA/Aircraft + - OCA/Runway + - Strike diff --git a/resources/squadrons/sa342/SAAF 976th Sqn.yaml b/resources/squadrons/sa342/SAAF 976th Sqn.yaml new file mode 100644 index 00000000..310a6c78 --- /dev/null +++ b/resources/squadrons/sa342/SAAF 976th Sqn.yaml @@ -0,0 +1,10 @@ +--- +name: 976th Squadron +nickname: 976th +country: Syria +role: Anti-Tank Helicopter +aircraft: SA 342L Gazelle +livery: "Syria Fictional" +mission_types: + - CAS + - BAI diff --git a/resources/squadrons/sa342/SAAF 977th Sqn.yaml b/resources/squadrons/sa342/SAAF 977th Sqn.yaml new file mode 100644 index 00000000..3089b764 --- /dev/null +++ b/resources/squadrons/sa342/SAAF 977th Sqn.yaml @@ -0,0 +1,10 @@ +--- +name: 977th Squadron +nickname: 977th +country: Syria +role: Anti-Tank Helicopter +aircraft: SA 342L Gazelle +livery: "Syria Fictional" +mission_types: + - CAS + - BAI diff --git a/resources/squadrons/sa342/SAAF 988th Sqn.yaml b/resources/squadrons/sa342/SAAF 988th Sqn.yaml new file mode 100644 index 00000000..9b19d7ee --- /dev/null +++ b/resources/squadrons/sa342/SAAF 988th Sqn.yaml @@ -0,0 +1,10 @@ +--- +name: 988th Squadron +nickname: 988th +country: Syria +role: Anti-Tank Helicopter +aircraft: SA 342M Gazelle +livery: "Syria Fictional" +mission_types: + - CAS + - BAI diff --git a/resources/squadrons/sa342/SAAF 989th Sqn.yaml b/resources/squadrons/sa342/SAAF 989th Sqn.yaml new file mode 100644 index 00000000..08f08958 --- /dev/null +++ b/resources/squadrons/sa342/SAAF 989th Sqn.yaml @@ -0,0 +1,10 @@ +--- +name: 989th Squadron +nickname: 989th +country: Syria +role: Anti-Tank Helicopter +aircraft: SA 342M Gazelle +livery: "Syria Fictional" +mission_types: + - CAS + - BAI From f63a35b1fa3059937dd9046342c6f0ea76d5fc24 Mon Sep 17 00:00:00 2001 From: Magnus Wolffelt Date: Tue, 17 Aug 2021 23:14:54 +0200 Subject: [PATCH 03/48] Use TACAN channels more selectively, use pytest (#1554) * Use TACAN channels more selectively * Increase tacan range to 126 * Use pytest and add workflow * Skip faction tests due to outdated test data * Run mypy on tests directory also * Use iterators for bands AND usages, add tests --- .github/workflows/build.yml | 5 ++ .github/workflows/test.yml | 33 ++++++++++ game/operation/operation.py | 6 +- gen/aircraft.py | 4 +- gen/airsupportgen.py | 6 +- gen/groundobjectsgen.py | 6 +- gen/tacan.py | 56 ++++++++++++++--- requirements.txt | 6 ++ tests/test_factions.py | 9 ++- tests/test_tacan.py | 117 ++++++++++++++++++++++++++++++++++++ 10 files changed, 229 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 tests/test_tacan.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4eaede79..37df907e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -36,6 +36,11 @@ jobs: run: | ./venv/scripts/activate mypy gen + + - name: mypy tests + run: | + ./venv/scripts/activate + mypy tests - name: update build number run: | diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..e4aaee13 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: [push, pull_request] + +jobs: + + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install environment + run: | + python -m venv ./venv + + - name: Install dependencies + run: | + ./venv/scripts/activate + python -m pip install -r requirements.txt + # For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead + Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force + + - name: run tests + run: | + ./venv/scripts/activate + pytest tests diff --git a/game/operation/operation.py b/game/operation/operation.py index 59952fdf..00f1d81b 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -34,7 +34,7 @@ from gen.kneeboard import KneeboardGenerator from gen.lasercoderegistry import LaserCodeRegistry from gen.naming import namegen from gen.radios import RadioFrequency, RadioRegistry -from gen.tacan import TacanRegistry +from gen.tacan import TacanRegistry, TacanUsage from gen.triggergen import TRIGGER_RADIUS_MEDIUM, TriggersGenerator from gen.visualgen import VisualGenerator from .. import db @@ -228,7 +228,9 @@ class Operation: if beacon.channel is None: logging.error(f"TACAN beacon has no channel: {beacon.callsign}") else: - cls.tacan_registry.reserve(beacon.tacan_channel) + cls.tacan_registry.reserve( + beacon.tacan_channel, TacanUsage.TransmitReceive + ) @classmethod def _create_radio_registry( diff --git a/gen/aircraft.py b/gen/aircraft.py index 59fe1c86..8c392040 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -92,7 +92,7 @@ from gen.flights.flight import ( from gen.lasercoderegistry import LaserCodeRegistry from gen.radios import RadioFrequency, RadioRegistry from gen.runways import RunwayData -from gen.tacan import TacanBand, TacanRegistry +from gen.tacan import TacanBand, TacanRegistry, TacanUsage from .airsupport import AirSupport, AwacsInfo, TankerInfo from .callsigns import callsign_for_support_unit from .flights.flightplan import ( @@ -435,7 +435,7 @@ class AircraftConflictGenerator: if isinstance(flight.flight_plan, RefuelingFlightPlan): callsign = callsign_for_support_unit(group) - tacan = self.tacan_registy.alloc_for_band(TacanBand.Y) + tacan = self.tacan_registy.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir) self.air_support.tankers.append( TankerInfo( group_name=str(group.name), diff --git a/gen/airsupportgen.py b/gen/airsupportgen.py index 44456dcc..8478eb89 100644 --- a/gen/airsupportgen.py +++ b/gen/airsupportgen.py @@ -22,7 +22,7 @@ from .conflictgen import Conflict from .flights.ai_flight_planner_db import AEWC_CAPABLE from .naming import namegen from .radios import RadioRegistry -from .tacan import TacanBand, TacanRegistry +from .tacan import TacanBand, TacanRegistry, TacanUsage if TYPE_CHECKING: from game import Game @@ -89,7 +89,9 @@ class AirSupportConflictGenerator: # TODO: Make loiter altitude a property of the unit type. alt, airspeed = self._get_tanker_params(tanker_unit_type.dcs_unit_type) freq = self.radio_registry.alloc_uhf() - tacan = self.tacan_registry.alloc_for_band(TacanBand.Y) + tacan = self.tacan_registry.alloc_for_band( + TacanBand.Y, TacanUsage.AirToAir + ) tanker_heading = Heading.from_degrees( self.conflict.red_cp.position.heading_between_point( self.conflict.blue_cp.position diff --git a/gen/groundobjectsgen.py b/gen/groundobjectsgen.py index 4efcfb92..e9184560 100644 --- a/gen/groundobjectsgen.py +++ b/gen/groundobjectsgen.py @@ -58,7 +58,7 @@ from game.unitmap import UnitMap from game.utils import Heading, feet, knots, mps from .radios import RadioFrequency, RadioRegistry from .runways import RunwayData -from .tacan import TacanBand, TacanChannel, TacanRegistry +from .tacan import TacanBand, TacanChannel, TacanRegistry, TacanUsage if TYPE_CHECKING: from game import Game @@ -377,7 +377,9 @@ class GenericCarrierGenerator(GenericGroundObjectGenerator[GenericCarrierGroundO 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 = self.tacan_registry.alloc_for_band( + TacanBand.X, TacanUsage.TransmitReceive + ) tacan_callsign = self.tacan_callsign() icls = next(self.icls_alloc) diff --git a/gen/tacan.py b/gen/tacan.py index 5e43202a..d17110b5 100644 --- a/gen/tacan.py +++ b/gen/tacan.py @@ -4,13 +4,37 @@ from enum import Enum from typing import Dict, Iterator, Set +class TacanUsage(Enum): + TransmitReceive = "transmit receive" + AirToAir = "air to air" + + class TacanBand(Enum): X = "X" Y = "Y" def range(self) -> Iterator["TacanChannel"]: """Returns an iterator over the channels in this band.""" - return (TacanChannel(x, self) for x in range(1, 100)) + return (TacanChannel(x, self) for x in range(1, 126 + 1)) + + def valid_channels(self, usage: TacanUsage) -> Iterator["TacanChannel"]: + for x in self.range(): + if x.number not in UNAVAILABLE[usage][self]: + yield x + + +# Avoid certain TACAN channels for various reasons +# https://forums.eagle.ru/topic/276390-datalink-issue/ +UNAVAILABLE = { + TacanUsage.TransmitReceive: { + TacanBand.X: set(range(2, 30 + 1)) | set(range(47, 63 + 1)), + TacanBand.Y: set(range(2, 30 + 1)) | set(range(64, 92 + 1)), + }, + TacanUsage.AirToAir: { + TacanBand.X: set(range(1, 36 + 1)) | set(range(64, 99 + 1)), + TacanBand.Y: set(range(1, 36 + 1)) | set(range(64, 99 + 1)), + }, +} @dataclass(frozen=True) @@ -36,30 +60,42 @@ class TacanChannelInUseError(RuntimeError): super().__init__(f"{channel} is already in use") +class TacanChannelForbiddenError(RuntimeError): + """Raised when attempting to reserve a, for technical reasons, forbidden channel.""" + + def __init__(self, channel: TacanChannel) -> None: + super().__init__(f"{channel} is forbidden") + + class TacanRegistry: """Manages allocation of TACAN channels.""" def __init__(self) -> None: self.allocated_channels: Set[TacanChannel] = set() - self.band_allocators: Dict[TacanBand, Iterator[TacanChannel]] = {} + self.allocators: Dict[TacanBand, Dict[TacanUsage, Iterator[TacanChannel]]] = {} for band in TacanBand: - self.band_allocators[band] = band.range() + self.allocators[band] = {} + for usage in TacanUsage: + self.allocators[band][usage] = band.valid_channels(usage) - def alloc_for_band(self, band: TacanBand) -> TacanChannel: + def alloc_for_band( + self, band: TacanBand, intended_usage: TacanUsage + ) -> TacanChannel: """Allocates a TACAN channel in the given band. Args: band: The TACAN band to allocate a channel for. + intended_usage: What the caller intends to use the tacan channel for. Returns: A TACAN channel in the given band. Raises: - OutOfChannelsError: All channels compatible with the given radio are + OutOfTacanChannelsError: All channels compatible with the given radio are already allocated. """ - allocator = self.band_allocators[band] + allocator = self.allocators[band][intended_usage] try: while (channel := next(allocator)) in self.allocated_channels: pass @@ -67,17 +103,21 @@ class TacanRegistry: except StopIteration: raise OutOfTacanChannelsError(band) - def reserve(self, channel: TacanChannel) -> None: + def reserve(self, channel: TacanChannel, intended_usage: TacanUsage) -> None: """Reserves the given channel. Reserving a channel ensures that it will not be allocated in the future. Args: channel: The channel to reserve. + intended_usage: What the caller intends to use the tacan channel for. Raises: - ChannelInUseError: The given frequency is already in use. + TacanChannelInUseError: The given channel is already in use. + TacanChannelForbiddenError: The given channel is forbidden. """ + if channel.number in UNAVAILABLE[intended_usage][channel.band]: + raise TacanChannelForbiddenError(channel) if channel in self.allocated_channels: raise TacanChannelInUseError(channel) self.allocated_channels.add(channel) diff --git a/requirements.txt b/requirements.txt index e42f5a80..a313c419 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ altgraph==0.17 appdirs==1.4.4 +attrs==21.2.0 black==21.4b0 certifi==2020.12.5 cfgv==3.2.0 @@ -9,7 +10,9 @@ Faker==8.2.1 filelock==3.0.12 future==0.18.2 identify==1.5.13 +iniconfig==1.1.1 Jinja2==2.11.3 +macholib==1.14 MarkupSafe==1.1.1 mypy==0.812 mypy-extensions==0.4.3 @@ -18,13 +21,16 @@ packaging==20.9 pathspec==0.8.1 pefile==2019.4.18 Pillow==8.2.0 +pluggy==0.13.1 pre-commit==2.10.1 +py==1.10.0 -e git://github.com/pydcs/dcs@eb0b9f2de660393ccd6ba17b2d82371d44e0d27b#egg=pydcs pyinstaller==4.3 pyinstaller-hooks-contrib==2021.1 pyparsing==2.4.7 pyproj==3.0.1 PySide2==5.15.2 +pytest==6.2.4 python-dateutil==2.8.1 pywin32-ctypes==0.2.0 PyYAML==5.4.1 diff --git a/tests/test_factions.py b/tests/test_factions.py index 677532c5..8b598c02 100644 --- a/tests/test_factions.py +++ b/tests/test_factions.py @@ -1,6 +1,7 @@ import json from pathlib import Path import unittest +import pytest from dcs.helicopters import UH_1H, AH_64A from dcs.planes import ( @@ -39,10 +40,11 @@ RESOURCES_DIR = THIS_DIR / "resources" class TestFactionLoader(unittest.TestCase): - def setUp(self): + def setUp(self) -> None: pass - def test_load_valid_faction(self): + @pytest.mark.skip(reason="Faction unit names in the json files are outdated") + def test_load_valid_faction(self) -> None: with (RESOURCES_DIR / "valid_faction.json").open("r") as data: faction = Faction.from_json(json.load(data)) @@ -112,7 +114,8 @@ class TestFactionLoader(unittest.TestCase): self.assertIn("OliverHazardPerryGroupGenerator", faction.navy_generators) self.assertIn("ArleighBurkeGroupGenerator", faction.navy_generators) - def test_load_valid_faction_with_invalid_country(self): + @pytest.mark.skip(reason="Faction unit names in the json files are outdated") + def test_load_valid_faction_with_invalid_country(self) -> None: with (RESOURCES_DIR / "invalid_faction_country.json").open("r") as data: try: diff --git a/tests/test_tacan.py b/tests/test_tacan.py new file mode 100644 index 00000000..216cac58 --- /dev/null +++ b/tests/test_tacan.py @@ -0,0 +1,117 @@ +from gen.tacan import ( + OutOfTacanChannelsError, + TacanBand, + TacanChannel, + TacanChannelForbiddenError, + TacanChannelInUseError, + TacanRegistry, + TacanUsage, +) +import pytest + + +ALL_VALID_X_TR = [1, *range(31, 46 + 1), *range(64, 126 + 1)] +ALL_VALID_X_A2A = [*range(37, 63 + 1), *range(100, 126 + 1)] + + +def test_allocate_first_few_channels() -> None: + registry = TacanRegistry() + chan1 = registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) + chan2 = registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) + chan3 = registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) + assert chan1 == TacanChannel(1, TacanBand.X) + assert chan2 == TacanChannel(31, TacanBand.X) + assert chan3 == TacanChannel(32, TacanBand.X) + + +def test_allocate_different_usages() -> None: + """Make sure unallocated channels for one use don't make channels unavailable for other usage""" + registry = TacanRegistry() + + chanA2AX = registry.alloc_for_band(TacanBand.X, TacanUsage.AirToAir) + chanA2AY = registry.alloc_for_band(TacanBand.Y, TacanUsage.AirToAir) + assert chanA2AX == TacanChannel(37, TacanBand.X) + assert chanA2AY == TacanChannel(37, TacanBand.Y) + + chanTRX = registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) + chanTRY = registry.alloc_for_band(TacanBand.Y, TacanUsage.TransmitReceive) + assert chanTRX == TacanChannel(1, TacanBand.X) + assert chanTRY == TacanChannel(1, TacanBand.Y) + + +def test_reserve_all_valid_transmit_receive() -> None: + registry = TacanRegistry() + print("All valid x", ALL_VALID_X_TR) + + for num in ALL_VALID_X_TR: + registry.reserve(TacanChannel(num, TacanBand.X), TacanUsage.TransmitReceive) + + with pytest.raises(OutOfTacanChannelsError): + registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) + + # Check that we still can allocate an a2a channel even + # though the T/R channels are used up + chanA2A = registry.alloc_for_band(TacanBand.X, TacanUsage.AirToAir) + assert chanA2A == TacanChannel(47, TacanBand.X) + + +def test_reserve_all_valid_a2a() -> None: + registry = TacanRegistry() + print("All valid x", ALL_VALID_X_A2A) + + for num in ALL_VALID_X_A2A: + registry.reserve(TacanChannel(num, TacanBand.X), TacanUsage.AirToAir) + + with pytest.raises(OutOfTacanChannelsError): + registry.alloc_for_band(TacanBand.X, TacanUsage.AirToAir) + + # Check that we still can allocate an a2a channel even + # though the T/R channels are used up + chanTR = registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) + assert chanTR == TacanChannel(1, TacanBand.X) + + +@pytest.mark.skip(reason="TODO") +def test_allocate_all() -> None: + pass + + +def test_reserve_invalid_tr_channels() -> None: + registry = TacanRegistry() + some_invalid_channels = [ + TacanChannel(2, TacanBand.X), + TacanChannel(30, TacanBand.X), + TacanChannel(47, TacanBand.X), + TacanChannel(63, TacanBand.X), + TacanChannel(2, TacanBand.Y), + TacanChannel(30, TacanBand.Y), + TacanChannel(64, TacanBand.Y), + TacanChannel(92, TacanBand.Y), + ] + for chan in some_invalid_channels: + with pytest.raises(TacanChannelForbiddenError): + registry.reserve(chan, TacanUsage.TransmitReceive) + + +def test_reserve_invalid_a2a_channels() -> None: + registry = TacanRegistry() + some_invalid_channels = [ + TacanChannel(1, TacanBand.X), + TacanChannel(36, TacanBand.X), + TacanChannel(64, TacanBand.X), + TacanChannel(99, TacanBand.X), + TacanChannel(1, TacanBand.Y), + TacanChannel(36, TacanBand.Y), + TacanChannel(64, TacanBand.Y), + TacanChannel(99, TacanBand.Y), + ] + for chan in some_invalid_channels: + with pytest.raises(TacanChannelForbiddenError): + registry.reserve(chan, TacanUsage.AirToAir) + + +def test_reserve_again() -> None: + registry = TacanRegistry() + with pytest.raises(TacanChannelInUseError): + registry.reserve(TacanChannel(1, TacanBand.X), TacanUsage.TransmitReceive) + registry.reserve(TacanChannel(1, TacanBand.X), TacanUsage.TransmitReceive) From 34ff5fbc6a9681c58dcc5d359614f793a696a1d9 Mon Sep 17 00:00:00 2001 From: Magnus Wolffelt Date: Wed, 18 Aug 2021 00:06:34 +0200 Subject: [PATCH 04/48] Allow operation.py to ignore TACAN rules --- game/operation/operation.py | 4 +++- gen/tacan.py | 13 ++++++++++--- tests/test_tacan.py | 9 +++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index 00f1d81b..b717a775 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -229,7 +229,9 @@ class Operation: logging.error(f"TACAN beacon has no channel: {beacon.callsign}") else: cls.tacan_registry.reserve( - beacon.tacan_channel, TacanUsage.TransmitReceive + beacon.tacan_channel, + TacanUsage.TransmitReceive, + ignore_rules=True, ) @classmethod diff --git a/gen/tacan.py b/gen/tacan.py index d17110b5..127ad86b 100644 --- a/gen/tacan.py +++ b/gen/tacan.py @@ -103,7 +103,12 @@ class TacanRegistry: except StopIteration: raise OutOfTacanChannelsError(band) - def reserve(self, channel: TacanChannel, intended_usage: TacanUsage) -> None: + def reserve( + self, + channel: TacanChannel, + intended_usage: TacanUsage, + ignore_rules: bool = False, + ) -> None: """Reserves the given channel. Reserving a channel ensures that it will not be allocated in the future. @@ -111,13 +116,15 @@ class TacanRegistry: Args: channel: The channel to reserve. intended_usage: What the caller intends to use the tacan channel for. + ignore_rules: Whether to reserve regardless of recommended rules. Raises: TacanChannelInUseError: The given channel is already in use. TacanChannelForbiddenError: The given channel is forbidden. """ - if channel.number in UNAVAILABLE[intended_usage][channel.band]: - raise TacanChannelForbiddenError(channel) + if not ignore_rules: + if channel.number in UNAVAILABLE[intended_usage][channel.band]: + raise TacanChannelForbiddenError(channel) if channel in self.allocated_channels: raise TacanChannelInUseError(channel) self.allocated_channels.add(channel) diff --git a/tests/test_tacan.py b/tests/test_tacan.py index 216cac58..92c11f49 100644 --- a/tests/test_tacan.py +++ b/tests/test_tacan.py @@ -24,6 +24,15 @@ def test_allocate_first_few_channels() -> None: assert chan3 == TacanChannel(32, TacanBand.X) +def test_reserve_ignoring_rules() -> None: + registry = TacanRegistry() + with pytest.raises(TacanChannelForbiddenError): + registry.reserve(TacanChannel(16, TacanBand.X), TacanUsage.TransmitReceive) + registry.reserve( + TacanChannel(16, TacanBand.X), TacanUsage.TransmitReceive, ignore_rules=True + ) + + def test_allocate_different_usages() -> None: """Make sure unallocated channels for one use don't make channels unavailable for other usage""" registry = TacanRegistry() From 0cb10e422422db377bcdfa95f2ed86b3a8420cba Mon Sep 17 00:00:00 2001 From: Nils Heiden <14958657+UKayeF@users.noreply.github.com> Date: Wed, 18 Aug 2021 02:00:16 +0200 Subject: [PATCH 05/48] Make the Mi-24 LHA capable. --- resources/units/aircraft/Mi-24P.yaml | 1 + resources/units/aircraft/Mi-24V.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/resources/units/aircraft/Mi-24P.yaml b/resources/units/aircraft/Mi-24P.yaml index 355619c0..4510182a 100644 --- a/resources/units/aircraft/Mi-24P.yaml +++ b/resources/units/aircraft/Mi-24P.yaml @@ -13,6 +13,7 @@ description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; \ Stakan), because of the flat glass plates that surround earlier Mi-24 variants'\ \ cockpits. It served to a great success in the Afghanistan war, until the Taliban\ \ where equipped with Stinger Missiles from the CIA." +lha_capable: true introduced: 1981 manufacturer: Mil origin: USSR/Russia diff --git a/resources/units/aircraft/Mi-24V.yaml b/resources/units/aircraft/Mi-24V.yaml index 8d1ed18f..7d6feb85 100644 --- a/resources/units/aircraft/Mi-24V.yaml +++ b/resources/units/aircraft/Mi-24V.yaml @@ -13,6 +13,7 @@ description: "The Mil Mi-24 (Russian: \u041C\u0438\u043B\u044C \u041C\u0438-24; \ Stakan), because of the flat glass plates that surround earlier Mi-24 variants'\ \ cockpits. It served to a great success in the Afghanistan war, until the Taliban\ \ where equiped with Stinger Misseles from the CIA." +lha_capable: true introduced: 1976 manufacturer: Mil origin: USSR/Russia From 056e6b28dabfc2c386de801dee44a7967a4f5016 Mon Sep 17 00:00:00 2001 From: Magnus Wolffelt Date: Wed, 18 Aug 2021 14:46:55 +0200 Subject: [PATCH 06/48] Simplify and rename TACAN registry reserve function (#1559) * Simplify and rename TACAN registry reserve function * Remove unused tacan error --- game/operation/operation.py | 6 +--- gen/tacan.py | 20 +------------ tests/test_tacan.py | 57 +++---------------------------------- 3 files changed, 6 insertions(+), 77 deletions(-) diff --git a/game/operation/operation.py b/game/operation/operation.py index b717a775..29ce0532 100644 --- a/game/operation/operation.py +++ b/game/operation/operation.py @@ -228,11 +228,7 @@ class Operation: if beacon.channel is None: logging.error(f"TACAN beacon has no channel: {beacon.callsign}") else: - cls.tacan_registry.reserve( - beacon.tacan_channel, - TacanUsage.TransmitReceive, - ignore_rules=True, - ) + cls.tacan_registry.mark_unavailable(beacon.tacan_channel) @classmethod def _create_radio_registry( diff --git a/gen/tacan.py b/gen/tacan.py index 127ad86b..96683be7 100644 --- a/gen/tacan.py +++ b/gen/tacan.py @@ -60,13 +60,6 @@ class TacanChannelInUseError(RuntimeError): super().__init__(f"{channel} is already in use") -class TacanChannelForbiddenError(RuntimeError): - """Raised when attempting to reserve a, for technical reasons, forbidden channel.""" - - def __init__(self, channel: TacanChannel) -> None: - super().__init__(f"{channel} is forbidden") - - class TacanRegistry: """Manages allocation of TACAN channels.""" @@ -103,28 +96,17 @@ class TacanRegistry: except StopIteration: raise OutOfTacanChannelsError(band) - def reserve( - self, - channel: TacanChannel, - intended_usage: TacanUsage, - ignore_rules: bool = False, - ) -> None: + def mark_unavailable(self, channel: TacanChannel) -> None: """Reserves the given channel. Reserving a channel ensures that it will not be allocated in the future. Args: channel: The channel to reserve. - intended_usage: What the caller intends to use the tacan channel for. - ignore_rules: Whether to reserve regardless of recommended rules. Raises: TacanChannelInUseError: The given channel is already in use. - TacanChannelForbiddenError: The given channel is forbidden. """ - if not ignore_rules: - if channel.number in UNAVAILABLE[intended_usage][channel.band]: - raise TacanChannelForbiddenError(channel) if channel in self.allocated_channels: raise TacanChannelInUseError(channel) self.allocated_channels.add(channel) diff --git a/tests/test_tacan.py b/tests/test_tacan.py index 92c11f49..d1afa346 100644 --- a/tests/test_tacan.py +++ b/tests/test_tacan.py @@ -2,7 +2,6 @@ from gen.tacan import ( OutOfTacanChannelsError, TacanBand, TacanChannel, - TacanChannelForbiddenError, TacanChannelInUseError, TacanRegistry, TacanUsage, @@ -24,15 +23,6 @@ def test_allocate_first_few_channels() -> None: assert chan3 == TacanChannel(32, TacanBand.X) -def test_reserve_ignoring_rules() -> None: - registry = TacanRegistry() - with pytest.raises(TacanChannelForbiddenError): - registry.reserve(TacanChannel(16, TacanBand.X), TacanUsage.TransmitReceive) - registry.reserve( - TacanChannel(16, TacanBand.X), TacanUsage.TransmitReceive, ignore_rules=True - ) - - def test_allocate_different_usages() -> None: """Make sure unallocated channels for one use don't make channels unavailable for other usage""" registry = TacanRegistry() @@ -53,7 +43,7 @@ def test_reserve_all_valid_transmit_receive() -> None: print("All valid x", ALL_VALID_X_TR) for num in ALL_VALID_X_TR: - registry.reserve(TacanChannel(num, TacanBand.X), TacanUsage.TransmitReceive) + registry.mark_unavailable(TacanChannel(num, TacanBand.X)) with pytest.raises(OutOfTacanChannelsError): registry.alloc_for_band(TacanBand.X, TacanUsage.TransmitReceive) @@ -69,7 +59,7 @@ def test_reserve_all_valid_a2a() -> None: print("All valid x", ALL_VALID_X_A2A) for num in ALL_VALID_X_A2A: - registry.reserve(TacanChannel(num, TacanBand.X), TacanUsage.AirToAir) + registry.mark_unavailable(TacanChannel(num, TacanBand.X)) with pytest.raises(OutOfTacanChannelsError): registry.alloc_for_band(TacanBand.X, TacanUsage.AirToAir) @@ -80,47 +70,8 @@ def test_reserve_all_valid_a2a() -> None: assert chanTR == TacanChannel(1, TacanBand.X) -@pytest.mark.skip(reason="TODO") -def test_allocate_all() -> None: - pass - - -def test_reserve_invalid_tr_channels() -> None: - registry = TacanRegistry() - some_invalid_channels = [ - TacanChannel(2, TacanBand.X), - TacanChannel(30, TacanBand.X), - TacanChannel(47, TacanBand.X), - TacanChannel(63, TacanBand.X), - TacanChannel(2, TacanBand.Y), - TacanChannel(30, TacanBand.Y), - TacanChannel(64, TacanBand.Y), - TacanChannel(92, TacanBand.Y), - ] - for chan in some_invalid_channels: - with pytest.raises(TacanChannelForbiddenError): - registry.reserve(chan, TacanUsage.TransmitReceive) - - -def test_reserve_invalid_a2a_channels() -> None: - registry = TacanRegistry() - some_invalid_channels = [ - TacanChannel(1, TacanBand.X), - TacanChannel(36, TacanBand.X), - TacanChannel(64, TacanBand.X), - TacanChannel(99, TacanBand.X), - TacanChannel(1, TacanBand.Y), - TacanChannel(36, TacanBand.Y), - TacanChannel(64, TacanBand.Y), - TacanChannel(99, TacanBand.Y), - ] - for chan in some_invalid_channels: - with pytest.raises(TacanChannelForbiddenError): - registry.reserve(chan, TacanUsage.AirToAir) - - def test_reserve_again() -> None: registry = TacanRegistry() with pytest.raises(TacanChannelInUseError): - registry.reserve(TacanChannel(1, TacanBand.X), TacanUsage.TransmitReceive) - registry.reserve(TacanChannel(1, TacanBand.X), TacanUsage.TransmitReceive) + registry.mark_unavailable(TacanChannel(1, TacanBand.X)) + registry.mark_unavailable(TacanChannel(1, TacanBand.X)) From 74577752e0b93afc19cbdb5f558deec68ed71384 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 26 Aug 2021 23:13:52 -0700 Subject: [PATCH 07/48] Update pydcs for the Viper CBU-105. --- changelog.md | 1 + requirements.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.md b/changelog.md index 8b01de88..e3a80639 100644 --- a/changelog.md +++ b/changelog.md @@ -15,6 +15,7 @@ Saves from 4.x are not compatible with 5.0. * **[Campaign AI]** Auto-planning mission range limits are now specified per-aircraft. On average this means that longer range missions will now be plannable. The limit only accounts for the direct distance to the target, not the path taken. * **[Campaign AI]** Transport aircraft will now be bought only if necessary at control points which can produce ground units and are capable to operate transport aircraft. * **[Campaign AI]** Aircraft will now only be automatically purchased or assigned at appropriate bases. Naval aircraft will default to only operating from carriers, Harriers will default to LHAs and shore bases, helicopters will operate from anywhere. This can be customized per-squadron. +* **[Engine]** Support for DCS 2.7.5.10869 and newer, including support for F-16 CBU-105s. * **[Mission Generation]** EWRs are now also headed towards the center of the conflict * **[Modding]** Campaigns now specify the squadrons that are present in the campaign, their roles, and their starting bases. Players can customize this at game start but the campaign will choose the defaults. * **[Kneeboard]** Minimum required fuel estimates have been added to the kneeboard for aircraft with supporting data (currently only the Hornet). diff --git a/requirements.txt b/requirements.txt index a313c419..83599356 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ Pillow==8.2.0 pluggy==0.13.1 pre-commit==2.10.1 py==1.10.0 --e git://github.com/pydcs/dcs@eb0b9f2de660393ccd6ba17b2d82371d44e0d27b#egg=pydcs +-e git://github.com/pydcs/dcs@5ec61b22a174ad8ddc762958998868db3150c947#egg=pydcs pyinstaller==4.3 pyinstaller-hooks-contrib==2021.1 pyparsing==2.4.7 From 4715773bba3807a81f54a017d07cb259d6fec6f2 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 28 Aug 2021 16:39:20 -0700 Subject: [PATCH 08/48] Store the owning coalition in ControlPoint. This is needed fairly often, and we have a lot of Game being passed around to ControlPoint methods specifically to support this. Just store the owning Coalition directly in the ControlPoint to clean up. I haven't cleaned up *every* API here, but did that aircraft allocations as an example. --- game/campaignloader/mizcampaignloader.py | 27 +++++---- game/commander/objectivefinder.py | 5 +- game/game.py | 3 + game/procurement.py | 4 +- game/purchaseadapter.py | 12 +--- game/theater/controlpoint.py | 59 ++++++++++++------- game/theater/start_generator.py | 21 +------ game/transfers.py | 2 +- qt_ui/main.py | 1 - qt_ui/windows/basemenu/QBaseMenu2.py | 2 +- qt_ui/windows/basemenu/QBaseMenuTabs.py | 4 +- .../airfield/QAircraftRecruitmentMenu.py | 9 +-- qt_ui/windows/basemenu/intel/QIntelInfo.py | 6 +- qt_ui/windows/intel.py | 2 +- qt_ui/windows/newgame/QNewGameWizard.py | 1 - 15 files changed, 75 insertions(+), 83 deletions(-) diff --git a/game/campaignloader/mizcampaignloader.py b/game/campaignloader/mizcampaignloader.py index d217250f..29dec2ff 100644 --- a/game/campaignloader/mizcampaignloader.py +++ b/game/campaignloader/mizcampaignloader.py @@ -105,8 +105,7 @@ class MizCampaignLoader: @staticmethod def control_point_from_airport(airport: Airport) -> ControlPoint: - cp = Airfield(airport) - cp.captured = airport.is_blue() + cp = Airfield(airport, starts_blue=airport.is_blue()) # Use the unlimited aircraft option to determine if an airfield should # be owned by the player when the campaign is "inverted". @@ -249,30 +248,38 @@ class MizCampaignLoader: for blue in (False, True): for group in self.off_map_spawns(blue): control_point = OffMapSpawn( - next(self.control_point_id), str(group.name), group.position + next(self.control_point_id), + str(group.name), + group.position, + starts_blue=blue, ) - control_point.captured = blue control_point.captured_invert = group.late_activation control_points[control_point.id] = control_point for ship in self.carriers(blue): control_point = Carrier( - ship.name, ship.position, next(self.control_point_id) + ship.name, + ship.position, + next(self.control_point_id), + starts_blue=blue, ) - control_point.captured = blue control_point.captured_invert = ship.late_activation control_points[control_point.id] = control_point for ship in self.lhas(blue): control_point = Lha( - ship.name, ship.position, next(self.control_point_id) + ship.name, + ship.position, + next(self.control_point_id), + starts_blue=blue, ) - control_point.captured = blue control_point.captured_invert = ship.late_activation control_points[control_point.id] = control_point for fob in self.fobs(blue): control_point = Fob( - str(fob.name), fob.position, next(self.control_point_id) + str(fob.name), + fob.position, + next(self.control_point_id), + starts_blue=blue, ) - control_point.captured = blue control_point.captured_invert = fob.late_activation control_points[control_point.id] = control_point diff --git a/game/commander/objectivefinder.py b/game/commander/objectivefinder.py index e684b98e..21fdd6f5 100644 --- a/game/commander/objectivefinder.py +++ b/game/commander/objectivefinder.py @@ -157,10 +157,7 @@ class ObjectiveFinder: for control_point in self.enemy_control_points(): if not isinstance(control_point, Airfield): continue - if ( - control_point.allocated_aircraft(self.game).total_present - >= min_aircraft - ): + if control_point.allocated_aircraft().total_present >= min_aircraft: airfields.append(control_point) return self._targets_by_range(airfields) diff --git a/game/game.py b/game/game.py index 2da28c2a..6d059692 100644 --- a/game/game.py +++ b/game/game.py @@ -123,6 +123,9 @@ class Game: self.blue.set_opponent(self.red) self.red.set_opponent(self.blue) + for control_point in self.theater.controlpoints: + control_point.finish_init(self) + self.blue.configure_default_air_wing(air_wing_config) self.red.configure_default_air_wing(air_wing_config) diff --git a/game/procurement.py b/game/procurement.py index 46048170..094e78c7 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -74,7 +74,7 @@ class ProcurementAi: self.game.coalition_for(self.is_player).transfers ) armor_investment += cp_ground_units.total_value - cp_aircraft = cp.allocated_aircraft(self.game) + cp_aircraft = cp.allocated_aircraft() aircraft_investment += cp_aircraft.total_value total_investment = aircraft_investment + armor_investment @@ -252,7 +252,7 @@ class ProcurementAi: for cp in distance_cache.operational_airfields: if not cp.is_friendly(self.is_player): continue - if cp.unclaimed_parking(self.game) < request.number: + if cp.unclaimed_parking() < request.number: continue if self.threat_zones.threatened(cp.position): threatened.append(cp) diff --git a/game/purchaseadapter.py b/game/purchaseadapter.py index 6376f15c..7da8e4e9 100644 --- a/game/purchaseadapter.py +++ b/game/purchaseadapter.py @@ -92,12 +92,9 @@ class PurchaseAdapter(Generic[ItemType]): class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]): - def __init__( - self, control_point: ControlPoint, coalition: Coalition, game: Game - ) -> None: - super().__init__(coalition) + def __init__(self, control_point: ControlPoint) -> None: + super().__init__(control_point.coalition) self.control_point = control_point - self.game = game def pending_delivery_quantity(self, item: Squadron) -> int: return item.pending_deliveries @@ -106,10 +103,7 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]): return item.owned_aircraft def can_buy(self, item: Squadron) -> bool: - return ( - super().can_buy(item) - and self.control_point.unclaimed_parking(self.game) > 0 - ) + return super().can_buy(item) and self.control_point.unclaimed_parking() > 0 def can_sell(self, item: Squadron) -> bool: return item.untasked_aircraft > 0 diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 8fe4dbb5..ef7aa594 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -55,6 +55,7 @@ if TYPE_CHECKING: from game import Game from gen.flights.flight import FlightType from game.squadrons.squadron import Squadron + from ..coalition import Coalition from ..transfers import PendingTransfers FREE_FRONTLINE_UNIT_SUPPLY: int = 15 @@ -280,7 +281,6 @@ class ControlPoint(MissionTarget, ABC): position = None # type: Point name = None # type: str - captured = False has_frontline = True alt = 0 @@ -294,6 +294,7 @@ class ControlPoint(MissionTarget, ABC): name: str, position: Point, at: db.StartingPosition, + starts_blue: bool, has_frontline: bool = True, cptype: ControlPointType = ControlPointType.AIRBASE, ) -> None: @@ -302,11 +303,12 @@ class ControlPoint(MissionTarget, ABC): self.id = cp_id self.full_name = name self.at = at + self.starts_blue = starts_blue self.connected_objectives: List[TheaterGroundObject[Any]] = [] self.preset_locations = PresetLocations() self.helipads: List[PointWithHeading] = [] - self.captured = False + self._coalition: Optional[Coalition] = None self.captured_invert = False # TODO: Should be Airbase specific. self.has_frontline = has_frontline @@ -328,6 +330,20 @@ class ControlPoint(MissionTarget, ABC): def __repr__(self) -> str: return f"<{self.__class__}: {self.name}>" + @property + def coalition(self) -> Coalition: + if self._coalition is None: + raise RuntimeError("ControlPoint not fully initialized: coalition not set") + return self._coalition + + def finish_init(self, game: Game) -> None: + assert self._coalition is None + self._coalition = game.coalition_for(self.starts_blue) + + @property + def captured(self) -> bool: + return self.coalition.player + @property def ground_objects(self) -> List[TheaterGroundObject[Any]]: return list(self.connected_objectives) @@ -561,7 +577,7 @@ class ControlPoint(MissionTarget, ABC): ) def aircraft_retreat_destination( - self, game: Game, airframe: AircraftType + self, airframe: AircraftType ) -> Optional[ControlPoint]: closest = ObjectiveDistanceCache.get_closest_airfields(self) # TODO: Should be airframe dependent. @@ -574,7 +590,7 @@ class ControlPoint(MissionTarget, ABC): continue if airbase.captured != self.captured: continue - if airbase.unclaimed_parking(game) > 0: + if airbase.unclaimed_parking() > 0: return airbase return None @@ -594,27 +610,23 @@ class ControlPoint(MissionTarget, ABC): # TODO: Should be Airbase specific. def capture(self, game: Game, for_player: bool) -> None: - coalition = game.coalition_for(for_player) - self.ground_unit_orders.refund_all(coalition) + new_coalition = game.coalition_for(for_player) + self.ground_unit_orders.refund_all(self.coalition) for squadron in self.squadrons: squadron.refund_orders() self.retreat_ground_units(game) self.retreat_air_units(game) self.depopulate_uncapturable_tgos() - if for_player: - self.captured = True - else: - self.captured = False - + self._coalition = new_coalition self.base.set_strength_to_minimum() @abstractmethod def can_operate(self, aircraft: AircraftType) -> bool: ... - def unclaimed_parking(self, game: Game) -> int: - return self.total_aircraft_parking - self.allocated_aircraft(game).total + def unclaimed_parking(self) -> int: + return self.total_aircraft_parking - self.allocated_aircraft().total @abstractmethod def active_runway( @@ -666,7 +678,7 @@ class ControlPoint(MissionTarget, ABC): u.position.x = u.position.x + delta.x u.position.y = u.position.y + delta.y - def allocated_aircraft(self, _game: Game) -> AircraftAllocations: + def allocated_aircraft(self) -> AircraftAllocations: present: dict[AircraftType, int] = defaultdict(int) on_order: dict[AircraftType, int] = defaultdict(int) for squadron in self.squadrons: @@ -771,13 +783,14 @@ class ControlPoint(MissionTarget, ABC): class Airfield(ControlPoint): - def __init__(self, airport: Airport, has_frontline: bool = True) -> None: + def __init__(self, airport: Airport, starts_blue: bool) -> None: super().__init__( airport.id, airport.name, airport.position, airport, - has_frontline, + starts_blue, + has_frontline=True, cptype=ControlPointType.AIRBASE, ) self.airport = airport @@ -941,12 +954,13 @@ class NavalControlPoint(ControlPoint, ABC): class Carrier(NavalControlPoint): - def __init__(self, name: str, at: Point, cp_id: int): + def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool): super().__init__( cp_id, name, at, at, + starts_blue, has_frontline=False, cptype=ControlPointType.AIRCRAFT_CARRIER_GROUP, ) @@ -981,12 +995,13 @@ class Carrier(NavalControlPoint): class Lha(NavalControlPoint): - def __init__(self, name: str, at: Point, cp_id: int): + def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool): super().__init__( cp_id, name, at, at, + starts_blue, has_frontline=False, cptype=ControlPointType.LHA_GROUP, ) @@ -1014,12 +1029,13 @@ class OffMapSpawn(ControlPoint): def runway_is_operational(self) -> bool: return True - def __init__(self, cp_id: int, name: str, position: Point): + def __init__(self, cp_id: int, name: str, position: Point, starts_blue: bool): super().__init__( cp_id, name, position, - at=position, + position, + starts_blue, has_frontline=False, cptype=ControlPointType.OFF_MAP, ) @@ -1067,12 +1083,13 @@ class OffMapSpawn(ControlPoint): class Fob(ControlPoint): - def __init__(self, name: str, at: Point, cp_id: int): + def __init__(self, name: str, at: Point, cp_id: int, starts_blue: bool): super().__init__( cp_id, name, at, at, + starts_blue, has_frontline=True, cptype=ControlPointType.FOB, ) diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 1281490d..7ccc3267 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -8,8 +8,6 @@ from datetime import datetime from typing import Any, Dict, Iterable, List, Set from dcs.mapping import Point -from dcs.task import CAP, CAS, PinpointStrike -from dcs.vehicles import AirDefence from game import Game from game.factions.faction import Faction @@ -30,7 +28,6 @@ from game.theater.theatergroundobject import ( ) from game.utils import Heading from game.version import VERSION -from gen.naming import namegen from gen.coastal.coastal_group_generator import generate_coastal_group from gen.defenses.armor_group_generator import generate_armor_group from gen.fleet.ship_group_generator import ( @@ -39,6 +36,7 @@ from gen.fleet.ship_group_generator import ( generate_ship_group, ) from gen.missiles.missiles_group_generator import generate_missile_group +from gen.naming import namegen from gen.sam.airdefensegroupgenerator import AirDefenseRange from gen.sam.ewr_group_generator import generate_ewr_group from gen.sam.sam_group_generator import generate_anti_air_group @@ -61,7 +59,6 @@ class GeneratorSettings: start_date: datetime player_budget: int enemy_budget: int - midgame: bool inverted: bool no_carrier: bool no_lha: bool @@ -121,11 +118,6 @@ class GameGenerator: def prepare_theater(self) -> None: to_remove: List[ControlPoint] = [] - # Auto-capture half the bases if midgame. - if self.generator_settings.midgame: - control_points = self.theater.controlpoints - for control_point in control_points[: len(control_points) // 2]: - control_point.captured = True # Remove carrier and lha, invert situation if needed for cp in self.theater.controlpoints: @@ -135,21 +127,12 @@ class GameGenerator: to_remove.append(cp) if self.generator_settings.inverted: - cp.captured = cp.captured_invert + cp.starts_blue = cp.captured_invert # do remove for cp in to_remove: self.theater.controlpoints.remove(cp) - # TODO: Fix this. This captures all bases for blue. - # reapply midgame inverted if needed - if self.generator_settings.midgame and self.generator_settings.inverted: - for i, cp in enumerate(reversed(self.theater.controlpoints)): - if i > len(self.theater.controlpoints): - break - else: - cp.captured = True - class ControlPointGroundObjectGenerator: def __init__( diff --git a/game/transfers.py b/game/transfers.py index 3a8d62f6..d7db80af 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -752,7 +752,7 @@ class PendingTransfers: ) def order_airlift_assets_at(self, control_point: ControlPoint) -> None: - unclaimed_parking = control_point.unclaimed_parking(self.game) + unclaimed_parking = control_point.unclaimed_parking() # Buy a maximum of unclaimed_parking only to prevent that aircraft procurement # take place at another base gap = min( diff --git a/qt_ui/main.py b/qt_ui/main.py index c4d6e4e0..ff3c6e5a 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -252,7 +252,6 @@ def create_game( start_date=start_date, player_budget=DEFAULT_BUDGET, enemy_budget=DEFAULT_BUDGET, - midgame=False, inverted=inverted, no_carrier=False, no_lha=False, diff --git a/qt_ui/windows/basemenu/QBaseMenu2.py b/qt_ui/windows/basemenu/QBaseMenu2.py index 7c333e1b..5f1d925e 100644 --- a/qt_ui/windows/basemenu/QBaseMenu2.py +++ b/qt_ui/windows/basemenu/QBaseMenu2.py @@ -190,7 +190,7 @@ class QBaseMenu2(QDialog): self.repair_button.setDisabled(True) def update_intel_summary(self) -> None: - aircraft = self.cp.allocated_aircraft(self.game_model.game).total_present + aircraft = self.cp.allocated_aircraft().total_present parking = self.cp.total_aircraft_parking ground_unit_limit = self.cp.frontline_unit_count_limit deployable_unit_info = "" diff --git a/qt_ui/windows/basemenu/QBaseMenuTabs.py b/qt_ui/windows/basemenu/QBaseMenuTabs.py index 38640f24..3ec0e403 100644 --- a/qt_ui/windows/basemenu/QBaseMenuTabs.py +++ b/qt_ui/windows/basemenu/QBaseMenuTabs.py @@ -1,6 +1,6 @@ from PySide2.QtWidgets import QTabWidget -from game.theater import ControlPoint, OffMapSpawn, Fob +from game.theater import ControlPoint, Fob from qt_ui.models import GameModel from qt_ui.windows.basemenu.DepartingConvoysMenu import DepartingConvoysMenu from qt_ui.windows.basemenu.airfield.QAirfieldCommand import QAirfieldCommand @@ -13,7 +13,7 @@ class QBaseMenuTabs(QTabWidget): super(QBaseMenuTabs, self).__init__() if not cp.captured: - self.intel = QIntelInfo(cp, game_model.game) + self.intel = QIntelInfo(cp) self.addTab(self.intel, "Intel") self.departing_convoys = DepartingConvoysMenu(cp, game_model) diff --git a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py index 99b18d5e..4d157dee 100644 --- a/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py +++ b/qt_ui/windows/basemenu/airfield/QAircraftRecruitmentMenu.py @@ -21,12 +21,7 @@ from game.purchaseadapter import AircraftPurchaseAdapter class QAircraftRecruitmentMenu(UnitTransactionFrame[Squadron]): def __init__(self, cp: ControlPoint, game_model: GameModel) -> None: - super().__init__( - game_model, - AircraftPurchaseAdapter( - cp, game_model.game.coalition_for(cp.captured), game_model.game - ), - ) + super().__init__(game_model, AircraftPurchaseAdapter(cp)) self.cp = cp self.game_model = game_model self.purchase_groups = {} @@ -96,7 +91,7 @@ class QHangarStatus(QHBoxLayout): self.setAlignment(Qt.AlignLeft) def update_label(self) -> None: - next_turn = self.control_point.allocated_aircraft(self.game_model.game) + next_turn = self.control_point.allocated_aircraft() max_amount = self.control_point.total_aircraft_parking components = [f"{next_turn.total_present} present"] diff --git a/qt_ui/windows/basemenu/intel/QIntelInfo.py b/qt_ui/windows/basemenu/intel/QIntelInfo.py index d73682db..91b13efb 100644 --- a/qt_ui/windows/basemenu/intel/QIntelInfo.py +++ b/qt_ui/windows/basemenu/intel/QIntelInfo.py @@ -11,22 +11,20 @@ from PySide2.QtWidgets import ( QWidget, ) -from game import Game from game.theater import ControlPoint class QIntelInfo(QFrame): - def __init__(self, cp: ControlPoint, game: Game): + def __init__(self, cp: ControlPoint): super(QIntelInfo, self).__init__() self.cp = cp - self.game = game layout = QVBoxLayout() scroll_content = QWidget() intel_layout = QVBoxLayout() units_by_task: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int)) - for unit_type, count in self.cp.allocated_aircraft(game).present.items(): + for unit_type, count in self.cp.allocated_aircraft().present.items(): if count: task_type = unit_type.dcs_unit_type.task_default.name units_by_task[task_type][unit_type.name] += count diff --git a/qt_ui/windows/intel.py b/qt_ui/windows/intel.py index 288b87fe..8ae11087 100644 --- a/qt_ui/windows/intel.py +++ b/qt_ui/windows/intel.py @@ -77,7 +77,7 @@ class AircraftIntelLayout(IntelTableLayout): total = 0 for control_point in game.theater.control_points_for(player): - allocation = control_point.allocated_aircraft(game) + allocation = control_point.allocated_aircraft() base_total = allocation.total_present total += base_total if not base_total: diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index 0b1f4539..c362d0c6 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -94,7 +94,6 @@ class NewGameWizard(QtWidgets.QWizard): enemy_budget=int(self.field("enemy_starting_money")), # QSlider forces integers, so we use 1 to 50 and divide by 10 to # give 0.1 to 5.0. - midgame=False, inverted=self.field("invertMap"), no_carrier=self.field("no_carrier"), no_lha=self.field("no_lha"), From 5fae17808197754b02f30a2bff8c33180b5fc494 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 28 Aug 2021 17:56:09 -0700 Subject: [PATCH 09/48] Reduce squadron location bookkeeping. --- game/squadrons/airwing.py | 1 - game/squadrons/squadron.py | 2 -- game/theater/controlpoint.py | 8 ++++++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index 74ad5f83..f174a132 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -19,7 +19,6 @@ class AirWing: self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) def add_squadron(self, squadron: Squadron) -> None: - squadron.location.squadrons.append(squadron) self.squadrons[squadron.aircraft].append(squadron) def squadrons_for(self, aircraft: AircraftType) -> Sequence[Squadron]: diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index b5169d26..993a7c07 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -82,9 +82,7 @@ class Squadron: return self.coalition.player def assign_to_base(self, base: ControlPoint) -> None: - self.location.squadrons.remove(self) self.location = base - self.location.squadrons.append(self) logging.debug(f"Assigned {self} to {base}") @property diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index ef7aa594..ecaf7e4a 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -325,8 +325,6 @@ class ControlPoint(MissionTarget, ABC): self.target_position: Optional[Point] = None - self.squadrons: list[Squadron] = [] - def __repr__(self) -> str: return f"<{self.__class__}: {self.name}>" @@ -348,6 +346,12 @@ class ControlPoint(MissionTarget, ABC): def ground_objects(self) -> List[TheaterGroundObject[Any]]: return list(self.connected_objectives) + @property + def squadrons(self) -> Iterator[Squadron]: + for squadron in self.coalition.air_wing.iter_squadrons(): + if squadron.location == self: + yield squadron + @property @abstractmethod def heading(self) -> Heading: From 469b1e5efeca8f4c4848a78d27a3ef1a94a7eceb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 28 Aug 2021 18:02:11 -0700 Subject: [PATCH 10/48] Reimplement aircraft retreats for captured bases. --- game/squadrons/squadron.py | 24 ++++++++++-- game/theater/controlpoint.py | 75 ++++++++++++++++++++++++++++-------- 2 files changed, 81 insertions(+), 18 deletions(-) diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 993a7c07..c7354c03 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -263,14 +263,32 @@ class Squadron: def can_fulfill_flight(self, count: int) -> bool: return self.can_provide_pilots(count) and self.untasked_aircraft >= count - def refund_orders(self) -> None: - self.coalition.adjust_budget(self.aircraft.price * self.pending_deliveries) - self.pending_deliveries = 0 + def refund_orders(self, count: Optional[int] = None) -> None: + if count is None: + count = self.pending_deliveries + self.coalition.adjust_budget(self.aircraft.price * count) + self.pending_deliveries -= count def deliver_orders(self) -> None: + self.cancel_overflow_orders() self.owned_aircraft += self.pending_deliveries self.pending_deliveries = 0 + def relocate_to(self, destination: ControlPoint) -> None: + self.location = destination + + def cancel_overflow_orders(self) -> None: + if self.pending_deliveries <= 0: + return + overflow = -self.location.unclaimed_parking() + if overflow > 0: + sell_count = min(overflow, self.pending_deliveries) + logging.debug( + f"{self.location} is overfull by {overflow} aircraft. Cancelling " + f"orders for {sell_count} aircraft to make room." + ) + self.refund_orders(sell_count) + @property def max_fulfillable_aircraft(self) -> int: return max(self.number_of_available_pilots, self.untasked_aircraft) diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index ecaf7e4a..0b738237 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -3,6 +3,7 @@ from __future__ import annotations import heapq import itertools import logging +import math from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass, field @@ -576,36 +577,82 @@ class ControlPoint(MissionTarget, ABC): value = airframe.price * count game.adjust_budget(value, player=not self.captured) game.message( - f"No valid retreat destination in range of {self.name} for {airframe}" + f"No valid retreat destination in range of {self.name} for {airframe} " f"{count} aircraft have been captured and sold for ${value}M." ) def aircraft_retreat_destination( - self, airframe: AircraftType + self, squadron: Squadron ) -> Optional[ControlPoint]: closest = ObjectiveDistanceCache.get_closest_airfields(self) - # TODO: Should be airframe dependent. - max_retreat_distance = nautical_miles(200) + max_retreat_distance = squadron.aircraft.max_mission_range # Skip the first airbase because that's the airbase we're retreating # from. airfields = list(closest.operational_airfields_within(max_retreat_distance))[1:] + not_preferred: Optional[ControlPoint] = None + overfull: list[ControlPoint] = [] for airbase in airfields: - if not airbase.can_operate(airframe): - continue if airbase.captured != self.captured: continue - if airbase.unclaimed_parking() > 0: - return airbase - return None - @staticmethod - def _retreat_squadron(squadron: Squadron) -> None: - logging.error("Air unit retreat not currently implemented") + if airbase.unclaimed_parking() < squadron.owned_aircraft: + if airbase.can_operate(squadron.aircraft): + overfull.append(airbase) + continue + + if squadron.operates_from(airbase): + # Has room, is a preferred base type for this squadron, and is the + # closest choice. No need to keep looking. + return airbase + + if not_preferred is None and airbase.can_operate(squadron.aircraft): + # Has room and is capable of operating from this base, but it isn't + # preferred. Remember this option and use it if we can't find a + # preferred base type with room. + not_preferred = airbase + if not_preferred is not None: + # It's not our best choice but the other choices don't have room for the + # squadron and would lead to aircraft being captured. + return not_preferred + + # No base was available with enough room. Find whichever base has the most room + # available so we lose as little as possible. The overfull list is already + # sorted by distance, and filtered for appropriate destinations. + base_for_fewest_losses: Optional[ControlPoint] = None + loss_count = math.inf + for airbase in overfull: + overflow = -( + airbase.unclaimed_parking() + - squadron.owned_aircraft + - squadron.pending_deliveries + ) + if overflow < loss_count: + loss_count = overflow + base_for_fewest_losses = airbase + return base_for_fewest_losses + + def _retreat_squadron(self, game: Game, squadron: Squadron) -> None: + destination = self.aircraft_retreat_destination(squadron) + if destination is None: + squadron.refund_orders() + self.capture_aircraft(game, squadron.aircraft, squadron.owned_aircraft) + return + logging.debug(f"{squadron} retreating to {destination} from {self}") + squadron.relocate_to(destination) + squadron.cancel_overflow_orders() + overflow = -destination.unclaimed_parking() + if overflow > 0: + logging.debug( + f"Not enough room for {squadron} at {destination}. Capturing " + f"{overflow} aircraft." + ) + self.capture_aircraft(game, squadron.aircraft, overflow) + squadron.owned_aircraft -= overflow def retreat_air_units(self, game: Game) -> None: # TODO: Capture in order of price to retain maximum value? for squadron in self.squadrons: - self._retreat_squadron(squadron) + self._retreat_squadron(game, squadron) def depopulate_uncapturable_tgos(self) -> None: for tgo in self.connected_objectives: @@ -616,8 +663,6 @@ class ControlPoint(MissionTarget, ABC): def capture(self, game: Game, for_player: bool) -> None: new_coalition = game.coalition_for(for_player) self.ground_unit_orders.refund_all(self.coalition) - for squadron in self.squadrons: - squadron.refund_orders() self.retreat_ground_units(game) self.retreat_air_units(game) self.depopulate_uncapturable_tgos() From 8fea8e7b4797a892312c44195c4f61b9c23c400b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 28 Aug 2021 18:03:33 -0700 Subject: [PATCH 11/48] Move squadron end-turn behavior into the squadron. --- game/coalition.py | 2 +- game/squadrons/airwing.py | 4 ++-- game/squadrons/squadron.py | 4 ++++ game/theater/controlpoint.py | 2 -- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/game/coalition.py b/game/coalition.py index e15d916d..4ccc9128 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -139,7 +139,7 @@ class Coalition: For more information on turn finalization in general, see the documentation for `Game.finish_turn`. """ - self.air_wing.replenish() + self.air_wing.end_turn() self.budget += Income(self.game, self.player).total # Need to recompute before transfers and deliveries to account for captures. diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index f174a132..9d01e65c 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -75,9 +75,9 @@ class AirWing: for squadron in self.iter_squadrons(): squadron.populate_for_turn_0() - def replenish(self) -> None: + def end_turn(self) -> None: for squadron in self.iter_squadrons(): - squadron.replenish_lost_pilots() + squadron.end_turn() def reset(self) -> None: for squadron in self.iter_squadrons(): diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index c7354c03..ada95795 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -167,6 +167,10 @@ class Squadron: raise ValueError("Squadrons can only be created with active pilots.") self._recruit_pilots(self.settings.squadron_pilot_limit) + def end_turn(self) -> None: + self.replenish_lost_pilots() + self.deliver_orders() + def replenish_lost_pilots(self) -> None: if not self.pilot_limits_enabled: return diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 0b738237..205a677f 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -704,8 +704,6 @@ class ControlPoint(MissionTarget, ABC): def process_turn(self, game: Game) -> None: self.ground_unit_orders.process(game) - for squadron in self.squadrons: - squadron.deliver_orders() runway_status = self.runway_status if runway_status is not None: From 380d6bf47a94d9ce115b855ed8f27db6caf90cc0 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 29 Aug 2021 02:17:10 -0700 Subject: [PATCH 12/48] Fix weird wrong default campaign field bug. I tried fixing this using setField after registering it, but it does nothing. I suspect this is because the page hasn't been registered with the wizard yet so it's setting the field for the wrong wizard. --- changelog.md | 1 + qt_ui/windows/newgame/QNewGameWizard.py | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/changelog.md b/changelog.md index e3a80639..6530da79 100644 --- a/changelog.md +++ b/changelog.md @@ -28,6 +28,7 @@ Saves from 4.x are not compatible with 5.0. * **[Campaign]** Naval control points will no longer claim ground objectives during campaign generation and prevent them from spawning. * **[Mission Generation]** Mission results and other files will now be opened with enforced utf-8 encoding to prevent an issue where destroyed ground units were untracked because of special characters in their names. * **[UI]** Selling of Units is now visible again in the UI dialog and shows the correct amount of sold units +* **[UI]** Fixed bug where an incompatible campaign could be generated if no action is taken on the campaign selection screen. # 4.1.1 diff --git a/qt_ui/windows/newgame/QNewGameWizard.py b/qt_ui/windows/newgame/QNewGameWizard.py index c362d0c6..40a25649 100644 --- a/qt_ui/windows/newgame/QNewGameWizard.py +++ b/qt_ui/windows/newgame/QNewGameWizard.py @@ -65,6 +65,8 @@ class NewGameWizard(QtWidgets.QWizard): logging.info("======================") campaign = self.field("selectedCampaign") + if campaign is None: + campaign = self.theater_page.campaignList.selected_campaign if campaign is None: campaign = self.campaigns[0] @@ -299,13 +301,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage): text="Show incompatible campaigns" ) show_incompatible_campaigns_checkbox.setChecked(False) - campaignList = QCampaignList( + self.campaignList = QCampaignList( campaigns, show_incompatible_campaigns_checkbox.isChecked() ) show_incompatible_campaigns_checkbox.toggled.connect( - lambda checked: campaignList.setup_content(show_incompatible=checked) + lambda checked: self.campaignList.setup_content(show_incompatible=checked) ) - self.registerField("selectedCampaign", campaignList) + self.registerField("selectedCampaign", self.campaignList) # Faction description self.campaignMapDescription = QTextEdit("") @@ -365,7 +367,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage): template_perf = jinja_env.get_template( "campaign_performance_template_EN.j2" ) - campaign = campaignList.selected_campaign + campaign = self.campaignList.selected_campaign self.setField("selectedCampaign", campaign) if campaign is None: self.campaignMapDescription.setText("No campaign selected") @@ -378,11 +380,13 @@ class TheaterConfiguration(QtWidgets.QWizardPage): template_perf.render({"performance": campaign.performance}) ) - campaignList.selectionModel().setCurrentIndex( - campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows + self.campaignList.selectionModel().setCurrentIndex( + self.campaignList.indexAt(QPoint(1, 1)), QItemSelectionModel.Rows ) - campaignList.selectionModel().selectionChanged.connect(on_campaign_selected) + self.campaignList.selectionModel().selectionChanged.connect( + on_campaign_selected + ) on_campaign_selected() docsText = QtWidgets.QLabel( @@ -409,7 +413,7 @@ class TheaterConfiguration(QtWidgets.QWizardPage): layout = QtWidgets.QGridLayout() layout.setColumnMinimumWidth(0, 20) - layout.addWidget(campaignList, 0, 0, 5, 1) + layout.addWidget(self.campaignList, 0, 0, 5, 1) layout.addWidget(show_incompatible_campaigns_checkbox, 5, 0, 1, 1) layout.addWidget(docsText, 6, 0, 1, 1) layout.addWidget(self.campaignMapDescription, 0, 1, 1, 1) From 7aca108ef5a2ec7ca9b98e5408506dab57d17d73 Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:03:40 -0700 Subject: [PATCH 13/48] Adds Russian VVS Mig-29S Squadrons --- .../squadrons/Mig-29/Russia VVS 115th GvIAP.yaml | 14 ++++++++++++++ .../squadrons/Mig-29/Russia VVS 28th GvIAP.yaml | 14 ++++++++++++++ .../squadrons/Mig-29/Russia VVS 31st GvIAP.yaml | 14 ++++++++++++++ .../squadrons/Mig-29/Russia VVS 773rd IAP.yaml | 14 ++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 resources/squadrons/Mig-29/Russia VVS 115th GvIAP.yaml create mode 100644 resources/squadrons/Mig-29/Russia VVS 28th GvIAP.yaml create mode 100644 resources/squadrons/Mig-29/Russia VVS 31st GvIAP.yaml create mode 100644 resources/squadrons/Mig-29/Russia VVS 773rd IAP.yaml diff --git a/resources/squadrons/Mig-29/Russia VVS 115th GvIAP.yaml b/resources/squadrons/Mig-29/Russia VVS 115th GvIAP.yaml new file mode 100644 index 00000000..8f7f1183 --- /dev/null +++ b/resources/squadrons/Mig-29/Russia VVS 115th GvIAP.yaml @@ -0,0 +1,14 @@ +--- +name: 115th Guards Aviation Regiment +nickname: 115th GvIAP +country: Russia +role: Air Superiority Fighter +aircraft: MiG-29S Fulcrum-C +livery: "115 GvIAP_Termez" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-29/Russia VVS 28th GvIAP.yaml b/resources/squadrons/Mig-29/Russia VVS 28th GvIAP.yaml new file mode 100644 index 00000000..23307b7f --- /dev/null +++ b/resources/squadrons/Mig-29/Russia VVS 28th GvIAP.yaml @@ -0,0 +1,14 @@ +--- +name: 28th Guards Aviation Regiment +nickname: 28th GvIAP +country: Russia +role: Air Superiority Fighter +aircraft: MiG-29S Fulcrum-C +livery: "28 GvIAP_Andreapol" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-29/Russia VVS 31st GvIAP.yaml b/resources/squadrons/Mig-29/Russia VVS 31st GvIAP.yaml new file mode 100644 index 00000000..3c87b645 --- /dev/null +++ b/resources/squadrons/Mig-29/Russia VVS 31st GvIAP.yaml @@ -0,0 +1,14 @@ +--- +name: 31st Guards Aviation Regiment +nickname: 31st GvIAP +country: Russia +role: Air Superiority Fighter +aircraft: MiG-29S Fulcrum-C +livery: "31 GvIAP_Zernograd" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/Mig-29/Russia VVS 773rd IAP.yaml b/resources/squadrons/Mig-29/Russia VVS 773rd IAP.yaml new file mode 100644 index 00000000..37d63414 --- /dev/null +++ b/resources/squadrons/Mig-29/Russia VVS 773rd IAP.yaml @@ -0,0 +1,14 @@ +--- +name: 773rd Aviation Regiment +nickname: 773rd IAP +country: Russia +role: Air Superiority Fighter +aircraft: MiG-29S Fulcrum-C +livery: "773 IAP_Damgarten" +mission_types: + - BARCAP + - TARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP From 41aa7439477a791bcc89886fd7d49b2d1f840623 Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:13:41 -0700 Subject: [PATCH 14/48] Adds all default USAF Viper Squadrons --- resources/squadrons/viper/USAF 132nd WG.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 13th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 14th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 152nd FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 174th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 179th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 22nd FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 23rd FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 36th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 480th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 522nd FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 55th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 77th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 79th FS.yaml | 21 ++++++++++++++++++++ resources/squadrons/viper/USAF 80th FS.yaml | 21 ++++++++++++++++++++ 15 files changed, 315 insertions(+) create mode 100644 resources/squadrons/viper/USAF 132nd WG.yaml create mode 100644 resources/squadrons/viper/USAF 13th FS.yaml create mode 100644 resources/squadrons/viper/USAF 14th FS.yaml create mode 100644 resources/squadrons/viper/USAF 152nd FS.yaml create mode 100644 resources/squadrons/viper/USAF 174th FS.yaml create mode 100644 resources/squadrons/viper/USAF 179th FS.yaml create mode 100644 resources/squadrons/viper/USAF 22nd FS.yaml create mode 100644 resources/squadrons/viper/USAF 23rd FS.yaml create mode 100644 resources/squadrons/viper/USAF 36th FS.yaml create mode 100644 resources/squadrons/viper/USAF 480th FS.yaml create mode 100644 resources/squadrons/viper/USAF 522nd FS.yaml create mode 100644 resources/squadrons/viper/USAF 55th FS.yaml create mode 100644 resources/squadrons/viper/USAF 77th FS.yaml create mode 100644 resources/squadrons/viper/USAF 79th FS.yaml create mode 100644 resources/squadrons/viper/USAF 80th FS.yaml diff --git a/resources/squadrons/viper/USAF 132nd WG.yaml b/resources/squadrons/viper/USAF 132nd WG.yaml new file mode 100644 index 00000000..c94f30df --- /dev/null +++ b/resources/squadrons/viper/USAF 132nd WG.yaml @@ -0,0 +1,21 @@ +--- +name: 132nd FW +nickname: Hawkeyes +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 132nd_Wing _Iowa_ANG +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 13th FS.yaml b/resources/squadrons/viper/USAF 13th FS.yaml new file mode 100644 index 00000000..58d125ed --- /dev/null +++ b/resources/squadrons/viper/USAF 13th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 13th FS +nickname: Panthers +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 13th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 14th FS.yaml b/resources/squadrons/viper/USAF 14th FS.yaml new file mode 100644 index 00000000..91f4512f --- /dev/null +++ b/resources/squadrons/viper/USAF 14th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 14th FS +nickname: Samurais +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 14th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 152nd FS.yaml b/resources/squadrons/viper/USAF 152nd FS.yaml new file mode 100644 index 00000000..04ecfecc --- /dev/null +++ b/resources/squadrons/viper/USAF 152nd FS.yaml @@ -0,0 +1,21 @@ +--- +name: 152nd FS +nickname: Las Vaqueros +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 152nd_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 174th FS.yaml b/resources/squadrons/viper/USAF 174th FS.yaml new file mode 100644 index 00000000..21d1e4c2 --- /dev/null +++ b/resources/squadrons/viper/USAF 174th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 174th FS +nickname: Bulldogs +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 174th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 179th FS.yaml b/resources/squadrons/viper/USAF 179th FS.yaml new file mode 100644 index 00000000..d2ea318a --- /dev/null +++ b/resources/squadrons/viper/USAF 179th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 179th FS +nickname: Bulldogs +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 179th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 22nd FS.yaml b/resources/squadrons/viper/USAF 22nd FS.yaml new file mode 100644 index 00000000..0dee1803 --- /dev/null +++ b/resources/squadrons/viper/USAF 22nd FS.yaml @@ -0,0 +1,21 @@ +--- +name: 22nd FS +nickname: Stingers +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 22nd_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 23rd FS.yaml b/resources/squadrons/viper/USAF 23rd FS.yaml new file mode 100644 index 00000000..d3a676c7 --- /dev/null +++ b/resources/squadrons/viper/USAF 23rd FS.yaml @@ -0,0 +1,21 @@ +--- +name: 23rd FS +nickname: Fighting Hawks +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 23rd_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 36th FS.yaml b/resources/squadrons/viper/USAF 36th FS.yaml new file mode 100644 index 00000000..4ded73f5 --- /dev/null +++ b/resources/squadrons/viper/USAF 36th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 36th FS +nickname: Flying Fiends +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 36th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 480th FS.yaml b/resources/squadrons/viper/USAF 480th FS.yaml new file mode 100644 index 00000000..336bfa24 --- /dev/null +++ b/resources/squadrons/viper/USAF 480th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 480th FS +nickname: Warhawks +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 480th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 522nd FS.yaml b/resources/squadrons/viper/USAF 522nd FS.yaml new file mode 100644 index 00000000..c42c2d27 --- /dev/null +++ b/resources/squadrons/viper/USAF 522nd FS.yaml @@ -0,0 +1,21 @@ +--- +name: 522nd FS +nickname: Fireballs +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 522nd_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 55th FS.yaml b/resources/squadrons/viper/USAF 55th FS.yaml new file mode 100644 index 00000000..1b1aacc5 --- /dev/null +++ b/resources/squadrons/viper/USAF 55th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 55th FS +nickname: Fifty Fifth +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 55th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 77th FS.yaml b/resources/squadrons/viper/USAF 77th FS.yaml new file mode 100644 index 00000000..fd361612 --- /dev/null +++ b/resources/squadrons/viper/USAF 77th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 77th FS +nickname: Gamblers +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 77th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 79th FS.yaml b/resources/squadrons/viper/USAF 79th FS.yaml new file mode 100644 index 00000000..276cf8b8 --- /dev/null +++ b/resources/squadrons/viper/USAF 79th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 79th FS +nickname: Tigers +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 79th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/viper/USAF 80th FS.yaml b/resources/squadrons/viper/USAF 80th FS.yaml new file mode 100644 index 00000000..6b90ce88 --- /dev/null +++ b/resources/squadrons/viper/USAF 80th FS.yaml @@ -0,0 +1,21 @@ +--- +name: 80th FS +nickname: Headhunters +country: USA +role: Strike Fighter +aircraft: F-16CM Fighting Falcon (Block 50) +livery: 80th_Fighter_Squadron +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - SEAD Escort + - Strike + - Fighter sweep + - TARCAP From 67405e4af5e7046a533b26aa5c7f6e5a00c75025 Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:15:23 -0700 Subject: [PATCH 15/48] Adds Tanker Squadrons --- resources/squadrons/KC-130/VMGR-352.yaml | 8 ++++++++ resources/squadrons/KC-135/18th ARS.yaml | 8 ++++++++ .../squadrons/KC-135/TuAF 101st Tanker Squadron.yaml | 9 +++++++++ resources/squadrons/KC-135MPRS/340th EARS.yaml | 8 ++++++++ 4 files changed, 33 insertions(+) create mode 100644 resources/squadrons/KC-130/VMGR-352.yaml create mode 100644 resources/squadrons/KC-135/18th ARS.yaml create mode 100644 resources/squadrons/KC-135/TuAF 101st Tanker Squadron.yaml create mode 100644 resources/squadrons/KC-135MPRS/340th EARS.yaml diff --git a/resources/squadrons/KC-130/VMGR-352.yaml b/resources/squadrons/KC-130/VMGR-352.yaml new file mode 100644 index 00000000..415c48c6 --- /dev/null +++ b/resources/squadrons/KC-130/VMGR-352.yaml @@ -0,0 +1,8 @@ +--- +name: VMGR-352 +nickname: Raiders +country: USA +role: Air-to-Air Refueling +aircraft: KC-130 +mission_types: + - Refueling diff --git a/resources/squadrons/KC-135/18th ARS.yaml b/resources/squadrons/KC-135/18th ARS.yaml new file mode 100644 index 00000000..e78452e1 --- /dev/null +++ b/resources/squadrons/KC-135/18th ARS.yaml @@ -0,0 +1,8 @@ +--- +name: 18th Air Refueling Squadron +nickname: +country: USA +role: Air-to-Air Refueling +aircraft: KC-135 Stratotanker +mission_types: + - Refueling diff --git a/resources/squadrons/KC-135/TuAF 101st Tanker Squadron.yaml b/resources/squadrons/KC-135/TuAF 101st Tanker Squadron.yaml new file mode 100644 index 00000000..d8282c85 --- /dev/null +++ b/resources/squadrons/KC-135/TuAF 101st Tanker Squadron.yaml @@ -0,0 +1,9 @@ +--- +name: 101st Tanker Squadron +nickname: Asena +country: Turkey +role: Air-to-Air Refueling +aircraft: KC-135 Stratotanker +livery: TurAF Standard +mission_types: + - Refueling diff --git a/resources/squadrons/KC-135MPRS/340th EARS.yaml b/resources/squadrons/KC-135MPRS/340th EARS.yaml new file mode 100644 index 00000000..35179cc5 --- /dev/null +++ b/resources/squadrons/KC-135MPRS/340th EARS.yaml @@ -0,0 +1,8 @@ +--- +name: 340th Expeditionary Air Refueling Squadron +nickname: Pythons +country: USA +role: Air-to-Air Refueling +aircraft: KC-135 Stratotanker MPRS +mission_types: + - Refueling From aac91c15d9c7d121edf35cd852570dfe8719615e Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:16:08 -0700 Subject: [PATCH 16/48] Adds AEW&C Squadrons --- resources/squadrons/E-2 Hawkeye/VAW-125.yaml | 9 +++++++++ resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml | 9 +++++++++ 2 files changed, 18 insertions(+) create mode 100644 resources/squadrons/E-2 Hawkeye/VAW-125.yaml create mode 100644 resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml diff --git a/resources/squadrons/E-2 Hawkeye/VAW-125.yaml b/resources/squadrons/E-2 Hawkeye/VAW-125.yaml new file mode 100644 index 00000000..612078f4 --- /dev/null +++ b/resources/squadrons/E-2 Hawkeye/VAW-125.yaml @@ -0,0 +1,9 @@ +--- +name: VAW-125 +nickname: Tigertails +country: USA +role: AEW&C +aircraft: E-2C Hawkeye +livery: VAW-125 Tigertails +mission_types: + - AEW&C diff --git a/resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml b/resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml new file mode 100644 index 00000000..51aede1d --- /dev/null +++ b/resources/squadrons/E-3 Sentry/USAF 960th AACS.yaml @@ -0,0 +1,9 @@ +--- +name: 960th AAC Squadron +nickname: Vikings +country: USA +role: AEW&C +aircraft: E-3A +livery: usaf standard +mission_types: + - AEW&C From 33709b05584ff9517fac6c1d5412a6e2fa89c91d Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:24:33 -0700 Subject: [PATCH 17/48] Adds All Default USAF F-15C Squadrons --- resources/squadrons/Eagle/USAF 12th FS.yaml | 14 ++++++++++++++ resources/squadrons/Eagle/USAF 390th FS.yaml | 14 ++++++++++++++ resources/squadrons/Eagle/USAF 493rd FS.yaml | 14 ++++++++++++++ resources/squadrons/Eagle/USAF 58th FS.yaml | 14 ++++++++++++++ 4 files changed, 56 insertions(+) create mode 100644 resources/squadrons/Eagle/USAF 12th FS.yaml create mode 100644 resources/squadrons/Eagle/USAF 390th FS.yaml create mode 100644 resources/squadrons/Eagle/USAF 493rd FS.yaml create mode 100644 resources/squadrons/Eagle/USAF 58th FS.yaml diff --git a/resources/squadrons/Eagle/USAF 12th FS.yaml b/resources/squadrons/Eagle/USAF 12th FS.yaml new file mode 100644 index 00000000..c2b80a71 --- /dev/null +++ b/resources/squadrons/Eagle/USAF 12th FS.yaml @@ -0,0 +1,14 @@ +--- +name: 12th FS +nickname: Dirty Dozen +country: USA +role: Air Superiority Fighter +aircraft: F-15C Eagle +livery: 12th Fighter SQN (AK) +mission_types: + - BARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP + diff --git a/resources/squadrons/Eagle/USAF 390th FS.yaml b/resources/squadrons/Eagle/USAF 390th FS.yaml new file mode 100644 index 00000000..0ca2e132 --- /dev/null +++ b/resources/squadrons/Eagle/USAF 390th FS.yaml @@ -0,0 +1,14 @@ +--- +name: 390th FS +nickname: Wild Boars +country: USA +role: Air Superiority Fighter +aircraft: F-15C Eagle +livery: 390th Fighter SQN +mission_types: + - BARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP + diff --git a/resources/squadrons/Eagle/USAF 493rd FS.yaml b/resources/squadrons/Eagle/USAF 493rd FS.yaml new file mode 100644 index 00000000..dcb95685 --- /dev/null +++ b/resources/squadrons/Eagle/USAF 493rd FS.yaml @@ -0,0 +1,14 @@ +--- +name: 493rd FS +nickname: Grim Reapers +country: USA +role: Air Superiority Fighter +aircraft: F-15C Eagle +livery: 493rd Fighter SQN (LN) +mission_types: + - BARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP + diff --git a/resources/squadrons/Eagle/USAF 58th FS.yaml b/resources/squadrons/Eagle/USAF 58th FS.yaml new file mode 100644 index 00000000..912f8bc6 --- /dev/null +++ b/resources/squadrons/Eagle/USAF 58th FS.yaml @@ -0,0 +1,14 @@ +--- +name: 58th FS +nickname: Gorillas +country: USA +role: Air Superiority Fighter +aircraft: F-15C Eagle +livery: 58th Fighter SQN (EG) +mission_types: + - BARCAP + - Escort + - Intercept + - Fighter sweep + - TARCAP + From 824745c11d79d32c21c57ecb075dabbbed21e1d8 Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:27:25 -0700 Subject: [PATCH 18/48] Adds All Default A-10C I & II Squadrons --- resources/squadrons/A-10C Warthog I/104th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/118th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/172nd FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/184th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/190th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/25th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/354th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/355th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/357th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/358th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/47th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/74th TFS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog I/81st FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog II/25th FS.yaml | 15 +++++++++++++++ .../squadrons/A-10C Warthog II/354th FS.yaml | 15 +++++++++++++++ .../squadrons/A-10C Warthog II/355th FS.yaml | 15 +++++++++++++++ .../squadrons/A-10C Warthog II/357th FS.yaml | 15 +++++++++++++++ .../squadrons/A-10C Warthog II/358th FS.yaml | 15 +++++++++++++++ resources/squadrons/A-10C Warthog II/81st FS.yaml | 15 +++++++++++++++ 19 files changed, 285 insertions(+) create mode 100644 resources/squadrons/A-10C Warthog I/104th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/118th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/172nd FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/184th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/190th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/25th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/354th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/355th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/357th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/358th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/47th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/74th TFS.yaml create mode 100644 resources/squadrons/A-10C Warthog I/81st FS.yaml create mode 100644 resources/squadrons/A-10C Warthog II/25th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog II/354th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog II/355th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog II/357th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog II/358th FS.yaml create mode 100644 resources/squadrons/A-10C Warthog II/81st FS.yaml diff --git a/resources/squadrons/A-10C Warthog I/104th FS.yaml b/resources/squadrons/A-10C Warthog I/104th FS.yaml new file mode 100644 index 00000000..d79517c1 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/104th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 104th FS +nickname: Eagles +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 104th FS Maryland ANG, Baltimore (MD) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/118th FS.yaml b/resources/squadrons/A-10C Warthog I/118th FS.yaml new file mode 100644 index 00000000..cb315113 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/118th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 118th FS +nickname: Flying Yankees +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 118th FS Bradley ANGB, Connecticut (CT) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/172nd FS.yaml b/resources/squadrons/A-10C Warthog I/172nd FS.yaml new file mode 100644 index 00000000..7f7ac5d5 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/172nd FS.yaml @@ -0,0 +1,15 @@ +--- +name: 172nd FS +nickname: +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 172nd FS Battle Creek ANGB, Michigan (BC) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/184th FS.yaml b/resources/squadrons/A-10C Warthog I/184th FS.yaml new file mode 100644 index 00000000..e09f5910 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/184th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 184th FS +nickname: Flying Razorbacks +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 184th FS Arkansas ANG, Fort Smith (FS) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/190th FS.yaml b/resources/squadrons/A-10C Warthog I/190th FS.yaml new file mode 100644 index 00000000..9fd3cb9d --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/190th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 190th FS +nickname: Skull Bangers +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 190th FS Boise ANGB, Idaho (ID) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/25th FS.yaml b/resources/squadrons/A-10C Warthog I/25th FS.yaml new file mode 100644 index 00000000..eca63e4b --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/25th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 25th FS +nickname: Assam Draggins +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 25th FS Osab AB, Korea (OS) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/354th FS.yaml b/resources/squadrons/A-10C Warthog I/354th FS.yaml new file mode 100644 index 00000000..6f9a6398 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/354th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 354th FS +nickname: Bulldogs +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 354th FS Davis Monthan AFB, Arizona (DM) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/355th FS.yaml b/resources/squadrons/A-10C Warthog I/355th FS.yaml new file mode 100644 index 00000000..e9192c54 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/355th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 355th FS +nickname: Fightin' Falcons +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 355th FS Eielson AFB, Alaska (AK) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/357th FS.yaml b/resources/squadrons/A-10C Warthog I/357th FS.yaml new file mode 100644 index 00000000..7dfb5bb7 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/357th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 357th FS +nickname: Dragons +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 357th FS Davis Monthan AFB, Arizona (DM) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/358th FS.yaml b/resources/squadrons/A-10C Warthog I/358th FS.yaml new file mode 100644 index 00000000..c39833c2 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/358th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 358th FS +nickname: Lobos +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 358th FS Davis Monthan AFB, Arizona (DM) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/47th FS.yaml b/resources/squadrons/A-10C Warthog I/47th FS.yaml new file mode 100644 index 00000000..49787e75 --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/47th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 47th FS +nickname: Termites +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 47th FS Barksdale AFB, Louisiana (BD) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/74th TFS.yaml b/resources/squadrons/A-10C Warthog I/74th TFS.yaml new file mode 100644 index 00000000..766cc7fc --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/74th TFS.yaml @@ -0,0 +1,15 @@ +--- +name: 74th TFS +nickname: Flying Tigers +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 23rd TFW England AFB (EL) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog I/81st FS.yaml b/resources/squadrons/A-10C Warthog I/81st FS.yaml new file mode 100644 index 00000000..7659c78d --- /dev/null +++ b/resources/squadrons/A-10C Warthog I/81st FS.yaml @@ -0,0 +1,15 @@ +--- +name: 81st FS +nickname: Termites +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 3) +livery: 81st FS Spangdahlem AB, Germany (SP) 2 +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog II/25th FS.yaml b/resources/squadrons/A-10C Warthog II/25th FS.yaml new file mode 100644 index 00000000..a5ebd22f --- /dev/null +++ b/resources/squadrons/A-10C Warthog II/25th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 25th FS +nickname: Assam Draggins +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 7) +livery: 25th FS Osab AB, Korea (OS) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog II/354th FS.yaml b/resources/squadrons/A-10C Warthog II/354th FS.yaml new file mode 100644 index 00000000..f1cb42c3 --- /dev/null +++ b/resources/squadrons/A-10C Warthog II/354th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 354th FS +nickname: Bulldogs +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 7) +livery: 354th FS Davis Monthan AFB, Arizona (DM) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog II/355th FS.yaml b/resources/squadrons/A-10C Warthog II/355th FS.yaml new file mode 100644 index 00000000..e71ec1a8 --- /dev/null +++ b/resources/squadrons/A-10C Warthog II/355th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 355th FS +nickname: Fightin' Falcons +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 7) +livery: 355th FS Eielson AFB, Alaska (AK) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog II/357th FS.yaml b/resources/squadrons/A-10C Warthog II/357th FS.yaml new file mode 100644 index 00000000..eb21b2a7 --- /dev/null +++ b/resources/squadrons/A-10C Warthog II/357th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 357th FS +nickname: Dragons +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 7) +livery: 357th FS Davis Monthan AFB, Arizona (DM) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog II/358th FS.yaml b/resources/squadrons/A-10C Warthog II/358th FS.yaml new file mode 100644 index 00000000..ad3e386c --- /dev/null +++ b/resources/squadrons/A-10C Warthog II/358th FS.yaml @@ -0,0 +1,15 @@ +--- +name: 358th FS +nickname: Lobos +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 7) +livery: 358th FS Davis Monthan AFB, Arizona (DM) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + diff --git a/resources/squadrons/A-10C Warthog II/81st FS.yaml b/resources/squadrons/A-10C Warthog II/81st FS.yaml new file mode 100644 index 00000000..fc798b6e --- /dev/null +++ b/resources/squadrons/A-10C Warthog II/81st FS.yaml @@ -0,0 +1,15 @@ +--- +name: 81st FS +nickname: Termites +country: USA +role: Close Air Support +aircraft: A-10C Thunderbolt II (Suite 7) +livery: 81st FS Spangdahlem AB, Germany (SP) 2 +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + From 46e2d8c1f968fb3a37dcedd307354531efc0b201 Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:28:49 -0700 Subject: [PATCH 19/48] Adds All Default USAF F-15E Squadrons --- resources/squadrons/Strike Eagle/335th FS.yaml | 14 ++++++++++++++ resources/squadrons/Strike Eagle/492nd FS.yaml | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 resources/squadrons/Strike Eagle/335th FS.yaml create mode 100644 resources/squadrons/Strike Eagle/492nd FS.yaml diff --git a/resources/squadrons/Strike Eagle/335th FS.yaml b/resources/squadrons/Strike Eagle/335th FS.yaml new file mode 100644 index 00000000..6790eedc --- /dev/null +++ b/resources/squadrons/Strike Eagle/335th FS.yaml @@ -0,0 +1,14 @@ +--- +name: 335th FS +nickname: Chiefs +country: USA +role: Strike Fighter +aircraft: F-15E Strike Eagle +livery: 335th Fighter SQN (SJ) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike diff --git a/resources/squadrons/Strike Eagle/492nd FS.yaml b/resources/squadrons/Strike Eagle/492nd FS.yaml new file mode 100644 index 00000000..deb413c3 --- /dev/null +++ b/resources/squadrons/Strike Eagle/492nd FS.yaml @@ -0,0 +1,15 @@ +--- +name: 492nd FS +nickname: Chiefs +country: USA +role: Strike Fighter +aircraft: F-15E Strike Eagle +livery: 492d Fighter SQN (LN) +mission_types: + - BAI + - CAS + - DEAD + - OCA/Aircraft + - OCA/Runway + - Strike + From 60a5ee42fe6c2d91dd88af4f4869488373e65100 Mon Sep 17 00:00:00 2001 From: Mustang-25 <72566076+Mustang-25@users.noreply.github.com> Date: Sun, 29 Aug 2021 14:33:41 -0700 Subject: [PATCH 20/48] Adds Most Default USN F-14A & B Squadrons --- .../F-14A 135-GR Tomcat (Late)/VF-11.yaml | 20 +++++++++++++++++++ .../F-14A 135-GR Tomcat (Late)/VF-111.yaml | 20 +++++++++++++++++++ .../F-14A 135-GR Tomcat (Late)/VF-21.yaml | 20 +++++++++++++++++++ .../F-14A 135-GR Tomcat (Late)/VF-211.yaml | 20 +++++++++++++++++++ .../F-14A 135-GR Tomcat (Late)/VF-33.yaml | 20 +++++++++++++++++++ resources/squadrons/F-14B Tomcat/VF-101.yaml | 20 +++++++++++++++++++ resources/squadrons/F-14B Tomcat/VF-102.yaml | 20 +++++++++++++++++++ resources/squadrons/F-14B Tomcat/VF-142.yaml | 19 ++++++++++++++++++ resources/squadrons/F-14B Tomcat/VF-211.yaml | 19 ++++++++++++++++++ 9 files changed, 178 insertions(+) create mode 100644 resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-11.yaml create mode 100644 resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-111.yaml create mode 100644 resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-21.yaml create mode 100644 resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-211.yaml create mode 100644 resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-33.yaml create mode 100644 resources/squadrons/F-14B Tomcat/VF-101.yaml create mode 100644 resources/squadrons/F-14B Tomcat/VF-102.yaml create mode 100644 resources/squadrons/F-14B Tomcat/VF-142.yaml create mode 100644 resources/squadrons/F-14B Tomcat/VF-211.yaml diff --git a/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-11.yaml b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-11.yaml new file mode 100644 index 00000000..645ea931 --- /dev/null +++ b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-11.yaml @@ -0,0 +1,20 @@ +--- +name: VF-11 +nickname: Red Rippers +country: USA +role: Strike Fighter +aircraft: F-14A Tomcat (Block 135-GR Late) +livery: VF-11 Red Rippers 106 +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-111.yaml b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-111.yaml new file mode 100644 index 00000000..0583c0fb --- /dev/null +++ b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-111.yaml @@ -0,0 +1,20 @@ +--- +name: VF-111 +nickname: Sundowners +country: USA +role: Strike Fighter +aircraft: F-14A Tomcat (Block 135-GR Late) +livery: VF-111 Sundowners 200 +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-21.yaml b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-21.yaml new file mode 100644 index 00000000..35415a74 --- /dev/null +++ b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-21.yaml @@ -0,0 +1,20 @@ +--- +name: VF-21 +nickname: Freelancers +country: USA +role: Strike Fighter +aircraft: F-14A Tomcat (Block 135-GR Late) +livery: VF-21 Freelancers 200 +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-211.yaml b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-211.yaml new file mode 100644 index 00000000..f8e2baa6 --- /dev/null +++ b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-211.yaml @@ -0,0 +1,20 @@ +--- +name: VF-211 +nickname: Fighting Checkmates +country: USA +role: Strike Fighter +aircraft: F-14A Tomcat (Block 135-GR Late) +livery: VF-211 Fighting Checkmates 105 +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-33.yaml b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-33.yaml new file mode 100644 index 00000000..1f4bbb5f --- /dev/null +++ b/resources/squadrons/F-14A 135-GR Tomcat (Late)/VF-33.yaml @@ -0,0 +1,20 @@ +--- +name: VF-33 +nickname: Starfighters +country: USA +role: Strike Fighter +aircraft: F-14A Tomcat (Block 135-GR Late) +livery: VF-33 Starfighters 201 +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14B Tomcat/VF-101.yaml b/resources/squadrons/F-14B Tomcat/VF-101.yaml new file mode 100644 index 00000000..603696b8 --- /dev/null +++ b/resources/squadrons/F-14B Tomcat/VF-101.yaml @@ -0,0 +1,20 @@ +--- +name: VF-101 +nickname: Grim Reapers +country: USA +role: Strike Fighter +aircraft: F-14B Tomcat +livery: VF-101 Dark +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14B Tomcat/VF-102.yaml b/resources/squadrons/F-14B Tomcat/VF-102.yaml new file mode 100644 index 00000000..81ba013c --- /dev/null +++ b/resources/squadrons/F-14B Tomcat/VF-102.yaml @@ -0,0 +1,20 @@ +--- +name: VF-102 +nickname: Diamond Backs +country: USA +role: Strike Fighter +aircraft: F-14B Tomcat +livery: VF-102 Diamondbacks 102 (2000) +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - SEAD + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14B Tomcat/VF-142.yaml b/resources/squadrons/F-14B Tomcat/VF-142.yaml new file mode 100644 index 00000000..94bfff95 --- /dev/null +++ b/resources/squadrons/F-14B Tomcat/VF-142.yaml @@ -0,0 +1,19 @@ +--- +name: VF-142 +nickname: Ghostriders +country: USA +role: Strike Fighter +aircraft: F-14B Tomcat +livery: VF-142 Ghostriders +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - Strike + - Fighter sweep + - TARCAP diff --git a/resources/squadrons/F-14B Tomcat/VF-211.yaml b/resources/squadrons/F-14B Tomcat/VF-211.yaml new file mode 100644 index 00000000..6a08cf27 --- /dev/null +++ b/resources/squadrons/F-14B Tomcat/VF-211.yaml @@ -0,0 +1,19 @@ +--- +name: VF-211 +nickname: Fighting Checkmates +country: USA +role: Strike Fighter +aircraft: F-14B Tomcat +livery: VF-211 Fighting Checkmates +mission_types: + - BAI + - BARCAP + - CAS + - DEAD + - Escort + - Intercept + - OCA/Aircraft + - OCA/Runway + - Strike + - Fighter sweep + - TARCAP From cd15de6d428553afbb51f033a9ea04da208816a1 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 29 Aug 2021 16:09:47 -0700 Subject: [PATCH 21/48] Add an uncaught exception handler. --- qt_ui/uncaughtexceptionhandler.py | 42 ++++++++++++++++++++++++++++++ qt_ui/windows/QLiberationWindow.py | 5 +++- 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 qt_ui/uncaughtexceptionhandler.py diff --git a/qt_ui/uncaughtexceptionhandler.py b/qt_ui/uncaughtexceptionhandler.py new file mode 100644 index 00000000..6e046fcf --- /dev/null +++ b/qt_ui/uncaughtexceptionhandler.py @@ -0,0 +1,42 @@ +# From https://timlehr.com/python-exception-hooks-with-qt-message-box/ +import logging +import sys +import traceback + +from PySide2.QtCore import Signal, QObject +from PySide2.QtWidgets import QMessageBox, QApplication + + +class UncaughtExceptionHandler(QObject): + _exception_caught = Signal(str, str) + + def __init__(self, parent: QObject): + super().__init__(parent) + sys.excepthook = self.exception_hook + # Use a signal so that the message box always comes from the main thread. + self._exception_caught.connect(self.show_exception_box) + + def exception_hook(self, exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + # Ignore keyboard interrupt to support console applications. + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logging.exception( + "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) + ) + self._exception_caught.emit( + str(exc_value), + "".join(traceback.format_exception(exc_type, exc_value, exc_traceback)), + ) + + def show_exception_box(self, message: str, exception: str) -> None: + if QApplication.instance() is not None: + QMessageBox().critical( + self.parent(), + "An unexpected error occurred", + "\n".join([message, "", exception]), + QMessageBox.Ok, + ) + else: + logging.critical("No QApplication instance available.") diff --git a/qt_ui/windows/QLiberationWindow.py b/qt_ui/windows/QLiberationWindow.py index a2c984d2..5fa02dac 100644 --- a/qt_ui/windows/QLiberationWindow.py +++ b/qt_ui/windows/QLiberationWindow.py @@ -24,6 +24,7 @@ from qt_ui import liberation_install from qt_ui.dialogs import Dialog from qt_ui.models import GameModel from qt_ui.uiconstants import URLS +from qt_ui.uncaughtexceptionhandler import UncaughtExceptionHandler from qt_ui.widgets.QTopPanel import QTopPanel from qt_ui.widgets.ato import QAirTaskingOrderPanel from qt_ui.widgets.map.QLiberationMap import QLiberationMap @@ -42,7 +43,9 @@ from qt_ui.windows.logs.QLogsWindow import QLogsWindow class QLiberationWindow(QMainWindow): def __init__(self, game: Optional[Game]) -> None: - super(QLiberationWindow, self).__init__() + super().__init__() + + self._uncaught_exception_handler = UncaughtExceptionHandler(self) self.game = game self.game_model = GameModel(game) From c2e5cba0616821669516e091bf198e23894efab7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 28 Aug 2021 18:09:33 -0700 Subject: [PATCH 22/48] Implement manual squadron transfers. Lightly tested but seems to work fine. https://github.com/dcs-liberation/dcs_liberation/issues/1145 --- game/squadrons/squadron.py | 43 +++++++++++++ game/theater/controlpoint.py | 14 +++-- qt_ui/errorreporter.py | 17 +++++ qt_ui/main.py | 2 +- qt_ui/windows/AirWingConfigurationDialog.py | 32 ---------- qt_ui/windows/AirWingDialog.py | 13 +++- qt_ui/windows/SquadronDialog.py | 70 ++++++++++++++++++++- 7 files changed, 148 insertions(+), 43 deletions(-) create mode 100644 qt_ui/errorreporter.py diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index ada95795..85fc19fc 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -53,6 +53,9 @@ class Squadron: settings: Settings = field(hash=False, compare=False) location: ControlPoint + destination: Optional[ControlPoint] = field( + init=False, hash=False, compare=False, default=None + ) owned_aircraft: int = field(init=False, hash=False, compare=False, default=0) untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0) @@ -168,6 +171,8 @@ class Squadron: self._recruit_pilots(self.settings.squadron_pilot_limit) def end_turn(self) -> None: + if self.destination is not None: + self.relocate_to(self.destination) self.replenish_lost_pilots() self.deliver_orders() @@ -280,6 +285,8 @@ class Squadron: def relocate_to(self, destination: ControlPoint) -> None: self.location = destination + if self.location == self.destination: + self.destination = None def cancel_overflow_orders(self) -> None: if self.pending_deliveries <= 0: @@ -297,6 +304,42 @@ class Squadron: def max_fulfillable_aircraft(self) -> int: return max(self.number_of_available_pilots, self.untasked_aircraft) + @property + def expected_size_next_turn(self) -> int: + return self.owned_aircraft + self.pending_deliveries + + def plan_relocation(self, destination: ControlPoint) -> None: + if destination == self.location: + logging.warning( + f"Attempted to plan relocation of {self} to current location " + f"{destination}. Ignoring." + ) + return + if destination == self.destination: + logging.warning( + f"Attempted to plan relocation of {self} to current destination " + f"{destination}. Ignoring." + ) + return + + if self.expected_size_next_turn >= destination.unclaimed_parking(): + raise RuntimeError(f"Not enough parking for {self} at {destination}.") + if not destination.can_operate(self.aircraft): + raise RuntimeError(f"{self} cannot operate at {destination}.") + self.destination = destination + + def cancel_relocation(self) -> None: + if self.destination is None: + logging.warning( + f"Attempted to cancel relocation of squadron with no transfer order. " + "Ignoring." + ) + return + + if self.expected_size_next_turn >= self.location.unclaimed_parking(): + raise RuntimeError(f"Not enough parking for {self} at {self.location}.") + self.destination = None + @classmethod def create_from( cls, diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 205a677f..7ea8a842 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -728,13 +728,19 @@ class ControlPoint(MissionTarget, ABC): def allocated_aircraft(self) -> AircraftAllocations: present: dict[AircraftType, int] = defaultdict(int) on_order: dict[AircraftType, int] = defaultdict(int) + transferring: dict[AircraftType, int] = defaultdict(int) for squadron in self.squadrons: present[squadron.aircraft] += squadron.owned_aircraft - # TODO: Only if this is the squadron destination, not location. - on_order[squadron.aircraft] += squadron.pending_deliveries + if squadron.destination is None: + on_order[squadron.aircraft] += squadron.pending_deliveries + else: + transferring[squadron.aircraft] -= squadron.owned_aircraft + for squadron in self.coalition.air_wing.iter_squadrons(): + if squadron.destination == self: + on_order[squadron.aircraft] += squadron.pending_deliveries + transferring[squadron.aircraft] += squadron.owned_aircraft - # TODO: Implement squadron transfers. - return AircraftAllocations(present, on_order, transferring={}) + return AircraftAllocations(present, on_order, transferring) def allocated_ground_units( self, transfers: PendingTransfers diff --git a/qt_ui/errorreporter.py b/qt_ui/errorreporter.py new file mode 100644 index 00000000..3e514c10 --- /dev/null +++ b/qt_ui/errorreporter.py @@ -0,0 +1,17 @@ +import logging +from collections import Iterator +from contextlib import contextmanager +from typing import Type + +from PySide2.QtWidgets import QDialog, QMessageBox + + +@contextmanager +def report_errors( + title: str, parent: QDialog, error_type: Type[Exception] = Exception +) -> Iterator[None]: + try: + yield + except error_type as ex: + logging.exception(title) + QMessageBox().critical(parent, title, str(ex), QMessageBox.Ok) diff --git a/qt_ui/main.py b/qt_ui/main.py index ff3c6e5a..14eda27f 100644 --- a/qt_ui/main.py +++ b/qt_ui/main.py @@ -13,6 +13,7 @@ from PySide2.QtWidgets import QApplication, QSplashScreen from dcs.payloads import PayloadDirectories from game import Game, VERSION, persistency +from game.campaignloader.campaign import Campaign from game.data.weapons import WeaponGroup, Pylon, Weapon from game.db import FACTIONS from game.dcs.aircrafttype import AircraftType @@ -27,7 +28,6 @@ from qt_ui import ( ) from qt_ui.windows.GameUpdateSignal import GameUpdateSignal from qt_ui.windows.QLiberationWindow import QLiberationWindow -from game.campaignloader.campaign import Campaign from qt_ui.windows.newgame.QNewGameWizard import DEFAULT_BUDGET from qt_ui.windows.preferences.QLiberationFirstStartWindow import ( QLiberationFirstStartWindow, diff --git a/qt_ui/windows/AirWingConfigurationDialog.py b/qt_ui/windows/AirWingConfigurationDialog.py index 745cac5c..4cdc41cc 100644 --- a/qt_ui/windows/AirWingConfigurationDialog.py +++ b/qt_ui/windows/AirWingConfigurationDialog.py @@ -10,7 +10,6 @@ from PySide2.QtCore import ( ) from PySide2.QtGui import QStandardItemModel, QStandardItem, QIcon from PySide2.QtWidgets import ( - QAbstractItemView, QDialog, QListView, QVBoxLayout, @@ -32,38 +31,7 @@ from game.dcs.aircrafttype import AircraftType from game.squadrons import AirWing, Pilot, Squadron from game.theater import ControlPoint, ConflictTheater from gen.flights.flight import FlightType -from qt_ui.models import AirWingModel, SquadronModel from qt_ui.uiconstants import AIRCRAFT_ICONS -from qt_ui.windows.AirWingDialog import SquadronDelegate -from qt_ui.windows.SquadronDialog import SquadronDialog - - -class SquadronList(QListView): - """List view for displaying the air wing's squadrons.""" - - def __init__(self, air_wing_model: AirWingModel) -> None: - super().__init__() - self.air_wing_model = air_wing_model - self.dialog: Optional[SquadronDialog] = None - - self.setIconSize(QSize(91, 24)) - self.setItemDelegate(SquadronDelegate(self.air_wing_model)) - self.setModel(self.air_wing_model) - self.selectionModel().setCurrentIndex( - self.air_wing_model.index(0, 0, QModelIndex()), QItemSelectionModel.Select - ) - - # self.setIconSize(QSize(91, 24)) - self.setSelectionBehavior(QAbstractItemView.SelectItems) - self.doubleClicked.connect(self.on_double_click) - - def on_double_click(self, index: QModelIndex) -> None: - if not index.isValid(): - return - self.dialog = SquadronDialog( - SquadronModel(self.air_wing_model.squadron_at_index(index)), self - ) - self.dialog.show() class AllowedMissionTypeControls(QVBoxLayout): diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index a8f8ca3f..9fcd1c7b 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -17,6 +17,7 @@ from PySide2.QtWidgets import ( ) from game.squadrons import Squadron +from game.theater import ConflictTheater from gen.flights.flight import Flight from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.models import GameModel, AirWingModel, SquadronModel @@ -56,9 +57,10 @@ class SquadronDelegate(TwoColumnRowDelegate): class SquadronList(QListView): """List view for displaying the air wing's squadrons.""" - def __init__(self, air_wing_model: AirWingModel) -> None: + def __init__(self, air_wing_model: AirWingModel, theater: ConflictTheater) -> None: super().__init__() self.air_wing_model = air_wing_model + self.theater = theater self.dialog: Optional[SquadronDialog] = None self.setIconSize(QSize(91, 24)) @@ -76,7 +78,9 @@ class SquadronList(QListView): if not index.isValid(): return self.dialog = SquadronDialog( - SquadronModel(self.air_wing_model.squadron_at_index(index)), self + SquadronModel(self.air_wing_model.squadron_at_index(index)), + self.theater, + self, ) self.dialog.show() @@ -194,7 +198,10 @@ class AirWingTabs(QTabWidget): def __init__(self, game_model: GameModel) -> None: super().__init__() - self.addTab(SquadronList(game_model.blue_air_wing_model), "Squadrons") + self.addTab( + SquadronList(game_model.blue_air_wing_model, game_model.game.theater), + "Squadrons", + ) self.addTab(AirInventoryView(game_model), "Inventory") diff --git a/qt_ui/windows/SquadronDialog.py b/qt_ui/windows/SquadronDialog.py index c17e5312..24b004cc 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -1,5 +1,5 @@ import logging -from typing import Callable +from typing import Callable, Iterator, Optional from PySide2.QtCore import ( QItemSelectionModel, @@ -16,11 +16,14 @@ from PySide2.QtWidgets import ( QHBoxLayout, QLabel, QCheckBox, + QComboBox, ) -from game.squadrons import Pilot +from game.squadrons import Pilot, Squadron +from game.theater import ControlPoint, ConflictTheater from gen.flights.flight import FlightType from qt_ui.delegates import TwoColumnRowDelegate +from qt_ui.errorreporter import report_errors from qt_ui.models import SquadronModel @@ -90,10 +93,50 @@ class AutoAssignedTaskControls(QVBoxLayout): self.squadron_model.set_auto_assignable(task, checked) +class SquadronDestinationComboBox(QComboBox): + def __init__(self, squadron: Squadron, theater: ConflictTheater) -> None: + super().__init__() + self.squadron = squadron + self.theater = theater + + room = squadron.location.unclaimed_parking() + self.addItem( + f"Remain at {squadron.location} (room for {room} more aircraft)", None + ) + selected_index: Optional[int] = None + for idx, destination in enumerate(sorted(self.iter_destinations(), key=str), 1): + if destination == squadron.destination: + selected_index = idx + room = destination.unclaimed_parking() + self.addItem( + f"Transfer to {destination} (room for {room} more aircraft)", + destination, + ) + + if squadron.destination is None: + selected_index = 0 + + if selected_index is not None: + self.setCurrentIndex(selected_index) + + def iter_destinations(self) -> Iterator[ControlPoint]: + size = self.squadron.expected_size_next_turn + for control_point in self.theater.control_points_for(self.squadron.player): + if control_point == self: + continue + if not control_point.can_operate(self.squadron.aircraft): + continue + if control_point.unclaimed_parking() < size: + continue + yield control_point + + class SquadronDialog(QDialog): """Dialog window showing a squadron.""" - def __init__(self, squadron_model: SquadronModel, parent) -> None: + def __init__( + self, squadron_model: SquadronModel, theater: ConflictTheater, parent + ) -> None: super().__init__(parent) self.squadron_model = squadron_model @@ -117,6 +160,15 @@ class SquadronDialog(QDialog): columns.addWidget(self.pilot_list) button_panel = QHBoxLayout() + + self.transfer_destination = SquadronDestinationComboBox( + squadron_model.squadron, theater + ) + self.transfer_destination.currentIndexChanged.connect( + self.on_destination_changed + ) + button_panel.addWidget(self.transfer_destination) + button_panel.addStretch() layout.addLayout(button_panel) @@ -132,6 +184,18 @@ class SquadronDialog(QDialog): self.toggle_leave_button.clicked.connect(self.toggle_leave) button_panel.addWidget(self.toggle_leave_button, alignment=Qt.AlignRight) + @property + def squadron(self) -> Squadron: + return self.squadron_model.squadron + + def on_destination_changed(self, index: int) -> None: + with report_errors("Could not change squadron destination", self): + destination = self.transfer_destination.itemData(index) + if destination is None: + self.squadron.cancel_relocation() + else: + self.squadron.plan_relocation(destination) + def check_disabled_button_states( self, button: QPushButton, index: QModelIndex ) -> bool: From e0047b1bbc70ec93f2eb91027128a4569c0ed6a9 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 31 Aug 2021 22:09:39 -0700 Subject: [PATCH 23/48] Move the start type requirement into ControlPoint. --- game/commander/packagebuilder.py | 5 ++--- game/theater/controlpoint.py | 8 ++++++++ game/transfers.py | 6 +++++- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index a4baf9c7..c990056b 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -43,9 +43,8 @@ class PackageBuilder: if assignment is None: return False airfield, squadron = assignment - if isinstance(airfield, OffMapSpawn): - start_type = "In Flight" - else: + start_type = airfield.required_aircraft_start_type + if start_type is None: start_type = self.start_type flight = Flight( diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 7ea8a842..27f7aa14 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -670,6 +670,10 @@ class ControlPoint(MissionTarget, ABC): self._coalition = new_coalition self.base.set_strength_to_minimum() + @property + def required_aircraft_start_type(self) -> Optional[str]: + return None + @abstractmethod def can_operate(self, aircraft: AircraftType) -> bool: ... @@ -1106,6 +1110,10 @@ class OffMapSpawn(ControlPoint): def can_operate(self, aircraft: AircraftType) -> bool: return True + @property + def required_aircraft_start_type(self) -> Optional[str]: + return "In Flight" + @property def heading(self) -> Heading: return Heading.from_degrees(0) diff --git a/game/transfers.py b/game/transfers.py index d7db80af..862af050 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -349,13 +349,17 @@ class AirliftPlanner: else: transfer = self.transfer + start_type = squadron.location.required_aircraft_start_type + if start_type is None: + start_type = self.game.settings.default_start_type + flight = Flight( self.package, self.game.country_for(squadron.player), squadron, flight_size, FlightType.TRANSPORT, - self.game.settings.default_start_type, + start_type, departure=squadron.location, arrival=squadron.location, divert=None, From a404792bd2fcc45def18a10892fb6832bf3de50f Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 31 Aug 2021 22:10:23 -0700 Subject: [PATCH 24/48] Reset the max flight size when changing squadrons. --- qt_ui/windows/mission/flight/QFlightCreator.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index cc465422..796ca5cd 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -223,6 +223,7 @@ class QFlightCreator(QDialog): def on_squadron_changed(self, index: int) -> None: squadron: Optional[Squadron] = self.squadron_selector.itemData(index) + self.update_max_size(self.squadron_selector.aircraft_available) # Clear the roster first so we return the pilots to the pool. This way if we end # up repopulating from the same squadron we'll get the same pilots back. self.roster_editor.replace(None) From f9f0b429b668a458a7484bd64867bc2648006a1b Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 31 Aug 2021 22:11:59 -0700 Subject: [PATCH 25/48] Set the flight airfields based on the Squadron. --- game/commander/packagebuilder.py | 2 -- game/squadrons/squadron.py | 4 ++++ game/transfers.py | 2 -- gen/aircraft.py | 19 +++++++------------ gen/flights/flight.py | 6 ++---- .../windows/mission/flight/QFlightCreator.py | 2 -- 6 files changed, 13 insertions(+), 22 deletions(-) diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index c990056b..0f84b69a 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -54,8 +54,6 @@ class PackageBuilder: plan.num_aircraft, plan.task, start_type, - departure=airfield, - arrival=airfield, divert=self.find_divert_field(squadron.aircraft, airfield), ) self.package.add_flight(flight) diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 85fc19fc..4cf43cbf 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -308,6 +308,10 @@ class Squadron: def expected_size_next_turn(self) -> int: return self.owned_aircraft + self.pending_deliveries + @property + def arrival(self) -> ControlPoint: + return self.location if self.destination is None else self.destination + def plan_relocation(self, destination: ControlPoint) -> None: if destination == self.location: logging.warning( diff --git a/game/transfers.py b/game/transfers.py index 862af050..0c8c6184 100644 --- a/game/transfers.py +++ b/game/transfers.py @@ -360,8 +360,6 @@ class AirliftPlanner: flight_size, FlightType.TRANSPORT, start_type, - departure=squadron.location, - arrival=squadron.location, divert=None, cargo=transfer, ) diff --git a/gen/aircraft.py b/gen/aircraft.py index 8c392040..2b0747b0 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -606,41 +606,36 @@ class AircraftConflictGenerator: for squadron in control_point.squadrons: try: - self._spawn_unused_at(control_point, country, faction, squadron) + self._spawn_unused_for(squadron, country, faction) except NoParkingSlotError: # If we run out of parking, stop spawning aircraft. return - def _spawn_unused_at( - self, - control_point: Airfield, - country: Country, - faction: Faction, - squadron: Squadron, + def _spawn_unused_for( + self, squadron: Squadron, country: Country, faction: Faction ) -> None: + assert isinstance(squadron.location, Airfield) for _ in range(squadron.untasked_aircraft): # Creating a flight even those this isn't a fragged mission lets us # reuse the existing debriefing code. # TODO: Special flight type? flight = Flight( - Package(control_point), + Package(squadron.location), faction.country, squadron, 1, FlightType.BARCAP, "Cold", - departure=control_point, - arrival=control_point, divert=None, ) group = self._generate_at_airport( - name=namegen.next_aircraft_name(country, control_point.id, flight), + name=namegen.next_aircraft_name(country, flight.departure.id, flight), side=country, unit_type=squadron.aircraft.dcs_unit_type, count=1, start_type="Cold", - airport=control_point.airport, + airport=squadron.location.airport, ) self._setup_livery(flight, group) diff --git a/gen/flights/flight.py b/gen/flights/flight.py index a4a2b427..b37a3d11 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -280,8 +280,6 @@ class Flight: count: int, flight_type: FlightType, start_type: str, - departure: ControlPoint, - arrival: ControlPoint, divert: Optional[ControlPoint], custom_name: Optional[str] = None, cargo: Optional[TransferOrder] = None, @@ -295,8 +293,8 @@ class Flight: self.roster = FlightRoster(self.squadron, initial_size=count) else: self.roster = roster - self.departure = departure - self.arrival = arrival + self.departure = self.squadron.location + self.arrival = self.squadron.arrival self.divert = divert self.flight_type = flight_type # TODO: Replace with FlightPlan. diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index 796ca5cd..fbbacab0 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -182,8 +182,6 @@ class QFlightCreator(QDialog): roster.max_size, task, self.start_type.currentText(), - squadron.location, - squadron.location, divert, custom_name=self.custom_name_text, roster=roster, From 1a4be911c06432921dff5b86d11fa75dbefb6968 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 31 Aug 2021 22:14:32 -0700 Subject: [PATCH 26/48] Implement ferry flights for squadron transfers. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1145 --- changelog.md | 2 +- game/squadrons/squadron.py | 68 ++++++++++++++++++++++++++---- gen/aircraft.py | 20 +++++++++ gen/ato.py | 1 + gen/flights/flight.py | 1 + gen/flights/flightplan.py | 73 ++++++++++++++++++++++++++++++++- qt_ui/windows/AirWingDialog.py | 17 ++++++-- qt_ui/windows/SquadronDialog.py | 13 ++++-- 8 files changed, 179 insertions(+), 16 deletions(-) diff --git a/changelog.md b/changelog.md index 6530da79..a1167905 100644 --- a/changelog.md +++ b/changelog.md @@ -7,7 +7,7 @@ Saves from 4.x are not compatible with 5.0. * **[Campaign]** Weather! Theaters now experience weather that is more realistic for the region and its current season. For example, Persian Gulf will have very hot, sunny summers and Marianas will experience lots of rain during fall. These changes affect pressure, temperature, clouds and precipitation. Additionally, temperature will drop during the night, by an amount that is somewhat realistic for the region. * **[Campaign]** Weapon data such as fallbacks and introduction years is now moddable. Due to the new architecture to support this, the old data was not automatically migrated. * **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts. -* **[Campaign]** (WIP) Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/issues/1145 for status. +* **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/discussions/1550 for details. * **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers. * **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI. diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 4cf43cbf..b9b242af 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -11,17 +11,19 @@ from typing import ( from faker import Faker -from game.dcs.aircrafttype import AircraftType from game.settings import AutoAtoBehavior, Settings -from game.squadrons.operatingbases import OperatingBases -from game.squadrons.pilot import Pilot, PilotStatus -from game.squadrons.squadrondef import SquadronDef +from gen.ato import Package +from gen.flights.flight import FlightType, Flight +from gen.flights.flightplan import FlightPlanBuilder +from .pilot import Pilot, PilotStatus if TYPE_CHECKING: from game import Game from game.coalition import Coalition - from gen.flights.flight import FlightType - from game.theater import ControlPoint + from game.dcs.aircrafttype import AircraftType + from game.theater import ControlPoint, ConflictTheater + from .operatingbases import OperatingBases + from .squadrondef import SquadronDef @dataclass @@ -312,7 +314,9 @@ class Squadron: def arrival(self) -> ControlPoint: return self.location if self.destination is None else self.destination - def plan_relocation(self, destination: ControlPoint) -> None: + def plan_relocation( + self, destination: ControlPoint, theater: ConflictTheater + ) -> None: if destination == self.location: logging.warning( f"Attempted to plan relocation of {self} to current location " @@ -331,6 +335,7 @@ class Squadron: if not destination.can_operate(self.aircraft): raise RuntimeError(f"{self} cannot operate at {destination}.") self.destination = destination + self.replan_ferry_flights(theater) def cancel_relocation(self) -> None: if self.destination is None: @@ -343,6 +348,55 @@ class Squadron: if self.expected_size_next_turn >= self.location.unclaimed_parking(): raise RuntimeError(f"Not enough parking for {self} at {self.location}.") self.destination = None + self.cancel_ferry_flights() + + def replan_ferry_flights(self, theater: ConflictTheater) -> None: + self.cancel_ferry_flights() + self.plan_ferry_flights(theater) + + def cancel_ferry_flights(self) -> None: + for package in self.coalition.ato.packages: + # Copy the list so our iterator remains consistent throughout the removal. + for flight in list(package.flights): + if flight.squadron == self and flight.flight_type is FlightType.FERRY: + package.remove_flight(flight) + flight.return_pilots_and_aircraft() + if not package.flights: + self.coalition.ato.remove_package(package) + + def plan_ferry_flights(self, theater: ConflictTheater) -> None: + if self.destination is None: + raise RuntimeError( + f"Cannot plan ferry flights for {self} because there is no destination." + ) + package = Package(self.destination) + builder = FlightPlanBuilder(package, self.coalition, theater) + remaining = self.untasked_aircraft + while remaining: + size = min(remaining, self.aircraft.max_group_size) + self.plan_ferry_flight(builder, package, size) + remaining -= size + package.set_tot_asap() + self.coalition.ato.add_package(package) + + def plan_ferry_flight( + self, builder: FlightPlanBuilder, package: Package, size: int + ) -> None: + start_type = self.location.required_aircraft_start_type + if start_type is None: + start_type = self.settings.default_start_type + + flight = Flight( + package, + self.coalition.country_name, + self, + size, + FlightType.FERRY, + start_type, + divert=None, + ) + package.add_flight(flight) + builder.populate_flight_plan(flight) @classmethod def create_from( diff --git a/gen/aircraft.py b/gen/aircraft.py index 2b0747b0..ea775f9d 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -57,6 +57,7 @@ from dcs.task import ( Transport, WeaponType, TargetType, + Nothing, ) from dcs.terrain.terrain import Airport, NoParkingSlotError from dcs.triggers import Event, TriggerOnce, TriggerRule @@ -1086,6 +1087,23 @@ class AircraftConflictGenerator: restrict_jettison=True, ) + def configure_ferry( + self, + group: FlyingGroup[Any], + package: Package, + flight: Flight, + dynamic_runways: Dict[str, RunwayData], + ) -> None: + group.task = Nothing.name + self._setup_group(group, package, flight, dynamic_runways) + self.configure_behavior( + flight, + group, + react_on_threat=OptReactOnThreat.Values.EvadeFire, + roe=OptROE.Values.WeaponHold, + restrict_jettison=True, + ) + def configure_unknown_task(self, group: FlyingGroup[Any], flight: Flight) -> None: logging.error(f"Unhandled flight type: {flight.flight_type}") self.configure_behavior(flight, group) @@ -1130,6 +1148,8 @@ class AircraftConflictGenerator: self.configure_oca_strike(group, package, flight, dynamic_runways) elif flight_type == FlightType.TRANSPORT: self.configure_transport(group, package, flight, dynamic_runways) + elif flight_type == FlightType.FERRY: + self.configure_ferry(group, package, flight, dynamic_runways) else: self.configure_unknown_task(group, flight) diff --git a/gen/ato.py b/gen/ato.py index 944cf316..ec62fc1f 100644 --- a/gen/ato.py +++ b/gen/ato.py @@ -183,6 +183,7 @@ class Package: FlightType.TARCAP, FlightType.BARCAP, FlightType.AEWC, + FlightType.FERRY, FlightType.REFUELING, FlightType.SWEEP, FlightType.ESCORT, diff --git a/gen/flights/flight.py b/gen/flights/flight.py index b37a3d11..bf520c3a 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -70,6 +70,7 @@ class FlightType(Enum): TRANSPORT = "Transport" SEAD_ESCORT = "SEAD Escort" REFUELING = "Refueling" + FERRY = "Ferry" def __str__(self) -> str: return self.value diff --git a/gen/flights/flightplan.py b/gen/flights/flightplan.py index 032c84e2..e471e44c 100644 --- a/gen/flights/flightplan.py +++ b/gen/flights/flightplan.py @@ -37,10 +37,8 @@ from game.theater.theatergroundobject import ( NavalGroundObject, BuildingGroundObject, ) - from game.threatzones import ThreatZones from game.utils import Distance, Heading, Speed, feet, meters, nautical_miles, knots - from .closestairfields import ObjectiveDistanceCache from .flight import Flight, FlightType, FlightWaypoint, FlightWaypointType from .traveltime import GroundSpeed, TravelTime @@ -836,6 +834,39 @@ class AirliftFlightPlan(FlightPlan): return self.package.time_over_target +@dataclass(frozen=True) +class FerryFlightPlan(FlightPlan): + takeoff: FlightWaypoint + nav_to_destination: list[FlightWaypoint] + land: FlightWaypoint + divert: Optional[FlightWaypoint] + bullseye: FlightWaypoint + + def iter_waypoints(self) -> Iterator[FlightWaypoint]: + yield self.takeoff + yield from self.nav_to_destination + yield self.land + if self.divert is not None: + yield self.divert + yield self.bullseye + + @property + def tot_waypoint(self) -> Optional[FlightWaypoint]: + return self.land + + def tot_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: + # TOT planning isn't really useful for ferries. They're behind the front + # lines so no need to wait for escorts or for other missions to complete. + return None + + def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> Optional[timedelta]: + return None + + @property + def mission_departure_time(self) -> timedelta: + return self.package.time_over_target + + @dataclass(frozen=True) class CustomFlightPlan(FlightPlan): custom_waypoints: List[FlightWaypoint] @@ -958,6 +989,8 @@ class FlightPlanBuilder: return self.generate_transport(flight) elif task == FlightType.REFUELING: return self.generate_refueling_racetrack(flight) + elif task == FlightType.FERRY: + return self.generate_ferry(flight) raise PlanningError(f"{task} flight plan generation not implemented") def regenerate_package_waypoints(self) -> None: @@ -1244,6 +1277,42 @@ class FlightPlanBuilder: bullseye=builder.bullseye(), ) + def generate_ferry(self, flight: Flight) -> FerryFlightPlan: + """Generate a ferry flight at a given location. + + Args: + flight: The flight to generate the flight plan for. + """ + + if flight.departure == flight.arrival: + raise PlanningError( + f"Cannot plan ferry flight: departure and arrival are both " + f"{flight.departure}" + ) + + altitude_is_agl = flight.unit_type.dcs_unit_type.helicopter + altitude = ( + feet(1500) + if altitude_is_agl + else flight.unit_type.preferred_patrol_altitude + ) + + builder = WaypointBuilder(flight, self.coalition) + return FerryFlightPlan( + package=self.package, + flight=flight, + takeoff=builder.takeoff(flight.departure), + nav_to_destination=builder.nav_path( + flight.departure.position, + flight.arrival.position, + altitude, + altitude_is_agl, + ), + land=builder.land(flight.arrival), + divert=builder.divert(flight.divert), + bullseye=builder.bullseye(), + ) + def cap_racetrack_for_objective( self, location: MissionTarget, barcap: bool ) -> Tuple[Point, Point]: diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index 9fcd1c7b..b90e826b 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -20,7 +20,7 @@ from game.squadrons import Squadron from game.theater import ConflictTheater from gen.flights.flight import Flight from qt_ui.delegates import TwoColumnRowDelegate -from qt_ui.models import GameModel, AirWingModel, SquadronModel +from qt_ui.models import GameModel, AirWingModel, SquadronModel, AtoModel from qt_ui.windows.SquadronDialog import SquadronDialog @@ -57,8 +57,14 @@ class SquadronDelegate(TwoColumnRowDelegate): class SquadronList(QListView): """List view for displaying the air wing's squadrons.""" - def __init__(self, air_wing_model: AirWingModel, theater: ConflictTheater) -> None: + def __init__( + self, + ato_model: AtoModel, + air_wing_model: AirWingModel, + theater: ConflictTheater, + ) -> None: super().__init__() + self.ato_model = ato_model self.air_wing_model = air_wing_model self.theater = theater self.dialog: Optional[SquadronDialog] = None @@ -78,6 +84,7 @@ class SquadronList(QListView): if not index.isValid(): return self.dialog = SquadronDialog( + self.ato_model, SquadronModel(self.air_wing_model.squadron_at_index(index)), self.theater, self, @@ -199,7 +206,11 @@ class AirWingTabs(QTabWidget): super().__init__() self.addTab( - SquadronList(game_model.blue_air_wing_model, game_model.game.theater), + SquadronList( + game_model.ato_model, + game_model.blue_air_wing_model, + game_model.game.theater, + ), "Squadrons", ) self.addTab(AirInventoryView(game_model), "Inventory") diff --git a/qt_ui/windows/SquadronDialog.py b/qt_ui/windows/SquadronDialog.py index 24b004cc..aafad8b8 100644 --- a/qt_ui/windows/SquadronDialog.py +++ b/qt_ui/windows/SquadronDialog.py @@ -24,7 +24,7 @@ from game.theater import ControlPoint, ConflictTheater from gen.flights.flight import FlightType from qt_ui.delegates import TwoColumnRowDelegate from qt_ui.errorreporter import report_errors -from qt_ui.models import SquadronModel +from qt_ui.models import SquadronModel, AtoModel class PilotDelegate(TwoColumnRowDelegate): @@ -135,10 +135,16 @@ class SquadronDialog(QDialog): """Dialog window showing a squadron.""" def __init__( - self, squadron_model: SquadronModel, theater: ConflictTheater, parent + self, + ato_model: AtoModel, + squadron_model: SquadronModel, + theater: ConflictTheater, + parent, ) -> None: super().__init__(parent) + self.ato_model = ato_model self.squadron_model = squadron_model + self.theater = theater self.setMinimumSize(1000, 440) self.setWindowTitle(str(squadron_model.squadron)) @@ -194,7 +200,8 @@ class SquadronDialog(QDialog): if destination is None: self.squadron.cancel_relocation() else: - self.squadron.plan_relocation(destination) + self.squadron.plan_relocation(destination, self.theater) + self.ato_model.replace_from_game(player=True) def check_disabled_button_states( self, button: QPushButton, index: QModelIndex From 90a8bb63dc02c7364d140d6df47ff2a4af8e14e4 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 31 Aug 2021 23:03:46 -0700 Subject: [PATCH 27/48] Fix AI landing behavior. The landing waypoints need the airdrome_id field set to actually associate with the airfield. Without this ferry flights will take off and immediately land at their departure airfield. --- changelog.md | 1 + game/theater/controlpoint.py | 8 ++++++++ gen/aircraft.py | 4 ++++ gen/flights/flight.py | 4 ++++ gen/flights/waypointbuilder.py | 13 +++++++++++-- 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/changelog.md b/changelog.md index a1167905..5f37d371 100644 --- a/changelog.md +++ b/changelog.md @@ -27,6 +27,7 @@ Saves from 4.x are not compatible with 5.0. * **[Campaign]** Naval control points will no longer claim ground objectives during campaign generation and prevent them from spawning. * **[Mission Generation]** Mission results and other files will now be opened with enforced utf-8 encoding to prevent an issue where destroyed ground units were untracked because of special characters in their names. +* **[Mission Generation]** Fixed generation of landing waypoints so that the AI obeys them. * **[UI]** Selling of Units is now visible again in the UI dialog and shows the correct amount of sold units * **[UI]** Fixed bug where an incompatible campaign could be generated if no action is taken on the campaign selection screen. diff --git a/game/theater/controlpoint.py b/game/theater/controlpoint.py index 27f7aa14..90ae2650 100644 --- a/game/theater/controlpoint.py +++ b/game/theater/controlpoint.py @@ -687,6 +687,10 @@ class ControlPoint(MissionTarget, ABC): ) -> RunwayData: ... + @property + def airdrome_id_for_landing(self) -> Optional[int]: + return None + @property def parking_slots(self) -> Iterator[ParkingSlot]: yield from [] @@ -904,6 +908,10 @@ class Airfield(ControlPoint): assigner = RunwayAssigner(conditions) return assigner.get_preferred_runway(self.airport) + @property + def airdrome_id_for_landing(self) -> Optional[int]: + return self.airport.id + @property def parking_slots(self) -> Iterator[ParkingSlot]: yield from self.airport.parking_slots diff --git a/gen/aircraft.py b/gen/aircraft.py index ea775f9d..5588701f 100644 --- a/gen/aircraft.py +++ b/gen/aircraft.py @@ -1736,6 +1736,8 @@ class LandingPointBuilder(PydcsWaypointBuilder): waypoint = super().build() waypoint.type = "Land" waypoint.action = PointAction.Landing + if (control_point := self.waypoint.control_point) is not None: + waypoint.airdrome_id = control_point.airdrome_id_for_landing return waypoint @@ -1745,6 +1747,8 @@ class CargoStopBuilder(PydcsWaypointBuilder): waypoint.type = "LandingReFuAr" waypoint.action = PointAction.LandingReFuAr waypoint.landing_refuel_rearm_time = 2 # Minutes. + if (control_point := self.waypoint.control_point) is not None: + waypoint.airdrome_id = control_point.airdrome_id_for_landing return waypoint diff --git a/gen/flights/flight.py b/gen/flights/flight.py index bf520c3a..c0766b39 100644 --- a/gen/flights/flight.py +++ b/gen/flights/flight.py @@ -160,6 +160,7 @@ class FlightWaypoint: x: float, y: float, alt: Distance = meters(0), + control_point: Optional[ControlPoint] = None, ) -> None: """Creates a flight waypoint. @@ -169,11 +170,14 @@ class FlightWaypoint: y: Y coordinate of the waypoint. alt: Altitude of the waypoint. By default this is MSL, but it can be changed to AGL by setting alt_type to "RADIO" + control_point: The control point to associate with this waypoint. Needed for + landing points. """ self.waypoint_type = waypoint_type self.x = x self.y = y self.alt = alt + self.control_point = control_point self.alt_type = "BARO" self.name = "" # TODO: Merge with pretty_name. diff --git a/gen/flights/waypointbuilder.py b/gen/flights/waypointbuilder.py index 3c5851fd..0b6b4810 100644 --- a/gen/flights/waypointbuilder.py +++ b/gen/flights/waypointbuilder.py @@ -110,7 +110,11 @@ class WaypointBuilder: waypoint.pretty_name = "Exit theater" else: waypoint = FlightWaypoint( - FlightWaypointType.LANDING_POINT, position.x, position.y, meters(0) + FlightWaypointType.LANDING_POINT, + position.x, + position.y, + meters(0), + control_point=arrival, ) waypoint.name = "LANDING" waypoint.alt_type = "RADIO" @@ -139,7 +143,11 @@ class WaypointBuilder: altitude_type = "RADIO" waypoint = FlightWaypoint( - FlightWaypointType.DIVERT, position.x, position.y, altitude + FlightWaypointType.DIVERT, + position.x, + position.y, + altitude, + control_point=divert, ) waypoint.alt_type = altitude_type waypoint.name = "DIVERT" @@ -488,6 +496,7 @@ class WaypointBuilder: control_point.position.x, control_point.position.y, meters(0), + control_point=control_point, ) waypoint.alt_type = "RADIO" waypoint.name = "DROP OFF" From c252fd6a778a7d7ef9a52c384c3eda821a93a2ff Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Tue, 31 Aug 2021 23:13:02 -0700 Subject: [PATCH 28/48] Add a ferry loadout for the viper. --- resources/customized_payloads/F-16C_50.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/resources/customized_payloads/F-16C_50.lua b/resources/customized_payloads/F-16C_50.lua index 447354e8..7fc7aee7 100644 --- a/resources/customized_payloads/F-16C_50.lua +++ b/resources/customized_payloads/F-16C_50.lua @@ -270,6 +270,25 @@ local unitPayloads = { [1] = 31, }, }, + [7] = { + ["name"] = "Liberation Ferry", + ["pylons"] = { + [1] = { + ["CLSID"] = "MXU-648-TP", + ["num"] = 5, + }, + [2] = { + ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", + ["num"] = 4, + }, + [3] = { + ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", + ["num"] = 6, + }, + }, + ["tasks"] = { + }, + }, }, ["unitType"] = "F-16C_50", } From 15ce48e7126ea8a41d6f8b7b375da53daa4bc338 Mon Sep 17 00:00:00 2001 From: Magnus Wolffelt Date: Wed, 1 Sep 2021 21:56:11 +0200 Subject: [PATCH 29/48] Arm the ferry viper :) (#1584) --- resources/customized_payloads/F-16C_50.lua | 26 +++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/resources/customized_payloads/F-16C_50.lua b/resources/customized_payloads/F-16C_50.lua index 7fc7aee7..4f8a10dd 100644 --- a/resources/customized_payloads/F-16C_50.lua +++ b/resources/customized_payloads/F-16C_50.lua @@ -274,17 +274,33 @@ local unitPayloads = { ["name"] = "Liberation Ferry", ["pylons"] = { [1] = { - ["CLSID"] = "MXU-648-TP", - ["num"] = 5, + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 9, }, [2] = { - ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", - ["num"] = 4, + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 8, }, [3] = { - ["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}", + ["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}", + ["num"] = 1, + }, + [4] = { + ["CLSID"] = "{5CE2FF2A-645A-4197-B48D-8720AC69394F}", + ["num"] = 2, + }, + [5] = { + ["CLSID"] = "MXU-648-TP", ["num"] = 6, }, + [6] = { + ["CLSID"] = "MXU-648-TP", + ["num"] = 4, + }, + [7] = { + ["CLSID"] = "{8A0BE8AE-58D4-4572-9263-3144C0D06364}", + ["num"] = 5, + }, }, ["tasks"] = { }, From 8a60fa5c833111cfa6f6c74e27fdf04532e9abd8 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 1 Sep 2021 17:14:51 -0700 Subject: [PATCH 30/48] Fix errors when changing task or aircraft type. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1587 --- qt_ui/windows/mission/flight/QFlightCreator.py | 9 ++------- .../windows/mission/flight/settings/QFlightSlotEditor.py | 2 ++ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/qt_ui/windows/mission/flight/QFlightCreator.py b/qt_ui/windows/mission/flight/QFlightCreator.py index fbbacab0..c432a845 100644 --- a/qt_ui/windows/mission/flight/QFlightCreator.py +++ b/qt_ui/windows/mission/flight/QFlightCreator.py @@ -85,7 +85,7 @@ class QFlightCreator(QDialog): squadron, initial_size=self.flight_size_spinner.value() ) self.roster_editor = FlightRosterEditor(roster) - self.flight_size_spinner.valueChanged.connect(self.resize_roster) + self.flight_size_spinner.valueChanged.connect(self.roster_editor.resize) self.squadron_selector.currentIndexChanged.connect(self.on_squadron_changed) roster_layout = QHBoxLayout() layout.addLayout(roster_layout) @@ -136,10 +136,6 @@ class QFlightCreator(QDialog): def set_custom_name_text(self, text: str): self.custom_name_text = text - def resize_roster(self, new_size: int) -> None: - self.roster_editor.roster.resize(new_size) - self.roster_editor.resize(new_size) - def verify_form(self) -> Optional[str]: aircraft: Optional[Type[FlyingType]] = self.aircraft_selector.currentData() squadron: Optional[Squadron] = self.squadron_selector.currentData() @@ -196,7 +192,6 @@ class QFlightCreator(QDialog): self.squadron_selector.update_items( self.task_selector.currentData(), new_aircraft ) - self.departure.change_aircraft(new_aircraft) self.divert.change_aircraft(new_aircraft) def on_departure_changed(self, departure: ControlPoint) -> None: @@ -229,7 +224,7 @@ class QFlightCreator(QDialog): self.roster_editor.replace( FlightRoster(squadron, self.flight_size_spinner.value()) ) - self.on_departure_changed(squadron.location) + self.on_departure_changed(squadron.location) def update_max_size(self, available: int) -> None: aircraft = self.aircraft_selector.currentData() diff --git a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py index 665e45ae..2aa0db27 100644 --- a/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py +++ b/qt_ui/windows/mission/flight/settings/QFlightSlotEditor.py @@ -176,6 +176,8 @@ class FlightRosterEditor(QVBoxLayout): def resize(self, new_size: int) -> None: if new_size > self.MAX_PILOTS: raise ValueError("A flight may not have more than four pilots.") + if self.roster is not None: + self.roster.resize(new_size) for controls in self.pilot_controls[:new_size]: controls.enable_and_reset() for controls in self.pilot_controls[new_size:]: From 9c3171f1ce5e4d1627681b952d308189c796323a Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 1 Sep 2021 18:01:53 -0700 Subject: [PATCH 31/48] Sort the animal names list. Make it easier to figure out what's already there. --- gen/naming.py | 372 +++++++++++++++++++++++++------------------------- 1 file changed, 186 insertions(+), 186 deletions(-) diff --git a/gen/naming.py b/gen/naming.py index 9f820a86..fe91e85b 100644 --- a/gen/naming.py +++ b/gen/naming.py @@ -43,209 +43,209 @@ ALPHA_MILITARY = [ ] ANIMALS: tuple[str, ...] = ( - "SHARK", - "TORTOISE", - "BAT", - "PANGOLIN", "AARDWOLF", - "MONKEY", - "BUFFALO", - "DOG", - "BOBCAT", - "LYNX", - "PANTHER", - "TIGER", - "LION", - "OWL", - "BUTTERFLY", - "BISON", - "DUCK", - "COBRA", - "MAMBA", - "DOLPHIN", - "PHEASANT", + "ALBATROSS", + "ALLIGATOR", + "ALPACA", + "ANACONDA", + "ANTELOPE", + "ARGALI", "ARMADILLO", - "RACOON", - "ZEBRA", + "BABOON", + "BADGER", + "BANDICOOT", + "BARRACUDA", + "BASILISK", + "BAT", + "BEAR", + "BISON", + "BOBCAT", + "BONGO", + "BUFFALO", + "BULLDOG", + "BUMBLEBEE", + "BUNNY", + "BUTTERFLY", + "CAIMAN", + "CAMEL", + "CANARY", + "CAPYBARA", + "CARACAL", + "CASTOR", + "CAT", + "CATERPILLAR", + "CATFISH", + "CENTIPEDE", + "CHAMELEON", + "CHEETAH", + "CHICKEN", + "COBRA", + "COLT", + "CORAL", + "CORGI", + "COTTONMOUTH", "COW", "COYOTE", - "FOX", - "LIGHTFOOT", - "COTTONMOUTH", - "TAURUS", - "VIPER", - "CASTOR", - "GIRAFFE", - "SNAKE", - "MONSTER", - "ALBATROSS", - "HAWK", - "DOVE", - "MOCKINGBIRD", - "GECKO", - "ORYX", - "GORILLA", - "HARAMBE", - "GOOSE", - "MAVERICK", - "HARE", - "JACKAL", - "LEOPARD", - "CAT", - "MUSK", - "ORCA", - "OCELOT", - "BEAR", - "PANDA", - "GULL", - "PENGUIN", - "PYTHON", - "RAVEN", - "DEER", - "MOOSE", - "REINDEER", - "SHEEP", - "GAZELLE", - "INSECT", - "VULTURE", - "WALLABY", - "KANGAROO", - "KOALA", - "KIWI", - "WHALE", - "FISH", - "RHINO", - "HIPPO", - "RAT", - "WOODPECKER", - "WORM", - "BABOON", - "YAK", - "SCORPIO", - "HORSE", - "POODLE", - "CENTIPEDE", - "CHICKEN", - "CHEETAH", - "CHAMELEON", - "CATFISH", - "CATERPILLAR", - "CARACAL", - "CAMEL", - "CAIMAN", - "BARRACUDA", - "BANDICOOT", - "ALLIGATOR", - "BONGO", - "CORAL", - "ELEPHANT", - "ANTELOPE", "CRAB", + "CROW", "DACHSHUND", + "DEER", + "DINGO", "DODO", - "FLAMINGO", - "FERRET", - "FALCON", - "BULLDOG", + "DOG", + "DOLPHIN", "DONKEY", - "IGUANA", - "TAMARIN", - "HARRIER", - "GRIZZLY", - "GREYHOUND", - "GRASSHOPPER", - "JAGUAR", - "LADYBUG", - "KOMODO", + "DOVE", "DRAGON", + "DUCK", + "ELEPHANT", + "ELK", + "FALCON", + "FAWN", + "FENNEC", + "FERRET", + "FINCH", + "FISH", + "FLAMINGO", + "FOX", + "FROG", + "GAZELLE", + "GECKO", + "GIRAFFE", + "GOOSE", + "GOPHER", + "GORILLA", + "GRASSHOPPER", + "GREYHOUND", + "GRIZZLY", + "GUANACO", + "GULL", + "HAMSTER", + "HARAMBE", + "HARE", + "HARRIER", + "HAWK", + "HEDGEHOG", + "HIPPO", + "HORSE", + "HUSKY", + "IGUANA", + "IMPALA", + "INSECT", + "JACKAL", + "JAGUAR", + "JELLYFISH", + "JERBOA", + "KANGAROO", + "KITTEN", + "KIWI", + "KOALA", + "KOMODO", + "LADYBUG", + "LEOPARD", + "LIGHTFOOT", + "LION", "LIZARD", "LLAMA", "LOBSTER", - "OCTOPUS", - "MANATEE", - "MAGPIE", - "MACAW", - "OSTRICH", - "OYSTER", - "MOLE", - "MULE", - "MOTH", - "MONGOOSE", - "MOLLY", - "MEERKAT", - "MOUSE", - "PEACOCK", - "PIKE", - "ROBIN", - "RAGDOLL", - "PLATYPUS", - "PELICAN", - "PARROT", - "PORCUPINE", - "PIRANHA", - "PUMA", - "PUG", - "TAPIR", - "TERMITE", - "URCHIN", - "SHRIMP", - "TURKEY", - "TOUCAN", - "TETRA", - "HUSKY", - "STARFISH", - "SWAN", - "FROG", - "SQUIRREL", - "WALRUS", - "WARTHOG", - "CORGI", - "WEASEL", - "WOMBAT", - "WOLVERINE", - "MAMMOTH", - "TOAD", - "WOLF", - "ZEBU", - "SEAL", - "SKATE", - "JELLYFISH", - "MOSQUITO", "LOCUST", + "LYNX", + "MACAW", + "MAGPIE", + "MAMBA", + "MAMMOTH", + "MANATEE", + "MARE", + "MAVERICK", + "MEERKAT", + "MINK", + "MOCKINGBIRD", + "MOLE", + "MOLLY", + "MONGOOSE", + "MONKEY", + "MONSTER", + "MOOSE", + "MOSQUITO", + "MOTH", + "MOUSE", + "MULE", + "MUSK", + "OCELOT", + "OCTOPUS", + "ORCA", + "ORYX", + "OSTRICH", + "OTTER", + "OWL", + "OX", + "OYSTER", + "PANDA", + "PANGOLIN", + "PANTHER", + "PARROT", + "PEACOCK", + "PELICAN", + "PENGUIN", + "PHEASANT", + "PIGLET", + "PIKE", + "PIRANHA", + "PLATYPUS", + "POODLE", + "PORCUPINE", + "PRONGHORN", + "PUG", + "PUMA", + "PYTHON", + "RACOON", + "RAGDOLL", + "RAT", + "RAVEN", + "REINDEER", + "RHINO", + "ROBIN", + "SCORPIO", + "SEAL", + "SHARK", + "SHEEP", + "SHRIMP", + "SKATE", + "SKUNK", "SLUG", "SNAIL", - "HEDGEHOG", - "PIGLET", - "FENNEC", - "BADGER", - "ALPACA", - "DINGO", - "COLT", - "SKUNK", - "BUNNY", - "IMPALA", - "GUANACO", - "CAPYBARA", - "ELK", - "MINK", - "PRONGHORN", - "CROW", - "BUMBLEBEE", - "FAWN", - "OTTER", + "SNAKE", + "SQUIRREL", + "STARFISH", + "SWAN", + "TAMARIN", + "TAPIR", + "TAURUS", + "TERMITE", + "TETRA", + "TIGER", + "TOAD", + "TORTOISE", + "TOUCAN", + "TURKEY", + "URCHIN", + "VIPER", + "VULTURE", + "WALLABY", + "WALRUS", + "WARTHOG", "WATERBUCK", - "JERBOA", - "KITTEN", - "ARGALI", - "OX", - "MARE", - "FINCH", - "BASILISK", - "GOPHER", - "HAMSTER", - "CANARY", + "WEASEL", + "WHALE", + "WOLF", + "WOLVERINE", + "WOMBAT", "WOODCHUCK", - "ANACONDA", + "WOODPECKER", + "WORM", + "YAK", + "ZEBRA", + "ZEBU", ) From 16d397db1c53c8574f482a5f94372f7fb51417d7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 1 Sep 2021 19:18:19 -0700 Subject: [PATCH 32/48] Fix unit info menus for aircraft. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1586 --- game/purchaseadapter.py | 14 +++++++++++++- qt_ui/windows/basemenu/UnitTransactionFrame.py | 6 ++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/game/purchaseadapter.py b/game/purchaseadapter.py index 7da8e4e9..03987f46 100644 --- a/game/purchaseadapter.py +++ b/game/purchaseadapter.py @@ -1,9 +1,11 @@ from abc import abstractmethod -from typing import TypeVar, Generic +from typing import TypeVar, Generic, Any from game import Game from game.coalition import Coalition +from game.dcs.aircrafttype import AircraftType from game.dcs.groundunittype import GroundUnitType +from game.dcs.unittype import UnitType from game.squadrons import Squadron from game.theater import ControlPoint @@ -90,6 +92,10 @@ class PurchaseAdapter(Generic[ItemType]): def name_of(self, item: ItemType, multiline: bool = False) -> str: ... + @abstractmethod + def unit_type_of(self, item: ItemType) -> UnitType[Any]: + ... + class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]): def __init__(self, control_point: ControlPoint) -> None: @@ -132,6 +138,9 @@ class AircraftPurchaseAdapter(PurchaseAdapter[Squadron]): separator = " " return separator.join([item.aircraft.name, str(item)]) + def unit_type_of(self, item: Squadron) -> AircraftType: + return item.aircraft + class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]): def __init__( @@ -172,3 +181,6 @@ class GroundUnitPurchaseAdapter(PurchaseAdapter[GroundUnitType]): def name_of(self, item: GroundUnitType, multiline: bool = False) -> str: return f"{item}" + + def unit_type_of(self, item: GroundUnitType) -> GroundUnitType: + return item diff --git a/qt_ui/windows/basemenu/UnitTransactionFrame.py b/qt_ui/windows/basemenu/UnitTransactionFrame.py index d9cbe57f..f5ad347c 100644 --- a/qt_ui/windows/basemenu/UnitTransactionFrame.py +++ b/qt_ui/windows/basemenu/UnitTransactionFrame.py @@ -273,6 +273,8 @@ class UnitTransactionFrame(QFrame, Generic[TransactionItemType]): else: return "Unit can not be sold." - def info(self, unit_type: UnitType) -> None: - self.info_window = QUnitInfoWindow(self.game_model.game, unit_type) + def info(self, item: TransactionItemType) -> None: + self.info_window = QUnitInfoWindow( + self.game_model.game, self.purchase_adapter.unit_type_of(item) + ) self.info_window.show() From 2c8f960696e2f7a764ba3e43daff5be064d5d0b7 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Wed, 1 Sep 2021 19:19:57 -0700 Subject: [PATCH 33/48] Prevent creating empty ferry packages. An empty squadron or a fully-assigned squadron won't have anything to assign to the ferry mission. Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1588 --- game/squadrons/squadron.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index b9b242af..607a8d8a 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -369,9 +369,12 @@ class Squadron: raise RuntimeError( f"Cannot plan ferry flights for {self} because there is no destination." ) + remaining = self.untasked_aircraft + if not remaining: + return + package = Package(self.destination) builder = FlightPlanBuilder(package, self.coalition, theater) - remaining = self.untasked_aircraft while remaining: size = min(remaining, self.aircraft.max_group_size) self.plan_ferry_flight(builder, package, size) From 24a0211d8cc66f473642b9ec3f480d4ea38c2dd2 Mon Sep 17 00:00:00 2001 From: Starfire13 <72491792+Starfire13@users.noreply.github.com> Date: Thu, 2 Sep 2021 19:43:32 +1000 Subject: [PATCH 34/48] Update naming.py --- gen/naming.py | 199 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/gen/naming.py b/gen/naming.py index fe91e85b..afb805c5 100644 --- a/gen/naming.py +++ b/gen/naming.py @@ -43,26 +43,50 @@ ALPHA_MILITARY = [ ] ANIMALS: tuple[str, ...] = ( + "AARDVARK", "AARDWOLF", + "ADDER", + "ALBACORE", "ALBATROSS", "ALLIGATOR", "ALPACA", "ANACONDA", + "ANOLE", + "ANTEATER", "ANTELOPE", + "ANTLION", + "ARAPAIMA", + "ARCHERFISH", "ARGALI", "ARMADILLO", + "ASP", + "AUROCHS", + "AXOLOTL", + "BABIRUSA", "BABOON", "BADGER", "BANDICOOT", "BARRACUDA", + "BARRAMUNDI", "BASILISK", + "BASS", "BAT", "BEAR", + "BEAVER", + "BEETLE", + "BELUGA", + "BETTONG", + "BINTURONG", "BISON", + "BLOODHOUND", + "BOA", "BOBCAT", "BONGO", + "BONITO", "BUFFALO", "BULLDOG", + "BULLFROG", + "BULLSHARK", "BUMBLEBEE", "BUNNY", "BUTTERFLY", @@ -71,6 +95,7 @@ ANIMALS: tuple[str, ...] = ( "CANARY", "CAPYBARA", "CARACAL", + "CARP", "CASTOR", "CAT", "CATERPILLAR", @@ -79,170 +104,342 @@ ANIMALS: tuple[str, ...] = ( "CHAMELEON", "CHEETAH", "CHICKEN", + "CHIMAERA", + "CICADA", + "CICHLID", + "CIVET", + "COBIA", "COBRA", + "COCKATOO", + "COD", + "COELACANTH", "COLT", + "CONDOR", + "COPPERHEAD", "CORAL", "CORGI", "COTTONMOUTH", + "COUGAR", "COW", "COYOTE", "CRAB", + "CRANE", + "CRICKET", + "CROCODILE", "CROW", + "CUTTLEFISH", "DACHSHUND", "DEER", "DINGO", + "DIREWOLF", "DODO", "DOG", "DOLPHIN", "DONKEY", "DOVE", + "DRACO", "DRAGON", + "DRAGONFLY", "DUCK", + "DUGONG", + "EAGLE", + "EARWIG", + "ECHIDNA", + "EEL", "ELEPHANT", "ELK", + "EMU", + "ERMINE", "FALCON", + "FANGTOOTH", "FAWN", "FENNEC", "FERRET", "FINCH", + "FIREFLY", "FISH", "FLAMINGO", + "FLEA", + "FLOUNDER", + "FORGMOUTH", "FOX", + "FRINGEHEAD", "FROG", + "GAR", "GAZELLE", "GECKO", + "GENET", + "GERBIL", + "GHARIAL", + "GIBBON", "GIRAFFE", "GOOSE", "GOPHER", "GORILLA", + "GOSHAWK", "GRASSHOPPER", "GREYHOUND", "GRIZZLY", + "GROUPER", + "GROUSE", + "GRYPHON", "GUANACO", "GULL", + "GUPPY", + "HADDOCK", + "HAGFISH", + "HALIBUT", "HAMSTER", "HARAMBE", "HARE", "HARRIER", "HAWK", "HEDGEHOG", + "HERMITCRAB", + "HERON", + "HERRING", "HIPPO", + "HORNBILL", + "HORNET", "HORSE", + "HUNTSMAN", "HUSKY", + "HYENA", + "IBEX", + "IBIS", "IGUANA", "IMPALA", "INSECT", + "IRUKANDJI", + "ISOPOD", "JACKAL", "JAGUAR", "JELLYFISH", "JERBOA", + "KAKAPO", "KANGAROO", + "KATYDID", + "KEA", + "KINGFISHER", "KITTEN", "KIWI", "KOALA", "KOMODO", + "KRAIT", "LADYBUG", + "LAMPREY", + "LEMUR", "LEOPARD", "LIGHTFOOT", "LION", + "LIONFISH", "LIZARD", "LLAMA", + "LOACH", "LOBSTER", "LOCUST", + "LORIKEET", + "LUNGFISH", "LYNX", "MACAW", "MAGPIE", + "MALLARD", "MAMBA", "MAMMOTH", "MANATEE", + "MANDRILL", + "MANTA", + "MANTIS", "MARE", + "MARLIN", + "MARMOT", + "MARTEN", + "MASTIFF", + "MASTODON", "MAVERICK", + "MAYFLY", "MEERKAT", + "MILLIPEDE", "MINK", + "MOA", "MOCKINGBIRD", "MOLE", + "MOLERAT", "MOLLY", "MONGOOSE", "MONKEY", + "MONKFISH", "MONSTER", "MOOSE", + "MORAY", "MOSQUITO", "MOTH", "MOUSE", + "MUDSKIPPER", "MULE", "MUSK", + "MYNA", + "NARWHAL", + "NAUTILUS", + "NEWT", + "NIGHTINGALE", + "NUMBAT", "OCELOT", "OCTOPUS", + "OKAPI", + "OLM", + "OPAH", + "OPOSSUM", "ORCA", "ORYX", + "OSPREY", "OSTRICH", "OTTER", "OWL", "OX", "OYSTER", + "PADDLEFISH", + "PADEMELON", "PANDA", "PANGOLIN", "PANTHER", + "PARAKEET", "PARROT", "PEACOCK", "PELICAN", "PENGUIN", + "PERCH", + "PEREGRINE", + "PETRAL", "PHEASANT", + "PIG", + "PIGEON", "PIGLET", "PIKE", "PIRANHA", "PLATYPUS", "POODLE", "PORCUPINE", + "PORPOISE", + "POSSUM", + "POTOROO", "PRONGHORN", + "PUFFERFISH", + "PUFFIN", "PUG", "PUMA", "PYTHON", + "QUAGGA", + "QUAIL", + "QUOKKA", + "QUOLL", + "RABBIT", "RACOON", "RAGDOLL", "RAT", + "RATTLESNAKE", "RAVEN", "REINDEER", "RHINO", + "ROACH", "ROBIN", - "SCORPIO", + "SABERTOOTH", + "SAILFISH", + "SALAMANDER", + "SALMON", + "SANDFLY", + "SARDINE", + "SAWFISH", + "SCARAB", + "SCORPION", + "SEAHORSE", "SEAL", + "SEALION", + "SERVAL", "SHARK", "SHEEP", + "SHOEBILL", + "SHRIKE", "SHRIMP", + "SIDEWINDER", + "SILKWORM", "SKATE", + "SKINK", "SKUNK", + "SLOTH", "SLUG", "SNAIL", "SNAKE", + "SNAPPER", + "SNOOK", + "SPARROW", + "SPIDER", + "SPRINGBOK", + "SQUID", "SQUIRREL", + "STAGHORN", "STARFISH", + "STINGRAY", + "STINKBUG", + "STOUT", + "STURGEON", + "SUGARGLIDER", + "SUNBEAR", + "SWALLOW", "SWAN", + "SWIFT", + "SWORDFISH", + "TAIPAN", + "TAKAHE", "TAMARIN", + "TANG", "TAPIR", + "TARANTULA", + "TARPON", + "TARSIER", "TAURUS", "TERMITE", + "TERRIER", "TETRA", + "THRUSH", + "THYLACINE", "TIGER", "TOAD", "TORTOISE", "TOUCAN", + "TREADFIN", + "TREVALLY", + "TRIGGERFISH", + "TROUT", + "TUATARA", + "TUNA", "TURKEY", + "TURTLE", "URCHIN", "VIPER", "VULTURE", "WALLABY", + "WALLAROO", + "WALLEYE", "WALRUS", "WARTHOG", + "WASP", "WATERBUCK", "WEASEL", + "WEEVIL", + "WEKA", "WHALE", + "WILDCAT", + "WILDEBEEST", "WOLF", + "WOLFHOUND", "WOLVERINE", "WOMBAT", "WOODCHUCK", "WOODPECKER", "WORM", + "WRASSE", + "WYVERN", "YAK", "ZEBRA", "ZEBU", From 7d0b3a096ddbd6d343dd03084921c94c31d2981c Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Thu, 2 Sep 2021 22:14:31 -0700 Subject: [PATCH 35/48] Update Abu Dhabi. --- resources/campaigns/battle_of_abu_dhabi.miz | Bin 42833 -> 42897 bytes resources/campaigns/battle_of_abu_dhabi.yaml | 206 ++++++++++++++++++- 2 files changed, 205 insertions(+), 1 deletion(-) diff --git a/resources/campaigns/battle_of_abu_dhabi.miz b/resources/campaigns/battle_of_abu_dhabi.miz index dcc30acffbf65f5e9f658c56fb700d5f553dce36..2248c63277c3fc619f93b1d6008f9705c2f6e871 100644 GIT binary patch delta 27770 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≪BZKLpX4+{ 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%0D`+YH#Bkrxx8dN@IJh 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 A$^ZZW diff --git a/resources/campaigns/battle_of_abu_dhabi.yaml b/resources/campaigns/battle_of_abu_dhabi.yaml index ebd2d5d4..2bebb1db 100644 --- a/resources/campaigns/battle_of_abu_dhabi.yaml +++ b/resources/campaigns/battle_of_abu_dhabi.yaml @@ -7,4 +7,208 @@ recommended_enemy_faction: United Arab Emirates 2015 description:

You have managed to establish a foothold near Ras Al Khaima. Continue pushing south.

miz: battle_of_abu_dhabi.miz performance: 2 -version": "8.0" \ No newline at end of file +version": "9.0" +squadrons: + # Blue CPs: + # The default faction is Iran, but the F-14B is given higher precedence so + # that it is used if the faction is something US. The F-14A will be used if + # the player picks some Iran faction that for some reason has carriers. + + # Bandar Abbas: + # This is the main transit hub for blue, so it contains all the logistics-type + # squadrons: airlift, refueling, and AEW&C. It also contains an air-to-air + # squadron for self defense, a bomber squadron, and some air-to-ground + # squadrons. + # + # Due to its location, this will be the primary airbase for the initial phase + # of the campaign. + 2: + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F-14A Tomcat (Block 135-GR Late) + - primary: DEAD + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F-14A Tomcat (Block 135-GR Late) + - primary: SEAD + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + - F-4E Phantom II + - primary: AEW&C + aircraft: + - E-3A + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - primary: Transport + aircraft: + - C-17A + - primary: Strike + secondary: air-to-ground + aircraft: + - B-1B Lancer + - Su-24MK Fencer-D + + # Kish: + # This airbase has better access to the theater as the front-line moves south + # west. It contains combat squadrons only. + 24: + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-16CM Fighting Falcon (Block 50) + - MiG-29A Fulcrum-A + - primary: CAS + secondary: air-to-ground + aircraft: + - A-10C Thunderbolt II (Suite 7) + - A-10C Thunderbolt II (Suite 3) + - Su-25 Frogfoot + - primary: BAI + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + - Su-24MK Fencer-D + - primary: DEAD + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + + Blue CV: + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-14B Tomcat + - F-14A Tomcat (Block 135-GR Late) + - primary: BARCAP + secondary: any + aircraft: + - F-14B Tomcat + - F-14A Tomcat (Block 135-GR Late) + - primary: Strike + secondary: any + aircraft: + - F/A-18C Hornet (Lot 20) + - F-14A Tomcat (Block 135-GR Late) + - primary: BAI + secondary: any + aircraft: + - F/A-18C Hornet (Lot 20) + - F-14A Tomcat (Block 135-GR Late) + - primary: Refueling + aircraft: + - S-3B Tanker + + Blue LHA: + - primary: BAI + secondary: air-to-ground + aircraft: + - AV-8B Harrier II Night Attack + - primary: CAS + secondary: air-to-ground + aircraft: + - UH-1H Iroquois + - SH-60B Seahawk + + # Red CPs: + # Squadrons are designed to work with either UAE 2015 (the default) or a + # typical Russian-sourced aircraft faction. + + # Al Dhafra AFB: + # This CP has factories attached and is the largest red base, so is the main + # logistics hub, with an airlift, AEW&C, and refueling squadron. + # + # For combat this base operates two pure air-to-air squadrons, two pure air- + # to-ground, and four multi-role. Al Minhad is closest to the front so CAS + # squadrons are placed there, but will retreat here after capture. + 4: + - primary: BARCAP + secondary: air-to-air + aircraft: + - Mirage 2000-5 + - Mirage 2000C + - Su-30 Flanker-C + - Su-27 Flanker-B + - primary: BARCAP + secondary: air-to-air + aircraft: + - F-16CM Fighting Falcon (Block 50) + - MiG-31 Foxhound + - MiG-25PD Foxbat-E + - primary: Strike + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + - Tu-160 Blackjack + - primary: SEAD + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + - primary: BAI + secondary: any + aircraft: + - F-16CM Fighting Falcon (Block 50) + - Su-34 Fullback + - Su-24M Fencer-D + - primary: BAI + secondary: any + - primary: DEAD + secondary: any + aircraft: + - F-16CM Fighting Falcon (Block 50) + - primary: DEAD + secondary: any + - primary: AEW&C + aircraft: + - E-3A + - A-50 + - primary: Refueling + aircraft: + - KC-135 Stratotanker + - IL-78M + - primary: Transport + aircraft: + - C-17A + - IL-78MD + + # Al Minhad AFB: + # The initial front line base. Contains CAS aircraft, as well as an air-to-air + # squadron and an air-to-ground squadron. + 12: + - primary: CAS + secondary: air-to-ground + aircraft: + - AH-64D Apache Longbow + - Mi-24V Hind-E + - Mi-24P Hind-F + - primary: CAS + secondary: air-to-ground + aircraft: + - F-16CM Fighting Falcon (Block 50) + - Su-25 Frogfoot + - primary: BARCAP + secondary: air-to-air + aircraft: + - Mirage 2000-5 + - Mirage 2000C + - Su-30 Flanker-C + - Su-27 Flanker-B + - primary: BAI + secondary: any + + # Liwa AFB: + # The last-stand base. Contains some factories as well. Begins with only an + # air-to-air squadron. Other squadrons can retreat here as the front-line + # moves. + 29: + - primary: BARCAP + secondary: air-to-air + aircraft: + - Mirage 2000-5 + - Mirage 2000C + - MiG-31 Foxhound + - MiG-25PD Foxbat-E From 757363e372cbccc9ef5e0514bf92f51c6fad0797 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 13:14:38 -0700 Subject: [PATCH 36/48] Fix JSON -> YAML translation error in Black Sea. --- resources/campaigns/black_sea.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/campaigns/black_sea.yaml b/resources/campaigns/black_sea.yaml index f7452d70..fa95d89a 100644 --- a/resources/campaigns/black_sea.yaml +++ b/resources/campaigns/black_sea.yaml @@ -4,7 +4,7 @@ theater: Caucasus authors: Colonel Panic description:

A medium sized theater with bases along the coast of the Black Sea.

miz: black_sea.miz -performance: 2, +performance: 2 version: "9.0" squadrons: # Anapa-Vityazevo @@ -148,4 +148,4 @@ squadrons: - primary: BAI secondary: air-to-ground - primary: CAS - secondary: air-to-ground \ No newline at end of file + secondary: air-to-ground From a1ee9d7476de8e9fa193aabaf4b70a412310a12d Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 13:23:23 -0700 Subject: [PATCH 37/48] Add more aircraft range estimates. --- resources/units/aircraft/F-4E.yaml | 4 +++- resources/units/aircraft/F-5E-3.yaml | 4 +++- resources/units/aircraft/M-2000C.yaml | 1 + resources/units/aircraft/MiG-21Bis.yaml | 1 + resources/units/aircraft/MiG-29A.yaml | 4 +++- resources/units/aircraft/MiG-29G.yaml | 4 +++- resources/units/aircraft/MiG-29S.yaml | 4 +++- resources/units/aircraft/Mirage 2000-5.yaml | 4 +++- resources/units/aircraft/Su-17M4.yaml | 4 +++- resources/units/aircraft/Su-24M.yaml | 8 +++++--- resources/units/aircraft/Su-24MR.yaml | 1 + 11 files changed, 29 insertions(+), 10 deletions(-) diff --git a/resources/units/aircraft/F-4E.yaml b/resources/units/aircraft/F-4E.yaml index 1d74d1dd..202af4a8 100644 --- a/resources/units/aircraft/F-4E.yaml +++ b/resources/units/aircraft/F-4E.yaml @@ -1,4 +1,5 @@ -description: Proving highly adaptable, the F-4 entered service with the Navy in 1961 +description: + Proving highly adaptable, the F-4 entered service with the Navy in 1961 before it was adopted by the United States Marine Corps and the United States Air Force, and by the mid-1960s it had become a major part of their air arms. Phantom production ran from 1958 to 1981 with a total of 5,195 aircraft built, making it @@ -12,6 +13,7 @@ manufacturer: McDonnell Douglas origin: USA price: 10 role: Fighter-Bomber +range: 200 variants: F-4E Phantom II: {} F-4EJ Kai Phantom II: {} diff --git a/resources/units/aircraft/F-5E-3.yaml b/resources/units/aircraft/F-5E-3.yaml index d1fa2348..000528e1 100644 --- a/resources/units/aircraft/F-5E-3.yaml +++ b/resources/units/aircraft/F-5E-3.yaml @@ -1,4 +1,5 @@ -description: "The F-5E was developed by Northrop Corporation in early 1970s. The light\ +description: + "The F-5E was developed by Northrop Corporation in early 1970s. The light\ \ tactical fighter is an upgraded version based on previous F-5A developments. The\ \ F-5s' combat role encompasses air superiority, ground support, and ground attack.\ \ Given its mission flexibility, ease of operation, and low cost, the Tiger II has,\ @@ -20,6 +21,7 @@ manufacturer: Northrop origin: USA price: 12 role: Light Fighter +range: 100 gunfighter: true variants: F-5E Tiger II: {} diff --git a/resources/units/aircraft/M-2000C.yaml b/resources/units/aircraft/M-2000C.yaml index 25f3a543..0a37f6d0 100644 --- a/resources/units/aircraft/M-2000C.yaml +++ b/resources/units/aircraft/M-2000C.yaml @@ -12,6 +12,7 @@ manufacturer: Dassault origin: France price: 17 role: Multirole Fighter +range: 400 variants: Mirage 2000C: {} radios: diff --git a/resources/units/aircraft/MiG-21Bis.yaml b/resources/units/aircraft/MiG-21Bis.yaml index 0d9140d7..396d437e 100644 --- a/resources/units/aircraft/MiG-21Bis.yaml +++ b/resources/units/aircraft/MiG-21Bis.yaml @@ -11,6 +11,7 @@ manufacturer: Mikoyan-Gurevich origin: USSR/Russia price: 12 role: Fighter +range: 200 gunfighter: true variants: J-7B: diff --git a/resources/units/aircraft/MiG-29A.yaml b/resources/units/aircraft/MiG-29A.yaml index 8678e4bd..1e55a636 100644 --- a/resources/units/aircraft/MiG-29A.yaml +++ b/resources/units/aircraft/MiG-29A.yaml @@ -1,4 +1,5 @@ -description: 'The MiG-29 "Fulcrum" is a Russian-designed, twin-engine, supersonic +description: + 'The MiG-29 "Fulcrum" is a Russian-designed, twin-engine, supersonic fighter. First operational in the early 1980s, the Fulcrum is a "light weight" fighter, comparable to the American F/A-18 Hornet and F-16. Designed to work in conjunction with the larger Su-27 Flanker, the MiG-29 is armed with an internal 30mm cannon @@ -24,5 +25,6 @@ manufacturer: Mikoyan origin: USSR/Russia price: 15 role: Multirole Fighter +range: 150 variants: MiG-29A Fulcrum-A: {} diff --git a/resources/units/aircraft/MiG-29G.yaml b/resources/units/aircraft/MiG-29G.yaml index b3ad36f5..365fe6af 100644 --- a/resources/units/aircraft/MiG-29G.yaml +++ b/resources/units/aircraft/MiG-29G.yaml @@ -1,4 +1,5 @@ -description: 'The MiG-29 "Fulcrum" is a Russian-designed, twin-engine, supersonic +description: + 'The MiG-29 "Fulcrum" is a Russian-designed, twin-engine, supersonic fighter. First operational in the early 1980s, the Fulcrum is a "light weight" fighter, comparable to the American F/A-18 Hornet and F-16. Designed to work in conjunction with the larger Su-27 Flanker, the MiG-29 is armed with an internal 30mm cannon @@ -24,5 +25,6 @@ manufacturer: Mikoyan origin: USSR/Russia price: 18 role: Multirole Fighter +range: 150 variants: MiG-29G Fulcrum-A: {} diff --git a/resources/units/aircraft/MiG-29S.yaml b/resources/units/aircraft/MiG-29S.yaml index c8168c2a..99302f42 100644 --- a/resources/units/aircraft/MiG-29S.yaml +++ b/resources/units/aircraft/MiG-29S.yaml @@ -1,4 +1,5 @@ -description: 'The MiG-29 "Fulcrum" is a Russian-designed, twin-engine, supersonic +description: + 'The MiG-29 "Fulcrum" is a Russian-designed, twin-engine, supersonic fighter. First operational in the early 1980s, the Fulcrum is a "light weight" fighter, comparable to the American F/A-18 Hornet and F-16. Designed to work in conjunction with the larger Su-27 Flanker, the MiG-29 is armed with an internal 30mm cannon @@ -24,5 +25,6 @@ manufacturer: Mikoyan origin: USSR/Russia price: 19 role: Multirole Fighter +range: 150 variants: MiG-29S Fulcrum-C: {} diff --git a/resources/units/aircraft/Mirage 2000-5.yaml b/resources/units/aircraft/Mirage 2000-5.yaml index 2d18c338..025ef81d 100644 --- a/resources/units/aircraft/Mirage 2000-5.yaml +++ b/resources/units/aircraft/Mirage 2000-5.yaml @@ -1,4 +1,5 @@ -description: "The Dassault Mirage 2000 is a French multirole, single-engined, fourth-generation\ +description: + "The Dassault Mirage 2000 is a French multirole, single-engined, fourth-generation\ \ jet fighter manufactured by Dassault Aviation. It was designed in the late 1970s\ \ as a lightweight fighter to replace the Mirage III for the French Air Force (Arm\xE9\ e de l'air). The Mirage 2000 evolved into a multirole aircraft with several variants\ @@ -11,5 +12,6 @@ manufacturer: Dassault origin: France price: 18 role: Multirole Fighter +range: 400 variants: Mirage 2000-5: {} diff --git a/resources/units/aircraft/Su-17M4.yaml b/resources/units/aircraft/Su-17M4.yaml index f88547fb..4bf4bbb6 100644 --- a/resources/units/aircraft/Su-17M4.yaml +++ b/resources/units/aircraft/Su-17M4.yaml @@ -1,4 +1,5 @@ -description: The Sukhoi Su-17 (izdeliye S-32) is a variable-sweep wing fighter-bomber +description: + The Sukhoi Su-17 (izdeliye S-32) is a variable-sweep wing fighter-bomber developed for the Soviet military. Its NATO reporting name is "Fitter". Developed from the Sukhoi Su-7, the Su-17 was the first variable-sweep wing aircraft to enter Soviet service. Two subsequent Sukhoi aircraft, the Su-20 and Su-22, have usually @@ -11,6 +12,7 @@ manufacturer: Sukhoi origin: USSR/Russia price: 10 role: Fighter-Bomber +range: 300 variants: Su-17M4 Fitter-K: {} Su-22M4 Fitter-K: diff --git a/resources/units/aircraft/Su-24M.yaml b/resources/units/aircraft/Su-24M.yaml index c165e0e6..fe253b04 100644 --- a/resources/units/aircraft/Su-24M.yaml +++ b/resources/units/aircraft/Su-24M.yaml @@ -1,15 +1,17 @@ -description: 'The Sukhoi Su-24 (NATO reporting name: Fencer) is a supersonic, all-weather +description: + "The Sukhoi Su-24 (NATO reporting name: Fencer) is a supersonic, all-weather attack aircraft developed in the Soviet Union. The aircraft has a variable-sweep wing, twin-engines and a side-by-side seating arrangement for its crew of two. It - was the first of the USSR''s aircraft to carry an integrated digital navigation/attack + was the first of the USSR's aircraft to carry an integrated digital navigation/attack system. It remains in service with the Russian Air Force, Syrian Air Force, Ukrainian Air Force, Azerbaijan Air Force , Iraqi Air Force and various air forces to which - it was exported.' + it was exported." introduced: 1983 manufacturer: Sukhoi origin: USSR/Russia price: 14 role: Attack +range: 200 variants: Su-24M Fencer-D: {} Su-24MK Fencer-D: diff --git a/resources/units/aircraft/Su-24MR.yaml b/resources/units/aircraft/Su-24MR.yaml index 9117c278..51cfc996 100644 --- a/resources/units/aircraft/Su-24MR.yaml +++ b/resources/units/aircraft/Su-24MR.yaml @@ -1,3 +1,4 @@ price: 15 +range: 200 variants: Su-24MR: null From 99acd52e89635bb9b60db25d864995f9505758a0 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 13:33:15 -0700 Subject: [PATCH 38/48] Increase some range estimates. These are still fairly pessimistic because the AI loves afterburner, but less so. --- resources/units/aircraft/F-14A-135-GR.yaml | 2 +- resources/units/aircraft/F-14B.yaml | 2 +- resources/units/aircraft/F-16A.yaml | 2 +- resources/units/aircraft/F-16C_50.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/units/aircraft/F-14A-135-GR.yaml b/resources/units/aircraft/F-14A-135-GR.yaml index b467d7e9..22ece158 100644 --- a/resources/units/aircraft/F-14A-135-GR.yaml +++ b/resources/units/aircraft/F-14A-135-GR.yaml @@ -21,7 +21,7 @@ manufacturer: Grumman origin: USA price: 22 role: Carrier-based Air-Superiority Fighter/Fighter Bomber -max_range: 250 +max_range: 350 variants: F-14A Tomcat (Block 135-GR Late): {} radios: diff --git a/resources/units/aircraft/F-14B.yaml b/resources/units/aircraft/F-14B.yaml index 2cc64fa4..21fc8f4d 100644 --- a/resources/units/aircraft/F-14B.yaml +++ b/resources/units/aircraft/F-14B.yaml @@ -21,7 +21,7 @@ manufacturer: Grumman origin: USA price: 26 role: Carrier-based Air-Superiority Fighter/Fighter Bomber -max_range: 250 +max_range: 350 variants: F-14B Tomcat: {} radios: diff --git a/resources/units/aircraft/F-16A.yaml b/resources/units/aircraft/F-16A.yaml index fdfcdb5c..3f478bac 100644 --- a/resources/units/aircraft/F-16A.yaml +++ b/resources/units/aircraft/F-16A.yaml @@ -1,5 +1,5 @@ description: The early verison of the F-16. It flew in Desert Storm. price: 15 -max_range: 200 +max_range: 350 variants: F-16A: null diff --git a/resources/units/aircraft/F-16C_50.yaml b/resources/units/aircraft/F-16C_50.yaml index 441acf94..03df560c 100644 --- a/resources/units/aircraft/F-16C_50.yaml +++ b/resources/units/aircraft/F-16C_50.yaml @@ -27,7 +27,7 @@ manufacturer: General Dynamics origin: USA price: 22 role: Multirole Fighter -max_range: 200 +max_range: 350 fuel: # Parking 44 to RWY 06L at Anderson AFB. taxi: 200 From a192e4c872ac894d37a2670686c0d3b2398a81d8 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 13:54:28 -0700 Subject: [PATCH 39/48] Allow showing the enemy aircraft inventory. --- changelog.md | 1 + qt_ui/windows/AirWingDialog.py | 45 ++++++++++++++++++++++++---------- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/changelog.md b/changelog.md index 5f37d371..d563e9d6 100644 --- a/changelog.md +++ b/changelog.md @@ -22,6 +22,7 @@ Saves from 4.x are not compatible with 5.0. * **[Kneeboard]** QNH (pressure MSL) and temperature have been added to the kneeboard. * **[New Game Wizard]** Can now customize the player's air wing before campaign start to disable, relocate, or rename squadrons. * **[UI]** Sell Button for aircraft will be disabled if there are no units available to be sold or all are already assigned to a mission +* **[UI]** Enemy aircraft inventory now viewable in the air wing menu. ## Fixes diff --git a/qt_ui/windows/AirWingDialog.py b/qt_ui/windows/AirWingDialog.py index b90e826b..6f5ac38a 100644 --- a/qt_ui/windows/AirWingDialog.py +++ b/qt_ui/windows/AirWingDialog.py @@ -14,6 +14,7 @@ from PySide2.QtWidgets import ( QTableWidget, QTableWidgetItem, QWidget, + QHBoxLayout, ) from game.squadrons import Squadron @@ -149,30 +150,47 @@ class AircraftInventoryData: class AirInventoryView(QWidget): def __init__(self, game_model: GameModel) -> None: super().__init__() - self.game_model = game_model - self.country = self.game_model.game.country_for(player=True) + + self.only_unallocated = False + self.enemy_info = False layout = QVBoxLayout() self.setLayout(layout) - self.only_unallocated_cb = QCheckBox("Unallocated Only?") - self.only_unallocated_cb.toggled.connect(self.update_table) + checkbox_row = QHBoxLayout() + layout.addLayout(checkbox_row) - layout.addWidget(self.only_unallocated_cb) + self.only_unallocated_cb = QCheckBox("Unallocated only") + self.only_unallocated_cb.toggled.connect(self.set_only_unallocated) + checkbox_row.addWidget(self.only_unallocated_cb) + + self.enemy_info_cb = QCheckBox("Show enemy info") + self.enemy_info_cb.toggled.connect(self.set_enemy_info) + checkbox_row.addWidget(self.enemy_info_cb) + + checkbox_row.addStretch() self.table = QTableWidget() layout.addWidget(self.table) self.table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table.verticalHeader().setVisible(False) - self.update_table(False) + self.set_only_unallocated(False) - def update_table(self, only_unallocated: bool) -> None: + def set_only_unallocated(self, value: bool) -> None: + self.only_unallocated = value + self.update_table() + + def set_enemy_info(self, value: bool) -> None: + self.enemy_info = value + self.update_table() + + def update_table(self) -> None: self.table.setSortingEnabled(False) self.table.clear() - inventory_rows = list(self.get_data(only_unallocated)) + inventory_rows = list(self.get_data()) self.table.setRowCount(len(inventory_rows)) headers = AircraftInventoryData.headers() self.table.setColumnCount(len(headers)) @@ -186,18 +204,19 @@ class AirInventoryView(QWidget): self.table.setSortingEnabled(True) def iter_allocated_aircraft(self) -> Iterator[AircraftInventoryData]: - for package in self.game_model.game.blue.ato.packages: + coalition = self.game_model.game.coalition_for(not self.enemy_info) + for package in coalition.ato.packages: for flight in package.flights: yield from AircraftInventoryData.from_flight(flight) def iter_unallocated_aircraft(self) -> Iterator[AircraftInventoryData]: - game = self.game_model.game - for squadron in game.blue.air_wing.iter_squadrons(): + coalition = self.game_model.game.coalition_for(not self.enemy_info) + for squadron in coalition.air_wing.iter_squadrons(): yield from AircraftInventoryData.each_untasked_from_squadron(squadron) - def get_data(self, only_unallocated: bool) -> Iterator[AircraftInventoryData]: + def get_data(self) -> Iterator[AircraftInventoryData]: yield from self.iter_unallocated_aircraft() - if not only_unallocated: + if not self.only_unallocated: yield from self.iter_allocated_aircraft() From 94fb0d8c6672f6ead58b504dc13188e551b0b841 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 15:24:55 -0700 Subject: [PATCH 40/48] Don't create squadrons for removed bases. --- .../campaignloader/defaultsquadronassigner.py | 8 ++--- game/theater/start_generator.py | 29 ++++++++++++------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/game/campaignloader/defaultsquadronassigner.py b/game/campaignloader/defaultsquadronassigner.py index c529738a..e08b7fc4 100644 --- a/game/campaignloader/defaultsquadronassigner.py +++ b/game/campaignloader/defaultsquadronassigner.py @@ -35,10 +35,10 @@ class DefaultSquadronAssigner: pass def assign(self) -> None: - for control_point, squadron_configs in self.config.by_location.items(): - if not control_point.is_friendly(self.coalition.player): - continue - for squadron_config in squadron_configs: + for control_point in self.game.theater.control_points_for( + self.coalition.player + ): + for squadron_config in self.config.by_location[control_point]: squadron_def = self.find_squadron_for(squadron_config, control_point) if squadron_def is None: logging.info( diff --git a/game/theater/start_generator.py b/game/theater/start_generator.py index 7ccc3267..f39db51f 100644 --- a/game/theater/start_generator.py +++ b/game/theater/start_generator.py @@ -12,7 +12,7 @@ from dcs.mapping import Point from game import Game from game.factions.faction import Faction from game.scenery_group import SceneryGroup -from game.theater import Carrier, Lha, PointWithHeading +from game.theater import PointWithHeading from game.theater.theatergroundobject import ( BuildingGroundObject, CarrierGroundObject, @@ -88,13 +88,12 @@ class GameGenerator: generator_settings: GeneratorSettings, mod_settings: ModSettings, ) -> None: - self.player = player - self.enemy = enemy + self.player = player.apply_mod_settings(mod_settings) + self.enemy = enemy.apply_mod_settings(mod_settings) self.theater = theater self.air_wing_config = air_wing_config self.settings = settings self.generator_settings = generator_settings - self.mod_settings = mod_settings def generate(self) -> Game: with logged_duration("TGO population"): @@ -102,8 +101,8 @@ class GameGenerator: namegen.reset() self.prepare_theater() game = Game( - player_faction=self.player.apply_mod_settings(self.mod_settings), - enemy_faction=self.enemy.apply_mod_settings(self.mod_settings), + player_faction=self.player, + enemy_faction=self.enemy, theater=self.theater, air_wing_config=self.air_wing_config, start_date=self.generator_settings.start_date, @@ -116,19 +115,27 @@ class GameGenerator: game.settings.version = VERSION return game + def should_remove_carrier(self, player: bool) -> bool: + faction = self.player if player else self.enemy + return self.generator_settings.no_carrier or not faction.carrier_names + + def should_remove_lha(self, player: bool) -> bool: + faction = self.player if player else self.enemy + return self.generator_settings.no_lha or not faction.helicopter_carrier_names + def prepare_theater(self) -> None: to_remove: List[ControlPoint] = [] # Remove carrier and lha, invert situation if needed for cp in self.theater.controlpoints: - if isinstance(cp, Carrier) and self.generator_settings.no_carrier: - to_remove.append(cp) - elif isinstance(cp, Lha) and self.generator_settings.no_lha: - to_remove.append(cp) - if self.generator_settings.inverted: cp.starts_blue = cp.captured_invert + if cp.is_carrier and self.should_remove_carrier(cp.starts_blue): + to_remove.append(cp) + elif cp.is_lha and self.should_remove_lha(cp.starts_blue): + to_remove.append(cp) + # do remove for cp in to_remove: self.theater.controlpoints.remove(cp) From ab2bb6814e13485d9dc011259742df31412fb3fb Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Fri, 3 Sep 2021 15:09:48 -0700 Subject: [PATCH 41/48] Clean up aircraft allocation and procurement. This also does improve the over-purchase problems, though I can't spot the behavior change that's causing that. Presumably the old implementation had a bug I can't spot and in rewriting it I solved the problem... Fixes https://github.com/dcs-liberation/dcs_liberation/issues/1582 --- game/coalition.py | 2 +- game/commander/aircraftallocator.py | 69 ----------------------------- game/commander/packagebuilder.py | 31 +++++++------ game/procurement.py | 41 ++++------------- game/squadrons/airwing.py | 51 ++++++++++++++------- game/squadrons/squadron.py | 14 +++++- 6 files changed, 76 insertions(+), 132 deletions(-) delete mode 100644 game/commander/aircraftallocator.py diff --git a/game/coalition.py b/game/coalition.py index 4ccc9128..8fb2ef18 100644 --- a/game/coalition.py +++ b/game/coalition.py @@ -40,7 +40,7 @@ class Coalition: self.procurement_requests: OrderedSet[AircraftProcurementRequest] = OrderedSet() self.bullseye = Bullseye(Point(0, 0)) self.faker = Faker(self.faction.locales) - self.air_wing = AirWing(game) + self.air_wing = AirWing(player) self.transfers = PendingTransfers(game, player) # Late initialized because the two coalitions in the game are mutually diff --git a/game/commander/aircraftallocator.py b/game/commander/aircraftallocator.py deleted file mode 100644 index 0339ff27..00000000 --- a/game/commander/aircraftallocator.py +++ /dev/null @@ -1,69 +0,0 @@ -from typing import Optional, Tuple - -from game.commander.missionproposals import ProposedFlight -from game.squadrons.airwing import AirWing -from game.squadrons.squadron import Squadron -from game.theater import ControlPoint, MissionTarget -from game.utils import meters -from gen.flights.ai_flight_planner_db import aircraft_for_task -from gen.flights.closestairfields import ClosestAirfields -from gen.flights.flight import FlightType - - -class AircraftAllocator: - """Finds suitable aircraft for proposed missions.""" - - def __init__( - self, air_wing: AirWing, closest_airfields: ClosestAirfields, is_player: bool - ) -> None: - self.air_wing = air_wing - self.closest_airfields = closest_airfields - self.is_player = is_player - - def find_squadron_for_flight( - self, target: MissionTarget, flight: ProposedFlight - ) -> Optional[Tuple[ControlPoint, Squadron]]: - """Finds aircraft suitable for the given mission. - - Searches for aircraft capable of performing the given mission within the - maximum allowed range. If insufficient aircraft are available for the - mission, None is returned. - - Airfields are searched ordered nearest to farthest from the target and - searched twice. The first search looks for aircraft which prefer the - mission type, and the second search looks for any aircraft which are - capable of the mission type. For example, an F-14 from a nearby carrier - will be preferred for the CAP of an airfield that has only F-16s, but if - the carrier has only F/A-18s the F-16s will be used for CAP instead. - - Note that aircraft *will* be removed from the global inventory on - success. This is to ensure that the same aircraft are not matched twice - on subsequent calls. If the found aircraft are not used, the caller is - responsible for returning them to the inventory. - """ - return self.find_aircraft_for_task(target, flight, flight.task) - - def find_aircraft_for_task( - self, target: MissionTarget, flight: ProposedFlight, task: FlightType - ) -> Optional[Tuple[ControlPoint, Squadron]]: - types = aircraft_for_task(task) - for airfield in self.closest_airfields.operational_airfields: - if not airfield.is_friendly(self.is_player): - continue - for aircraft in types: - if not airfield.can_operate(aircraft): - continue - distance_to_target = meters(target.distance_to(airfield)) - if distance_to_target > aircraft.max_mission_range: - continue - # Valid location with enough aircraft available. Find a squadron to fit - # the role. - squadrons = self.air_wing.auto_assignable_for_task_with_type( - aircraft, task, airfield - ) - for squadron in squadrons: - if squadron.operates_from(airfield) and squadron.can_fulfill_flight( - flight.num_aircraft - ): - return airfield, squadron - return None diff --git a/game/commander/packagebuilder.py b/game/commander/packagebuilder.py index 0f84b69a..1600d03d 100644 --- a/game/commander/packagebuilder.py +++ b/game/commander/packagebuilder.py @@ -1,16 +1,20 @@ -from typing import Optional +from __future__ import annotations + +from typing import Optional, TYPE_CHECKING -from game.commander.aircraftallocator import AircraftAllocator -from game.commander.missionproposals import ProposedFlight -from game.dcs.aircrafttype import AircraftType -from game.squadrons.airwing import AirWing -from game.theater import MissionTarget, OffMapSpawn, ControlPoint from game.utils import nautical_miles from gen.ato import Package -from gen.flights.closestairfields import ClosestAirfields +from game.theater import MissionTarget, OffMapSpawn, ControlPoint from gen.flights.flight import Flight +if TYPE_CHECKING: + from game.dcs.aircrafttype import AircraftType + from game.squadrons.airwing import AirWing + from gen.flights.closestairfields import ClosestAirfields + from .missionproposals import ProposedFlight + + class PackageBuilder: """Builds a Package for the flights it receives.""" @@ -28,7 +32,7 @@ class PackageBuilder: self.is_player = is_player self.package_country = package_country self.package = Package(location, auto_asap=asap) - self.allocator = AircraftAllocator(air_wing, closest_airfields, is_player) + self.air_wing = air_wing self.start_type = start_type def plan_flight(self, plan: ProposedFlight) -> bool: @@ -39,11 +43,12 @@ class PackageBuilder: caller should return any previously planned flights to the inventory using release_planned_aircraft. """ - assignment = self.allocator.find_squadron_for_flight(self.package.target, plan) - if assignment is None: + squadron = self.air_wing.best_squadron_for( + self.package.target, plan.task, plan.num_aircraft, this_turn=True + ) + if squadron is None: return False - airfield, squadron = assignment - start_type = airfield.required_aircraft_start_type + start_type = squadron.location.required_aircraft_start_type if start_type is None: start_type = self.start_type @@ -54,7 +59,7 @@ class PackageBuilder: plan.num_aircraft, plan.task, start_type, - divert=self.find_divert_field(squadron.aircraft, airfield), + divert=self.find_divert_field(squadron.aircraft, squadron.location), ) self.package.add_flight(flight) return True diff --git a/game/procurement.py b/game/procurement.py index 094e78c7..f51b21c8 100644 --- a/game/procurement.py +++ b/game/procurement.py @@ -221,45 +221,22 @@ class ProcurementAi: else: return self.game.theater.enemy_points() - @staticmethod - def squadron_rank_for_task(squadron: Squadron, task: FlightType) -> int: - return aircraft_for_task(task).index(squadron.aircraft) - - def compatible_squadrons_at_airbase( - self, airbase: ControlPoint, request: AircraftProcurementRequest - ) -> Iterator[Squadron]: - compatible: list[Squadron] = [] - for squadron in airbase.squadrons: - if not squadron.can_auto_assign(request.task_capability): - continue - if not squadron.can_provide_pilots(request.number): - continue - - distance_to_target = meters(request.near.distance_to(airbase)) - if distance_to_target > squadron.aircraft.max_mission_range: - continue - compatible.append(squadron) - yield from sorted( - compatible, - key=lambda s: self.squadron_rank_for_task(s, request.task_capability), - ) - def best_squadrons_for( self, request: AircraftProcurementRequest ) -> Iterator[Squadron]: - distance_cache = ObjectiveDistanceCache.get_closest_airfields(request.near) threatened = [] - for cp in distance_cache.operational_airfields: - if not cp.is_friendly(self.is_player): + for squadron in self.air_wing.best_squadrons_for( + request.near, request.task_capability, request.number, this_turn=False + ): + if not squadron.can_provide_pilots(request.number): continue - if cp.unclaimed_parking() < request.number: + if squadron.location.unclaimed_parking() < request.number: continue - if self.threat_zones.threatened(cp.position): - threatened.append(cp) + if self.threat_zones.threatened(squadron.location.position): + threatened.append(squadron) continue - yield from self.compatible_squadrons_at_airbase(cp, request) - for threatened_base in threatened: - yield from self.compatible_squadrons_at_airbase(threatened_base, request) + yield squadron + yield from threatened def ground_reinforcement_candidate(self) -> Optional[ControlPoint]: worst_supply = math.inf diff --git a/game/squadrons/airwing.py b/game/squadrons/airwing.py index 9d01e65c..4032702a 100644 --- a/game/squadrons/airwing.py +++ b/game/squadrons/airwing.py @@ -2,20 +2,21 @@ from __future__ import annotations import itertools from collections import defaultdict -from typing import Sequence, Iterator, TYPE_CHECKING +from typing import Sequence, Iterator, TYPE_CHECKING, Optional from game.dcs.aircrafttype import AircraftType +from gen.flights.ai_flight_planner_db import aircraft_for_task +from gen.flights.closestairfields import ObjectiveDistanceCache from .squadron import Squadron -from ..theater import ControlPoint +from ..theater import ControlPoint, MissionTarget if TYPE_CHECKING: - from game import Game from gen.flights.flight import FlightType class AirWing: - def __init__(self, game: Game) -> None: - self.game = game + def __init__(self, player: bool) -> None: + self.player = player self.squadrons: dict[AircraftType, list[Squadron]] = defaultdict(list) def add_squadron(self, squadron: Squadron) -> None: @@ -31,6 +32,35 @@ class AirWing: except StopIteration: return False + def best_squadrons_for( + self, location: MissionTarget, task: FlightType, size: int, this_turn: bool + ) -> list[Squadron]: + airfield_cache = ObjectiveDistanceCache.get_closest_airfields(location) + best_aircraft = aircraft_for_task(task) + ordered: list[Squadron] = [] + for control_point in airfield_cache.operational_airfields: + if control_point.captured != self.player: + continue + capable_at_base = [] + for squadron in control_point.squadrons: + if squadron.can_auto_assign_mission(location, task, size, this_turn): + capable_at_base.append(squadron) + + ordered.extend( + sorted( + capable_at_base, + key=lambda s: best_aircraft.index(s.aircraft), + ) + ) + return ordered + + def best_squadron_for( + self, location: MissionTarget, task: FlightType, size: int, this_turn: bool + ) -> Optional[Squadron]: + for squadron in self.best_squadrons_for(location, task, size, this_turn): + return squadron + return None + @property def available_aircraft_types(self) -> Iterator[AircraftType]: for aircraft, squadrons in self.squadrons.items(): @@ -51,17 +81,6 @@ class AirWing: if squadron.can_auto_assign(task) and squadron.location == base: yield squadron - def auto_assignable_for_task_with_type( - self, aircraft: AircraftType, task: FlightType, base: ControlPoint - ) -> Iterator[Squadron]: - for squadron in self.squadrons_for(aircraft): - if ( - squadron.location == base - and squadron.can_auto_assign(task) - and squadron.has_available_pilots - ): - yield squadron - def squadron_for(self, aircraft: AircraftType) -> Squadron: return self.squadrons_for(aircraft)[0] diff --git a/game/squadrons/squadron.py b/game/squadrons/squadron.py index 607a8d8a..33106740 100644 --- a/game/squadrons/squadron.py +++ b/game/squadrons/squadron.py @@ -16,12 +16,13 @@ from gen.ato import Package from gen.flights.flight import FlightType, Flight from gen.flights.flightplan import FlightPlanBuilder from .pilot import Pilot, PilotStatus +from ..utils import meters if TYPE_CHECKING: from game import Game from game.coalition import Coalition from game.dcs.aircrafttype import AircraftType - from game.theater import ControlPoint, ConflictTheater + from game.theater import ControlPoint, ConflictTheater, MissionTarget from .operatingbases import OperatingBases from .squadrondef import SquadronDef @@ -252,6 +253,17 @@ class Squadron: def can_auto_assign(self, task: FlightType) -> bool: return task in self.auto_assignable_mission_types + def can_auto_assign_mission( + self, location: MissionTarget, task: FlightType, size: int, this_turn: bool + ) -> bool: + if not self.can_auto_assign(task): + return False + if this_turn and not self.can_fulfill_flight(size): + return False + + distance_to_target = meters(location.distance_to(self.location)) + return distance_to_target <= self.aircraft.max_mission_range + def operates_from(self, control_point: ControlPoint) -> bool: if control_point.is_carrier: return self.operating_bases.carrier From 2a6f25070615c84ac781898139bc24aaf6c18fbd Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sat, 4 Sep 2021 14:20:29 -0700 Subject: [PATCH 42/48] Fix transcription error in Abu Dhabi. --- resources/campaigns/battle_of_abu_dhabi.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/campaigns/battle_of_abu_dhabi.yaml b/resources/campaigns/battle_of_abu_dhabi.yaml index 2bebb1db..54caed5a 100644 --- a/resources/campaigns/battle_of_abu_dhabi.yaml +++ b/resources/campaigns/battle_of_abu_dhabi.yaml @@ -7,7 +7,7 @@ recommended_enemy_faction: United Arab Emirates 2015 description:

You have managed to establish a foothold near Ras Al Khaima. Continue pushing south.

miz: battle_of_abu_dhabi.miz performance: 2 -version": "9.0" +version: "9.0" squadrons: # Blue CPs: # The default faction is Iran, but the F-14B is given higher precedence so From b1fee9fe5609b2e6c9696ff6cdf517cecb4b8803 Mon Sep 17 00:00:00 2001 From: MetalStormGhost <89945461+MetalStormGhost@users.noreply.github.com> Date: Sun, 5 Sep 2021 12:10:24 +0300 Subject: [PATCH 43/48] Add an option to use FC3-compatible laser codes. FC3 aircraft don't have laser codes like all the other aircraft do, they just use 1113. --- gen/armor.py | 10 +++++++++- resources/plugins/jtacautolase/jtacautolase-config.lua | 9 +++++++++ resources/plugins/jtacautolase/plugin.json | 5 +++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/gen/armor.py b/gen/armor.py index e13827a8..6880e1c4 100644 --- a/gen/armor.py +++ b/gen/armor.py @@ -143,9 +143,17 @@ class GroundConflictGenerator: # Add JTAC if self.game.blue.faction.has_jtac: n = "JTAC" + str(self.conflict.blue_cp.id) + str(self.conflict.red_cp.id) - code: int = self.laser_code_registry.get_next_laser_code() + code: int freq = self.radio_registry.alloc_uhf() + # If the option fc3LaserCode is enabled, force all JTAC + # laser codes to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs. + # Otherwise use 1688 for the first JTAC, 1687 for the second etc. + if self.game.settings.plugins["plugins.jtacautolase.fc3LaserCode"]: + code = 1113 + else: + code = self.laser_code_registry.get_next_laser_code() + utype = self.game.blue.faction.jtac_unit if utype is None: utype = AircraftType.named("MQ-9 Reaper") diff --git a/resources/plugins/jtacautolase/jtacautolase-config.lua b/resources/plugins/jtacautolase/jtacautolase-config.lua index c8ad8301..ffae2462 100644 --- a/resources/plugins/jtacautolase/jtacautolase-config.lua +++ b/resources/plugins/jtacautolase/jtacautolase-config.lua @@ -13,6 +13,7 @@ if dcsLiberation then -- specific options local smoke = false + local fc3LaserCode = false -- retrieve specific options values if dcsLiberation.plugins then @@ -22,6 +23,9 @@ if dcsLiberation then env.info("DCSLiberation|JTACAutolase plugin - dcsLiberation.plugins.jtacautolase") smoke = dcsLiberation.plugins.jtacautolase.smoke env.info(string.format("DCSLiberation|JTACAutolase plugin - smoke = %s",tostring(smoke))) + + fc3LaserCode = dcsLiberation.plugins.jtacautolase.fc3LaserCode + env.info(string.format("DCSLiberation|JTACAutolase plugin - fc3LaserCode = %s",tostring(fc3LaserCode))) end end @@ -29,6 +33,11 @@ if dcsLiberation then for _, jtac in pairs(dcsLiberation.JTACs) do env.info(string.format("DCSLiberation|JTACAutolase plugin - setting up %s",jtac.dcsUnit)) if JTACAutoLase then + if fc3LaserCode then + -- If fc3LaserCode is enabled in the plugin configuration, force the JTAC + -- laser code to 1113 to allow lasing for Su-25 Frogfoots and A-10A Warthogs. + jtac.laserCode = 1113 + end env.info("DCSLiberation|JTACAutolase plugin - calling JTACAutoLase") JTACAutoLase(jtac.dcsUnit, jtac.laserCode, smoke, 'vehicle') end diff --git a/resources/plugins/jtacautolase/plugin.json b/resources/plugins/jtacautolase/plugin.json index 9954aba3..f36759ab 100644 --- a/resources/plugins/jtacautolase/plugin.json +++ b/resources/plugins/jtacautolase/plugin.json @@ -6,6 +6,11 @@ "nameInUI": "Use smoke", "mnemonic": "smoke", "defaultValue": true + }, + { + "nameInUI": "Use FC3 laser code (1113)", + "mnemonic": "fc3LaserCode", + "defaultValue": false } ], "scriptsWorkOrders": [ From 12ad4fbf63433450f10048ef8e50f491c633a9d5 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 5 Sep 2021 02:12:54 -0700 Subject: [PATCH 44/48] Note the FC3 laser codes in the changelog. --- changelog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.md b/changelog.md index d563e9d6..f8c9a60b 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ Saves from 4.x are not compatible with 5.0. * **[Campaign AI]** Aircraft will now only be automatically purchased or assigned at appropriate bases. Naval aircraft will default to only operating from carriers, Harriers will default to LHAs and shore bases, helicopters will operate from anywhere. This can be customized per-squadron. * **[Engine]** Support for DCS 2.7.5.10869 and newer, including support for F-16 CBU-105s. * **[Mission Generation]** EWRs are now also headed towards the center of the conflict +* **[Mission Generation]** FACs can now use FC3 compatible laser codes. Note that this setting is global, not per FAC. * **[Modding]** Campaigns now specify the squadrons that are present in the campaign, their roles, and their starting bases. Players can customize this at game start but the campaign will choose the defaults. * **[Kneeboard]** Minimum required fuel estimates have been added to the kneeboard for aircraft with supporting data (currently only the Hornet). * **[Kneeboard]** QNH (pressure MSL) and temperature have been added to the kneeboard. From 18336f58d3efb069f3481626cc46219fadf06ed6 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 5 Sep 2021 21:14:18 -0700 Subject: [PATCH 45/48] Minor cleanup of notification system. --- game/event/event.py | 41 +++++++---------------- game/game.py | 12 +++---- qt_ui/windows/settings/QSettingsWindow.py | 13 ------- 3 files changed, 18 insertions(+), 48 deletions(-) diff --git a/game/event/event.py b/game/event/event.py index e40cb76f..41fda58f 100644 --- a/game/event/event.py +++ b/game/event/event.py @@ -8,7 +8,6 @@ from dcs.task import Task from game import persistency from game.debriefing import Debriefing -from game.infos.information import Information from game.operation.operation import Operation from game.theater import ControlPoint from gen.ato import AirTaskingOrder @@ -173,13 +172,10 @@ class Event: def commit_building_losses(self, debriefing: Debriefing) -> None: for loss in debriefing.building_losses: loss.ground_object.kill() - self.game.informations.append( - Information( - "Building destroyed", - f"{loss.ground_object.dcs_identifier} has been destroyed at " - f"location {loss.ground_object.obj_name}", - self.game.turn, - ) + self.game.message( + "Building destroyed", + f"{loss.ground_object.dcs_identifier} has been destroyed at " + f"location {loss.ground_object.obj_name}", ) @staticmethod @@ -191,19 +187,16 @@ class Event: for captured in debriefing.base_captures: try: if captured.captured_by_player: - info = Information( + self.game.message( f"{captured.control_point} captured!", f"We took control of {captured.control_point}.", - self.game.turn, ) else: - info = Information( + self.game.message( f"{captured.control_point} lost!", f"The enemy took control of {captured.control_point}.", - self.game.turn, ) - self.game.informations.append(info) captured.control_point.capture(self.game, captured.captured_by_player) logging.info(f"Will run redeploy for {captured.control_point}") self.redeploy_units(captured.control_point) @@ -330,34 +323,28 @@ class Event: # Handle the case where there are no casualties at all on either side but both sides still have units if delta == 0.0: print(status_msg) - info = Information( + self.game.message( "Frontline Report", f"Our ground forces from {cp.name} reached a stalemate with enemy forces from {enemy_cp.name}.", - self.game.turn, ) - self.game.informations.append(info) else: if player_won: print(status_msg) cp.base.affect_strength(delta) enemy_cp.base.affect_strength(-delta) - info = Information( + self.game.message( "Frontline Report", - f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}", - self.game.turn, + f"Our ground forces from {cp.name} are making progress toward {enemy_cp.name}. {status_msg}", ) - self.game.informations.append(info) else: print(status_msg) enemy_cp.base.affect_strength(delta) cp.base.affect_strength(-delta) - info = Information( + self.game.message( "Frontline Report", f"Our ground forces from {cp.name} are losing ground against the enemy forces from " f"{enemy_cp.name}. {status_msg}", - self.game.turn, ) - self.game.informations.append(info) def redeploy_units(self, cp: ControlPoint) -> None: """ " @@ -410,10 +397,8 @@ class Event: total_units_redeployed += move_count if total_units_redeployed > 0: - text = ( + self.game.message( + "Units redeployed", f"{total_units_redeployed} units have been redeployed from " - f"{source.name} to {destination.name}" + f"{source.name} to {destination.name}", ) - info = Information("Units redeployed", text, self.game.turn) - self.game.informations.append(info) - logging.info(text) diff --git a/game/game.py b/game/game.py index 6d059692..b6821f8e 100644 --- a/game/game.py +++ b/game/game.py @@ -105,8 +105,8 @@ class Game: self.game_stats = GameStats() self.notes = "" self.ground_planners: dict[int, GroundPlanner] = {} - self.informations = [] - self.informations.append(Information("Game Start", "-" * 40, 0)) + self.informations: list[Information] = [] + self.message("Game Start", "-" * 40) # Culling Zones are for areas around points of interest that contain things we may not wish to cull. self.__culling_zones: List[Point] = [] self.__destroyed_units: list[dict[str, Union[float, str]]] = [] @@ -269,9 +269,7 @@ class Game: Args: skipped: True if the turn was skipped. """ - self.informations.append( - Information("End of turn #" + str(self.turn), "-" * 40, 0) - ) + self.message("End of turn #" + str(self.turn), "-" * 40) self.turn += 1 # The coalition-specific turn finalization *must* happen before unit deliveries, @@ -404,8 +402,8 @@ class Game: gplanner.plan_groundwar() self.ground_planners[cp.id] = gplanner - def message(self, text: str) -> None: - self.informations.append(Information(text, turn=self.turn)) + def message(self, title: str, text: str = "") -> None: + self.informations.append(Information(title, text, turn=self.turn)) @property def current_turn_time_of_day(self) -> TimeOfDay: diff --git a/qt_ui/windows/settings/QSettingsWindow.py b/qt_ui/windows/settings/QSettingsWindow.py index 8cd86618..c7717a74 100644 --- a/qt_ui/windows/settings/QSettingsWindow.py +++ b/qt_ui/windows/settings/QSettingsWindow.py @@ -22,7 +22,6 @@ from dcs.forcedoptions import ForcedOptions import qt_ui.uiconstants as CONST from game.game import Game -from game.infos.information import Information from game.settings import Settings, AutoAtoBehavior from qt_ui.widgets.QLabeledWidget import QLabeledWidget from qt_ui.widgets.spinsliders import TenthsSpinSlider, TimeInputs @@ -894,18 +893,6 @@ class QSettingsWindow(QDialog): def cheatMoney(self, amount): logging.info("CHEATING FOR AMOUNT : " + str(amount) + "M") self.game.blue.budget += amount - if amount > 0: - self.game.informations.append( - Information( - "CHEATER", - "You are a cheater and you should feel bad", - self.game.turn, - ) - ) - else: - self.game.informations.append( - Information("CHEATER", "You are still a cheater !", self.game.turn) - ) GameUpdateSignal.get_instance().updateGame(self.game) def applySettings(self): From 45b52f4dea1a67a6059243d136eb052d646af417 Mon Sep 17 00:00:00 2001 From: Dan Albert Date: Sun, 5 Sep 2021 21:17:38 -0700 Subject: [PATCH 46/48] Remove auto-loss on front line for skipped turns. --- changelog.md | 1 + game/game.py | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/changelog.md b/changelog.md index f8c9a60b..b0587444 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ Saves from 4.x are not compatible with 5.0. * **[Campaign]** Era-restricted loadouts will now skip LGBs when no TGP is available in the loadout. This only applies to default loadouts; buddy-lasing can be coordinated with custom loadouts. * **[Campaign]** Squadrons now have a home base and will not operate out of other bases. See https://github.com/dcs-liberation/dcs_liberation/discussions/1550 for details. * **[Campaign]** Aircraft now belong to squadrons rather than bases to support squadron location transfers. +* **[Campaign]** Skipped turns are no longer counted as defeats on front lines. * **[Campaign AI]** Overhauled campaign AI target prioritization. This currently only affects the ordering of DEAD missions. * **[Campaign AI]** Player front line stances can now be automated. Improved stance selection for AI. * **[Campaign AI]** Reworked layout of hold, join, split, and ingress points. Should result in much shorter flight plans in general while still maintaining safe join/split/hold points. diff --git a/game/game.py b/game/game.py index b6821f8e..eec8e5a3 100644 --- a/game/game.py +++ b/game/game.py @@ -285,10 +285,6 @@ class Game: if not skipped: for cp in self.theater.player_points(): cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY) - elif self.turn > 1: - for cp in self.theater.player_points(): - if not cp.is_carrier and not cp.is_lha: - cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY) self.conditions = self.generate_conditions() From 65abef7979bdab5718a5ee2f83f4503def116d0c Mon Sep 17 00:00:00 2001 From: Starfire13 <72491792+Starfire13@users.noreply.github.com> Date: Tue, 7 Sep 2021 17:36:59 +1000 Subject: [PATCH 47/48] Update squadrondefgenerator.py --- game/campaignloader/squadrondefgenerator.py | 251 +++++++++++++++++++- 1 file changed, 247 insertions(+), 4 deletions(-) diff --git a/game/campaignloader/squadrondefgenerator.py b/game/campaignloader/squadrondefgenerator.py index f8b90a1a..658b3e73 100644 --- a/game/campaignloader/squadrondefgenerator.py +++ b/game/campaignloader/squadrondefgenerator.py @@ -61,13 +61,256 @@ class SquadronDefGenerator: adjective = random.choice( ( None, - "Red", - "Blue", - "Green", - "Golden", + "Aggressive", + "Alpha", + "Ancient", + "Angelic", + "Angry", + "Apoplectic", + "Aquamarine", + "Astral", + "Avenging", + "Azure", + "Badass", + "Barbaric", + "Battle", + "Battling", + "Bellicose", + "Belligerent", + "Big", + "Bionic", "Black", + "Bladed", + "Blazoned", + "Blood", + "Bloody", + "Blue", + "Bold", + "Boxing", + "Brash", + "Brass", + "Brave", + "Brazen", + "Brown", + "Brutal", + "Brzone", + "Burning", + "Buzzing", + "Celestial", + "Clever", + "Cloud", + "Cobalt", + "Copper", + "Coral", + "Crazy", + "Crimson", + "Crouching", + "Cursed", + "Cyan", + "Danger", + "Dangerous", + "Dapper", + "Daring", + "Dark", + "Dawn", + "Day", + "Deadly", + "Death", + "Defiant", + "Demon", + "Desert", + "Devil", + "Devil's", + "Diabolical", + "Diamond", + "Dire", + "Dirty", + "Doom", + "Doomed", + "Double", + "Drunken", + "Dusk", + "Dusty", + "Eager", + "Ebony", + "Electric", + "Emerald", + "Eternal", + "Evil", + "Faithful", + "Famous", + "Fanged", + "Fearless", + "Feisty", + "Ferocious", + "Fierce", + "Fiery", "Fighting", + "Fire", + "First", + "Flame", + "Flaming", "Flying", + "Forest", + "Frenzied", + "Frosty", + "Frozen", + "Furious", + "Gallant", + "Ghost", + "Giant", + "Gigantic", + "Glaring", + "Global", + "Gold", + "Golden", + "Green", + "Grey", + "Grim", + "Grizzly", + "Growling", + "Grumpy", + "Hammer", + "Hard", + "Hardy", + "Heavy", + "Hell", + "Hell's", + "Hidden", + "Homicidal", + "Hostile", + "Howling", + "Hyper", + "Ice", + "Icy", + "Immortal", + "Indignant", + "Infamous", + "Invincible", + "Iron", + "Jolly", + "Laser", + "Lava", + "Lavender", + "Lethal", + "Light", + "Lightning", + "Livid", + "Lucky", + "Mad", + "Magenta", + "Magma", + "Maroon", + "Menacing", + "Merciless", + "Metal", + "Midnight", + "Mighty", + "Mithril", + "Mocking", + "Moon", + "Mountain", + "Muddy", + "Nasty", + "Naughty", + "Night", + "Nova", + "Nutty", + "Obsidian", + "Ocean", + "Oddball", + "Old", + "Omega", + "Onyx", + "Orange", + "Perky", + "Pink", + "Power", + "Prickly", + "Proud", + "Puckered", + "Pugnacious", + "Puking", + "Purple", + "Ragged", + "Raging", + "Rainbow", + "Rampant", + "Razor", + "Ready", + "Reaper", + "Reckless", + "Red", + "Roaring", + "Rocky", + "Rolling", + "Royal", + "Rusty", + "Sable", + "Salty", + "Sand", + "Sarcastic", + "Saucy", + "Scarlet", + "Scarred", + "Scary", + "Screaming", + "Scythed", + "Shadow", + "Shiny", + "Shocking", + "Silver", + "Sky", + "Smoke", + "Smokin'", + "Snapping", + "Snappy", + "Snarling", + "Snow", + "Soaring", + "Space", + "Spiky", + "Spiny", + "Star", + "Steady", + "Steel", + "Stone", + "Storm", + "Striking", + "Strong", + "Stubborn", + "Sun", + "Super", + "Terrible", + "Thorny", + "Thunder", + "Top", + "Tough", + "Toxic", + "Tricky", + "Turquoise", + "Typhoon", + "Ultimate", + "Ultra", + "Ultramarine", + "Vengeful", + "Venom", + "Vermillion", + "Vicious", + "Victorious", + "Vigilant", + "Violent", + "Violet", + "War", + "Water", + "Whistling", + "White", + "Wicked", + "Wild", + "Wizard", + "Wrathful", + "Yellow", + "Young", ) ) if adjective is None: From a4b03c5cfe682c9123e38aff7af585d81f5c1ea4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Sep 2021 02:31:24 +0000 Subject: [PATCH 48/48] Bump pillow from 8.2.0 to 8.3.2 Bumps [pillow](https://github.com/python-pillow/Pillow) from 8.2.0 to 8.3.2. - [Release notes](https://github.com/python-pillow/Pillow/releases) - [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst) - [Commits](https://github.com/python-pillow/Pillow/compare/8.2.0...8.3.2) --- updated-dependencies: - dependency-name: pillow dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 83599356..12a94858 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,7 @@ nodeenv==1.5.0 packaging==20.9 pathspec==0.8.1 pefile==2019.4.18 -Pillow==8.2.0 +Pillow==8.3.2 pluggy==0.13.1 pre-commit==2.10.1 py==1.10.0