Compare commits

...

121 Commits

Author SHA1 Message Date
Dan Albert
4da4956df8 Fix waypoint drag and drop.
The fix for https://github.com/dcs-liberation/dcs_liberation/issues/3037
wasn't complete. It seems this `- 1` was here to work around the UI
wrongly having two takeoff points... Now that we fixed that, this also
needs to go.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3059.

(cherry picked from commit 02c9fe93c5)
2023-06-27 23:50:29 -07:00
Starfire13
618159c1fa Add F-15E Suite 4+ squadrons.
(cherry picked from commit 427df21da5)
2023-06-27 18:49:57 -07:00
Dan Albert
d8c662e7f8 Test SupplyRoute.
(cherry picked from commit f1e9abd157)
2023-06-27 18:49:57 -07:00
Dan Albert
12c41b57c9 Add test for SplitLines.
(cherry picked from commit eeacc79cb6)
2023-06-27 18:49:57 -07:00
Dan Albert
85a27845bc Make loadout/properties tab scrollable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3044.

(cherry picked from commit d54d906593)
2023-06-26 23:06:23 -07:00
Dan Albert
e3f6347e16 Fix off-by-one error in livery selector.
(cherry picked from commit cc2dfa5d35)
2023-06-26 22:02:53 -07:00
Dan Albert
fffe1b6e94 Fix UI waypoint numbering.
The flight plan used to not include a waypoint for departure, so a few
places would create one for the sake of the UI, or were built to assume
there was a missing waypoint that was okay to ignore. At some point we
added them to the flight plan, but never updated the UI, so the waypoint
list in the flight dialog started counting from 1 instead of 0, and the
openapi endpoint wrongly reported two departure waypoints to the front-
end.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3037.

(cherry picked from commit f7b0dfc3a5)
2023-06-26 22:02:53 -07:00
Dan Albert
5a7a730e23 Undo addition of "(AI)" F-15E variant.
This interacts badly with the built-in squadrons:
https://github.com/dcs-liberation/dcs_liberation/issues/3033. Better to
split the display name and "ID" (which is effectively how the key here
is treated), but that's a more invasive change than I'd like to tackle
in this release.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3033.

(cherry picked from commit 4e90c724bf)
2023-06-26 22:02:53 -07:00
Starfire13
576f777320 Update Starfire's campaigns.
* Added Razbam Strike Eagle options.

(cherry picked from commit fc90b6f2df)
2023-06-26 22:02:53 -07:00
Starfire13
87f7fe5307 Fix F-15E Suite 4+ loadouts for the DCS AI.
DCS AI cannot yet use LGBs.

A2G loadouts for anti-unit have been switched to CBU-97s, which appear
to be the most effective weapon type.
A2G loadouts against static targets (OCA/aircraft, OCA/runway, strike)
have been change to Mk82s and Mk84s.

(cherry picked from commit 266c453c99)
2023-06-26 22:02:53 -07:00
Dan Albert
e1434378a8 Add radio config for the new F-15E.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3028.

(cherry picked from commit 658a86dff5)
2023-06-26 22:02:53 -07:00
Dan Albert
e03b0d99d8 Razbam F-15E banner and icon.
Just reusing the old one.

https://github.com/dcs-liberation/dcs_liberation/issues/3028
(cherry picked from commit d31644c46a)
2023-06-26 22:02:53 -07:00
Dan Albert
e4eb3dec1b Add Razbam F-15E to factions with the old F-15E.
https://github.com/dcs-liberation/dcs_liberation/issues/3028
(cherry picked from commit f27c9f5a3d)
2023-06-26 22:02:53 -07:00
Dan Albert
b365016496 Add YAML file for Razbam Strike Eagle.
The old DCS AI F-15E is sticking around because the two have very
different weapon sets for now, so it's probably better to use the AI-
only one for squadrons that don't expect players.

I've avoided renaming the old one (we probably should name it "... (AI)"
for clarity) because the rename will break save compat. I have added a
_new_ name that new campaigns can use though.

https://github.com/dcs-liberation/dcs_liberation/issues/3028
(cherry picked from commit f805febd43)
2023-06-26 22:02:53 -07:00
Dan Albert
c359b3f7fc Update pydcs (Strike Eagle).
https://github.com/dcs-liberation/dcs_liberation/issues/3028
(cherry picked from commit dca02fea31)
2023-06-26 22:02:53 -07:00
Starfire13
302613069e Add loadouts for Razbam F-15E Strike Eagle.
(cherry picked from commit f97cd5d28f)
2023-06-26 22:02:53 -07:00
Dan Albert
5a22b62e3b Update version to 8.1.0. 2023-06-26 22:02:53 -07:00
Dan Albert
001e7dfed9 Ignore inconsistent DCS beacon information.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/3021.
2023-06-20 18:28:17 -07:00
Dan Albert
cf985d3d37 Test NavMeshLayer. 2023-06-16 22:18:46 -07:00
Dan Albert
1044a1f45f Test FrontLinesLayer. 2023-06-16 22:05:12 -07:00
Starfire13
9fe31859d3 Add Sinai campaign: Exercise Bright Star. 2023-06-16 21:49:13 -07:00
Dan Albert
09417322e7 Partial tests for FrontLine.
We need to mock the backend to usefully test the contextmenu handler.
I'd like to finish all the low hanging fruit before going for that.
2023-06-16 21:39:50 -07:00
Dan Albert
136a9b5f02 Test FlightPlansLayer. 2023-06-16 11:18:16 -07:00
Dan Albert
02f22d4930 Test CullingExclusionZones. 2023-06-16 10:35:31 -07:00
Dan Albert
ca133b9fd1 Add a coverage badge to the readme. 2023-06-15 23:54:19 -07:00
Dan Albert
b1af6dfbe1 Test ControlPointsLayer. 2023-06-15 22:50:39 -07:00
Dan Albert
647d1f57f9 Add tests for CombatLayer. 2023-06-15 22:50:39 -07:00
zhexu14
b250fe2f1e Make waypoint altitudes editable. 2023-06-15 22:41:49 -07:00
Starfire13
3be57bf6bb Add S-300 SAM to Egypt 2000 (#3004)
Adds S-300 as an alternative to Egypt's S-300VM for those who are not
using the High Digits SAM mod.
2023-06-15 22:39:50 -07:00
Dan Albert
3c8d0b023e Test Combat. 2023-06-15 22:24:37 -07:00
Dan Albert
adceb3a224 Add tests for AirDefenseRangeLayer. 2023-06-15 21:58:05 -07:00
zhexu14
ab02cd34c5 bump pyinstaller version
This PR bumps pyinstaller version to fix issues with build.
2023-06-15 12:57:32 -07:00
zhexu14
c74b603d81 restore killed_ground_units as it is relied on to track scenery kills
This PR fixes a regression introduced in
https://github.com/dcs-liberation/dcs_liberation/pull/2792 where
refactoring meant that scenery deaths were not tracked correctly.

This PR has been tested by striking a scenery target and confirming that
it appears in state.json and is updated in Liberation. I've also
confirmed that ground units are tracked.
2023-06-15 03:07:54 +00:00
Nosajthedevil
5815401e73 Update OV10A Weapons files 2023-06-14 20:01:16 -07:00
Starfire13
f463fe50f2 Add Chengdu J-7B to Egypt_2000 2023-06-14 20:00:47 -07:00
Starfire13
f60bf62897 Add missing units to Bluefor Modern 2023-06-14 20:00:32 -07:00
Dan Albert
9d43eb8f03 Handle game over.
The contents are completely uninteresting, but at least it's visible.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/978.
2023-06-12 23:43:26 -07:00
Dan Albert
8f0ca08b89 Move turn passing to post-debrief.
If we're going to show a game over dialog, we need to do so before
moving to the next turn, but we will still want to show the debriefing
window. Move those steps to happen after the debrief window.

https://github.com/dcs-liberation/dcs_liberation/issues/978
2023-06-12 23:43:26 -07:00
Dan Albert
0534f66b30 Allow NGW to be called from anywhere.
Any real end game dialog needs a "new game" button. If only the main
window can usefully call the NGW we'd have to plumb that object through
and call into it from that dialog, which is gross. Just make it easier
to call the wizard.

https://github.com/dcs-liberation/dcs_liberation/issues/978
2023-06-12 22:39:17 -07:00
Nosajthedevil
1162e0aa29 Added and updated weapons files.
Added or updated weapons files including 

The various Hellfire II iterations - this covers fallbacks to rockets
for all 3 Apache variants, the Supercobra, and the Kiowa.
This also adds the 184 Long and 131 pods. 
Lastly, this adds date and fallback information to the shrikes in
advance of the AGM-45B being added to the A-4E mod.
2023-06-13 01:04:14 +00:00
Dan Albert
36c4bb88be Sinai support.
The rest of the work is done, so bump the campaign version and update
the changelog.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2979.
2023-06-12 17:56:38 -07:00
Dan Albert
dc6624a159 Fix Sinai timezone offset.
https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-12 17:56:38 -07:00
Nosajthedevil
8b55331326 Fix F-16A icons, loadouts, and tasking.
Updates the filename of the F-16A banner so that liberation can read
it.

Updates the F-16A.yaml resource file to add BAI / CAS / antiship
mission types - since the 15A is capable of these.

Updated the F-16A payload provide more capability - primarily adding
jamming pods to the centerline, moving the fuel tanks from the
centerline to the inner wings, replacing the AIS_ASQ_T50 on the
wingtips with 120Bs, and changing the CAP loadout to have 120Bs on
pylons 8 and 3 so they fall back to sparrows on historical campaigns.
2023-06-12 17:54:47 -07:00
Starfire13
33ca77e3d1 Add Egyptian faction.
I figured it's a good time to add an Egyptian faction since we now have
the Sinai map.
2023-06-12 17:52:19 -07:00
Dan Albert
b92b01b245 Add Sinai landmap.
https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-12 17:44:57 -07:00
Dan Albert
b18b371904 Basic Sinai support.
Not ready (most importantly no landmap).

https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-11 23:54:41 -07:00
Dan Albert
9c7e16d121 Beacons for Sinai.
https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-11 23:54:41 -07:00
Dan Albert
87e869d963 Fix Scenic Inland yaml, fixing NGW. 2023-06-11 23:48:56 -07:00
Dan Albert
4a059a4f8b Update pydcs.
Includes Sinai terrain export.

https://github.com/dcs-liberation/dcs_liberation/issues/2979
2023-06-11 23:44:37 -07:00
Dan Albert
674254e55b Note button relocation in the changelog. 2023-06-11 21:31:46 -07:00
Dan Albert
9fd0e06c05 Make patch coverage task informational.
Not reasonable to require all PRs to avoid regressing coverage yet...
2023-06-11 21:23:31 -07:00
Dan Albert
ecaf84ea55 Update Fuzzle's campaigns.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2970.
2023-06-11 21:07:46 -07:00
Dan Albert
e4028cb013 Update pydcs.
Includes the updates needed to fix the Gazelle, and a terrain export for
Normandy for the new airfields added in the latest update.

No Sinai support yet.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2984.
2023-06-11 20:48:40 -07:00
Dan Albert
c45ac50370 Make overfull airbase display scrollable.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2974.
2023-06-08 04:59:34 +00:00
Dan Albert
6640609caf Move misc buttons to the toolbar.
The top panel is a bit overfull on some displays whenever the weather
description is verbose.
2023-06-08 04:50:31 +00:00
Dan Albert
e44b6b416b Stop preloading images that are rarely used.
The aircraft banners are only used for the unit info window, and that's
not a normal part of gameplay. We spend a bit over 1.5 seconds
preloading this data at startup (about 25% of the non-game load startup
time). This data is only expensive to load in aggregate, and we never
need all of it. The unit info window is not noticeably slowed by this,
but startup is noticeably faster without it.
2023-06-07 05:01:06 +00:00
Dan Albert
8a861d3da5 Remove pointless suffixes on banner files.
I think someone just copied this pattern from the icons where the suffix
represented the icon size. These are definitely not 24px banners, and
some of the suffixes are even wrong (_25).
2023-06-07 05:01:06 +00:00
Dan Albert
380d6551be Add tests for AircraftLayer. 2023-06-06 07:08:57 +00:00
Dan Albert
4cb035b955 Fix Python coverage reporting.
Apparently the fact that I want the coverage report to be XML isn't
enough of a hint that I want coverage.
2023-06-06 03:12:49 +00:00
Dan Albert
e50be9bbde Update bug templates for 7.1.0 release. 2023-06-03 22:27:14 +00:00
Dan Albert
ec49a10135 Configure squadron sizes for Abu Dhabi. 2023-06-03 22:01:15 +00:00
Dan Albert
23e3630169 Fix Black Sea LHA parking limits.
Everything else was within the limits, but I had forgotten to check the
LHAs.
2023-06-03 21:56:53 +00:00
Dan Albert
e20ab5fbc0 Ack campaign versions for new squadron limits.
I haven't tested all of them, but I know these are compatible, so
advertise them as such.
2023-06-03 21:53:49 +00:00
Dan Albert
4fd2bb131b Warn for new squadron rules with old campaigns.
It's not feasible to actually check the parking limits because we can't
identify parking limits for carriers until the theater is populated.
Doing so is expensive (and depends on other NGW inputs). Instead,
compare against the version of the campaign and guess.

A new (minor) campaign version has been introduced which makes this
required to improve the UI hint. Campaigns that are compatible with the
new rules should update their version to advertise support.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2909.
2023-06-03 21:32:42 +00:00
Dan Albert
42a7102948 Disallow air wing generation with overfull bases.
This also changes the window close button of the air wing configuration
dialog to cancel rather than revert and continue, because otherwise
there's no way for the user to back out of the dialog without fixing all
the overfull bases first.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2910.
2023-06-03 20:55:23 +00:00
Dan Albert
d271ff17c2 Show overfull airbase details in air wing config.
https://github.com/dcs-liberation/dcs_liberation/issues/2910
2023-06-03 20:47:56 +00:00
Dan Albert
cb61dfccc4 Show parking capacities in air wing config.
This does show the theoretical parking use of full squadrons even when
the new rules are not enabled. Since limits can be enabled manually
later in the game, it's still useful information, even if it's a bit
misleading.

https://github.com/dcs-liberation/dcs_liberation/issues/2910
2023-06-03 19:31:35 +00:00
Dan Albert
56f93c76eb Add new-game option to show air wing config.
Working on this UI was a huge pain because it required manually creating
a game before the UI could be used.
2023-06-03 19:11:29 +00:00
Dan Albert
36cb3a386c Move CLI game generation after UI init. 2023-06-03 19:11:29 +00:00
Dan Albert
c25e830e6c Factor out game creation parameters in main.
Want to move this deeper into the launch process so that it can use the
UI, but don't want to pass the loosely typed argparse namespace any
more than we have to.
2023-06-03 19:11:29 +00:00
Dan Albert
5d08990cd0 Fix line endings. 2023-06-01 22:49:27 -07:00
Starfire13
2a45cd8899 Add Final Countdown II campaign.
Designed for Normandy 2.0
2023-06-01 22:34:42 -07:00
ColonelAkirNakesh
90b880ec3c Updates china_2010.yaml
Replaces T-55 with Type 59 MBT, adds Type 093 attack sub from China Assets pack
2023-06-01 22:33:11 -07:00
ColonelAkirNakesh
5f0c570d65 Update russia_2010.yaml
Adds Ropucha landing ship, Improved Kilo sub
2023-06-01 22:32:40 -07:00
ColonelAkirNakesh
ce102fcc50 Update allies_1944.yaml
Adds 105mm field howitzer to allies
2023-06-01 22:32:23 -07:00
ColonelAkirNakesh
30c792c15a Enforces Topgun: Maverick Rogue Nation livery for Iranian Tomcat 2023-06-01 22:32:09 -07:00
ColonelAkirNakesh
2f45b856d6 Adds support for Chinese sub Type_093.yaml 2023-06-01 22:31:52 -07:00
ColonelAkirNakesh
31d2b756ab Create TYPE-59.yaml 2023-06-01 22:31:30 -07:00
ColonelAkirNakesh
b5cf889c09 Create Horch_901_typ_40_kfz_21.yaml 2023-06-01 22:31:20 -07:00
ColonelAkirNakesh
19958f91ca Create Pak40.yaml 2023-06-01 22:31:08 -07:00
ColonelAkirNakesh
c775a898a4 Create Wespe124.yaml 2023-06-01 22:30:55 -07:00
ColonelAkirNakesh
535244f6f3 Create LeFH_18-40-105.yaml 2023-06-01 22:30:42 -07:00
ColonelAkirNakesh
9d1d3bdcfa Create Higgins_boat.yaml 2023-06-01 22:29:45 -07:00
ColonelAkirNakesh
36eef2b1b9 Create M2A1-105.yaml 2023-06-01 22:29:27 -07:00
ColonelAkirNakesh
7788425c5c Create IMPROVED_KILO.yaml 2023-06-01 22:28:57 -07:00
ColonelAkirNakesh
ee0c21b3e5 Create BDK-775.yaml 2023-06-01 22:28:43 -07:00
ColonelAkirNakesh
54cd619f75 Create santafe.yaml 2023-06-01 22:28:27 -07:00
ColonelAkirNakesh
051940e23c Create leander-gun-condell.yaml 2023-06-01 22:28:13 -07:00
ColonelAkirNakesh
4fbd7defa3 Create leander-gun-lynch.yaml 2023-06-01 22:27:59 -07:00
Dan Albert
90bda9383d Add missing note about 7.0.0 -> 7.1.0 save compat. 2023-05-31 00:17:02 -07:00
Dan Albert
7798e2970c Minor campaign version bump for Normandy 2.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2804.
2023-05-30 23:49:42 -07:00
Dan Albert
410c25b331 Update beacon data.
Did this for Normandy 2... but unsurprisingly there aren't a whole lot
of beacons in a WW2 map.
2023-05-30 23:49:42 -07:00
Dan Albert
cff74525d6 Update pydcs.
Normandy 2 support.

https://github.com/dcs-liberation/dcs_liberation/issues/2804
2023-05-30 23:49:42 -07:00
Dan Albert
8b7f107044 Update Normandy landmap for Normandy 2.
https://github.com/dcs-liberation/dcs_liberation/issues/2804
2023-05-30 23:24:58 -07:00
Dan Albert
c365a0d739 Add Normandy 2 landmap inputs.
https://github.com/dcs-liberation/dcs_liberation/issues/2804
2023-05-30 23:24:58 -07:00
Dan Albert
1f4fd0fd04 Force polygons into validity during GIS import.
Not sure why, but some polygons become invalid (which usually means a
self-intersecting "polygon", such as two triangles that meet at a point)
during this transformation. Shapely includes a tool to reshape polygons
into validity, so use that.
2023-05-30 23:24:58 -07:00
Dan Albert
4bb60cb500 Tolerate empty settings files. 2023-05-30 23:24:58 -07:00
Dan Albert
fe96a415be Add settings for battlefield commander slots.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2235.
2023-05-30 22:06:47 -07:00
Dan Albert
6699289bf7 Add performance option to prevent missile tasks.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2508.
2023-05-30 21:47:16 -07:00
Dan Albert
a85d3243fb Add changelog note for BAI fix.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2922.
2023-05-30 21:12:45 -07:00
Dan Albert
7f2607cf08 Replace more Patriot STRs with real EWRs.
Not all of these nations actually field this radar (according to
Wikipedia), but at least it's a real EWR, and it's the only blue one
we've got.
2023-05-30 21:05:15 -07:00
zhexu14
e50ee976ed Add ability to convert landmap to/from miz.
This PR adds utility functions that import/export landmap files to .miz
polygons. In addition to the unit test, this PR has been tested by
writing the Caucuses & Syria landmaps to a .miz file, loading the
generated .miz file back in and checking that the loaded landmap object
is identical to the original files.
2023-05-30 21:01:05 -07:00
ColonelAkirNakesh
29ffb526f2 Replaces Patriot STR with AN/FPS-117 EWR, adds USS Harry Truman 2023-05-30 20:41:00 -07:00
zhexu14
e024013093 issue 2922: make BAI plannable against missile and costal sites 2023-05-30 20:39:39 -07:00
Dan Albert
257dabe4fa Fix formatting of takeoff time. 2023-05-25 22:35:50 -07:00
Dan Albert
406fb61fa4 Add UI for TOT offset adjustment.
Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2912.
2023-05-25 22:35:50 -07:00
Dan Albert
49dfa95c61 Save the TOT offset in the flight plan.
Prep work for exposing this to the UI.
2023-05-25 22:35:50 -07:00
Dan Albert
c80e5b259f Allow save compat to exist for two versions.
We want to clean up eventually, but allowing it to exist in both develop
and the release branch makes cherry picks easier.
2023-05-25 22:35:50 -07:00
Dan Albert
64e2213f28 Make the flight details menu modal.
Prevents players from accidentally deleting flights they're currently
viewing, which would cause an error.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2911.
2023-05-25 21:10:21 -07:00
Dan Albert
ced93afd49 Update bug templates now that 7.0.0 is out. 2023-05-25 21:10:21 -07:00
Dan Albert
f719a5ec34 Update bug templates now that 7.0.0 is out. 2023-05-23 01:38:16 -07:00
Dan Albert
6f4ac1dc39 Fix line endings.
These get broken whenever someone uses the GitHub file upload editor,
since that doesn't understand .gitattributes.
2023-05-23 00:37:28 -07:00
Starfire13
f831c8efdd Update Golan Heights and Caen to Evreux campaigns.
I had asked Khopa for permission to edit two of his campaigns to fix
some issues. Only the YAMLs have been edited, .miz files did not need
changes. I have tested both YAMLs to make sure campaigns will generate.
Also tested generating turn 1 .miz and ran it in DCS.

Golan Heights:
1. Removed the 2 problematic squadrons from Marj Ruhayyil that were
causing aircraft losses due to larger aircraft sizes not fitting at that
airfield (which is intended for helicopters).
2. Implemented squadron limits.

Caen to Evreux:
1. Re-arranged squadrons for better force distribution and revised
primary and secondary mission types for better default play experience.
2. Implemented squadron limits.
2023-05-23 00:23:41 -07:00
Starfire13
e3c6b03603 Update Apache loadouts.
BAI loadout updated to use the new radar guided hellfires. Aux tanks
removed in favour of extra cannon ammo.
2023-05-20 09:21:16 +00:00
Dan Albert
7a2e8279cd Fix syntax error in bluefor_modern.yaml. 2023-05-19 17:51:22 -07:00
Dan Albert
24e72475b4 Checkpoint game before sim, auto-revert on abort.
An alternative to
https://github.com/dcs-liberation/dcs_liberation/pull/2891 that I ended
up liking much better (I had assumed some part of the UI would fail or
at least look terrible with this approach, but it seems to work quite
well).

On by default now since it's far less frightening than the previous
thing.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.
2023-05-19 17:44:18 -07:00
Starfire13
f10350dac4 Update scenic_inland.yaml
A formatting fix for scenic route 2 that was preventing new campaign start. Fixing at Fuzzle's request as he doesn't have the time for it right now.
2023-05-19 17:36:29 -07:00
Dan Albert
f068976749 Fuzzle campaign updates.
https://github.com/dcs-liberation/dcs_liberation/issues/2889
2023-05-19 01:17:51 -07:00
Dan Albert
4b4c45e90f Attempt to reset the simulation on abort.
This is optional because I really don't know if I trust it. I don't see
much wrong with it (aside from the warning about not using it with auto-
resolve, because it won't restore lost aircraft), but it's really not
something I'd built for since it's not going to be possible as the RTS
features grow.

Fixes https://github.com/dcs-liberation/dcs_liberation/issues/2735.
2023-05-19 01:05:25 -07:00
Dan Albert
527eac1f4a Fix docs version.
This ought to be auto-imported but that requires liberation being
importable here and that's proving non-trivial.
2023-05-18 23:17:08 -07:00
Dan Albert
92c3087187 Advance develop to 8.0.0-preview. 2023-05-18 22:44:39 -07:00
356 changed files with 5547 additions and 720 deletions

View File

@@ -31,7 +31,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 6.1.1
- 7.1.0
- Development build
- type: textarea
attributes:
@@ -53,9 +53,9 @@ body:
description: >
Attach any files needed to reproduce the bug here. **A save game is
required.** We typically cannot help without a save game (the
`.liberation` (or `.liberation.zip`, for 7.x) file found in
`%USERPROFILE%/Saved Games/DCS/Liberation/Saves`), so most bugs filed
without saved games will be closed without investigation.
`.liberation.zip` file found in `%USERPROFILE%/Saved
Games/DCS/Liberation/Saves`), so most bugs filed without saved games
will be closed without investigation.
Other useful files to include are:
@@ -73,10 +73,7 @@ body:
The `state.json` file for the most recently completed turn, located at
`<Liberation install directory>/state.json`. This file is essential for
investigating any issues with end-of-turn results processing. **If you
include this file, also include `last_turn.liberation`** (unless the
save is from 7.x or newer, which includes that information in the save
automatically).
investigating any issues with end-of-turn results processing.
You can attach files to the bug by dragging and dropping the file into

View File

@@ -39,7 +39,7 @@ body:
If the bug was found in a development build, select "Development build"
and provide a link to the build in the field below.
options:
- 6.1.1
- 7.1.0
- Development build
- type: textarea
attributes:

View File

@@ -15,7 +15,7 @@ jobs:
- name: run tests
run: |
./venv/scripts/activate
pytest --cov-report=xml tests
pytest --cov --cov-report=xml tests
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3

1
.gitignore vendored
View File

@@ -9,6 +9,7 @@ venv
.vscode/settings.json
dist/**
/.coverage
/coverage.xml
# User-specific stuff
.idea/
.env

View File

@@ -8,6 +8,7 @@
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![codecov](https://codecov.io/gh/dcs-liberation/dcs_liberation/branch/develop/graph/badge.svg?token=EEQ7G76K2L)](https://codecov.io/gh/dcs-liberation/dcs_liberation)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/dcs-liberation/dcs_liberation)](https://github.com/dcs-liberation/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/dcs-liberation/dcs_liberation?style=social)

View File

@@ -1,3 +1,56 @@
# 8.1.0
Saves from 8.0.0 are compatible with 8.1.0
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41363, including F-15E support.
* **[UI]** Flight loadout/properties tab is now scrollable.
## Fixes
* **[Campaign]** Fixed liveries for premade squadrons all being off-by-one.
* **[UI]** Fixed numbering of waypoints in the map and flight dialog (first waypoint is now 0 rather than 1).
# 8.0.0
Saves from 7.x are not compatible with 8.0.
## Features/Improvements
* **[Engine]** Support for DCS 2.8.6.41066, including the new Sinai map.
* **[UI]** Limited size of overfull airbase display and added scrollbar.
* **[UI]** Waypoint altitudes can be edited in Waypoints tab of Edit Flight window.
* **[UI]** Moved air wing and transfer menus to the toolbar to improve UI fit on low resolution displays.
* **[UI]** Added basic game over dialog.
## Fixes
* **[Campaign]** Fix bug introduced in 7.0 where map strike target deaths are no longer tracked.
* **[Mission Generation]** Fix crash during mission generation caused by out of date DCS data for the Gazelle.
* **[Mission Generation]** Fix crash during mission generation when DCS beacon data is inconsistent.
# 7.1.0
Saves from 7.0.0 are compatible with 7.1.0
## Features/Improvements
* **[Engine]** Support for Normandy 2 airfields.
* **[Factions]** Replaced Patriot STRs "EWRs" with AN/FPS-117 for blue factions 1980 or newer.
* **[Mission Generation]** Added option to prevent scud and V2 sites from firing at the start of the mission.
* **[Mission Generation]** Added settings for controlling number of tactical commander, observer, JTAC, and game master slots.
* **[Mission Planning]** Per-flight TOT offsets can now be set in the flight details UI. This allows individual flights to be scheduled ahead of or behind the rest of the package.
* **[New Game Wizard]** The air wing configuration dialog will check for and reject overfull airbases before continuing when the new squadron rules are used.
* **[New Game Wizard]** Closing the air wing configuration dialog will now cancel and return to the new game wizard rather than reverting changes and continuing.
* **[New Game Wizard]** A warning will be displayed next to the new squadron rules button if the campaign predates the new rules and will likely require user intervention before continuing.
* **[UI]** Parking capacity of each squadron's base is now shown during air wing configuration to avoid overcrowding bases when beginning the game with full squadrons.
## Fixes
* **[Mission Planning]** BAI is once again plannable against missile sites and coastal defense batteries.
* **[UI]** Fixed formatting of departure time in flight details dialog.
# 7.0.0
Saves from 6.x are not compatible with 7.0.
@@ -16,6 +69,7 @@ Saves from 6.x are not compatible with 7.0.
* **[Mission Generation]** Wind speeds no longer follow a uniform distribution. Median wind speeds are now much lower and the standard deviation has been reduced considerably at altitude but increased somewhat at MSL.
* **[Mission Generation]** Improved task generation for SEAD flights carrying TALDs.
* **[Mission Generation]** Added task timeout for SEAD flights with TALDs to prevent AI from overflying the target.
* **[Mission Generation]** Game state will automatically be checkpointed before fast-forwarding the mission, and restored on mission abort. This means that it's now possible to abort a mission and make changes without needing to manually re-load your game.
* **[Modding]** Updated Community A-4E-C mod version support to 2.1.0 release.
* **[Modding]** Add support for VSN F-4B and F-4C mod.
* **[Modding]** Added support for AI C-47 mod.

View File

@@ -1,11 +1,11 @@
import App from "./App";
import { store } from "./app/store";
import { setupStore } from "./app/store";
import { render } from "@testing-library/react";
import { Provider } from "react-redux";
test("app renders", () => {
render(
<Provider store={store}>
<Provider store={setupStore()}>
<App />
</Provider>
);

View File

@@ -3,36 +3,48 @@ import combatReducer from "../api/combatSlice";
import controlPointsReducer from "../api/controlPointsSlice";
import flightsReducer from "../api/flightsSlice";
import frontLinesReducer from "../api/frontLinesSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import mapReducer from "../api/mapSlice";
import navMeshReducer from "../api/navMeshSlice";
import supplyRoutesReducer from "../api/supplyRoutesSlice";
import tgosReducer from "../api/tgosSlice";
import iadsNetworkReducer from "../api/iadsNetworkSlice";
import threatZonesReducer from "../api/threatZonesSlice";
import unculledZonesReducer from "../api/unculledZonesSlice";
import { Action, ThunkAction, configureStore } from "@reduxjs/toolkit";
import unculledZonesReducer from "../api/unculledZonesSlice";
import {
Action,
PreloadedState,
ThunkAction,
combineReducers,
configureStore,
} from "@reduxjs/toolkit";
export const store = configureStore({
reducer: {
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
const rootReducer = combineReducers({
combat: combatReducer,
controlPoints: controlPointsReducer,
flights: flightsReducer,
frontLines: frontLinesReducer,
map: mapReducer,
navmeshes: navMeshReducer,
supplyRoutes: supplyRoutesReducer,
iadsNetwork: iadsNetworkReducer,
tgos: tgosReducer,
threatZones: threatZonesReducer,
[baseApi.reducerPath]: baseApi.reducer,
unculledZones: unculledZonesReducer,
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export function setupStore(preloadedState?: PreloadedState<RootState>) {
return configureStore({
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(baseApi.middleware),
preloadedState: preloadedState,
});
}
export type AppStore = ReturnType<typeof setupStore>;
export type AppDispatch = AppStore["dispatch"];
export type RootState = ReturnType<typeof rootReducer>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,

View File

@@ -0,0 +1,53 @@
import { renderWithProviders } from "../../testutils";
import AircraftLayer from "./AircraftLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockMarker = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
test("layer is empty by default", async () => {
renderWithProviders(<AircraftLayer />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
test("layer has aircraft if non-empty", async () => {
renderWithProviders(<AircraftLayer />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
bar: {
id: "bar",
blue: false,
sidc: "",
position: {
lat: 0,
lng: 0,
},
},
},
selected: null,
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockMarker).toHaveBeenCalledTimes(2);
});

View File

@@ -0,0 +1,146 @@
import { renderWithProviders } from "../../testutils";
import AirDefenseRangeLayer, { colorFor } from "./AirDefenseRangeLayer";
import { PropsWithChildren } from "react";
const mockLayerGroup = jest.fn();
const mockCircle = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("colorFor", () => {
it("has a unique color for each configuration", () => {
const params = [
[false, false],
[false, true],
[true, false],
[true, true],
];
var colors = new Set<string>();
for (const [blue, detection] of params) {
colors.add(colorFor(blue, detection));
}
expect(colors.size).toEqual(4);
});
});
describe("AirDefenseRangeLayer", () => {
it("draws nothing when there are no TGOs", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("does not draw wrong range types", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: false,
position: {
lat: 0,
lng: 0,
},
units: [],
threat_ranges: [],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).not.toHaveBeenCalled();
});
it("draws threat ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 10,
color: colorFor(true, false),
interactive: false,
})
);
});
it("draws detection ranges", () => {
renderWithProviders(<AirDefenseRangeLayer blue={true} detection />, {
preloadedState: {
tgos: {
tgos: {
foo: {
id: "foo",
name: "Foo",
control_point_name: "Bar",
category: "AA",
blue: true,
position: {
lat: 10,
lng: 20,
},
units: [],
threat_ranges: [10],
detection_ranges: [20],
dead: false,
sidc: "",
},
},
},
},
});
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 10,
lng: 20,
},
radius: 20,
color: colorFor(true, true),
interactive: false,
})
);
});
});

View File

@@ -9,7 +9,7 @@ interface TgoRangeCirclesProps {
detection?: boolean;
}
function colorFor(blue: boolean, detection: boolean) {
export function colorFor(blue: boolean, detection: boolean) {
if (blue) {
return detection ? "#bb89ff" : "#0084ff";
}

View File

@@ -0,0 +1,132 @@
import { renderWithProviders } from "../../testutils";
import Combat from "./Combat";
import { LatLng } from "leaflet";
const mockPolyline = jest.fn();
const mockPolygon = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: any) => {
mockPolyline(props);
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
describe("Combat", () => {
describe("footprint", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [[new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)]],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders single polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary],
}}
/>
);
expect(mockPolygon).toBeCalledWith(
expect.objectContaining({ positions: boundary })
);
});
// Fails because we don't handle multi-poly combat footprints correctly.
it.skip("renders multiple polygons", () => {
const boundary = [new LatLng(0, 0), new LatLng(0, 1), new LatLng(1, 0)];
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: null,
target_positions: null,
footprint: [boundary, boundary],
}}
/>
);
expect(mockPolygon).toBeCalledTimes(2);
});
});
describe("lines", () => {
it("is not interactive", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({ interactive: false })
);
});
it("renders single line", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledWith(
expect.objectContaining({
positions: [new LatLng(0, 0), new LatLng(0, 1)],
})
);
});
it("renders multiple lines", () => {
renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1), new LatLng(1, 0)],
footprint: null,
}}
/>
);
expect(mockPolyline).toBeCalledTimes(2);
});
});
it("renders nothing if no footprint or targets", () => {
const { container } = renderWithProviders(
<Combat
combat={{
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: null,
footprint: null,
}}
/>
);
expect(container).toBeEmptyDOMElement();
});
});

View File

@@ -0,0 +1,48 @@
import { renderWithProviders } from "../../testutils";
import CombatLayer from "./CombatLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
describe("CombatLayer", () => {
it("renders each combat", () => {
renderWithProviders(<CombatLayer />, {
preloadedState: {
combat: {
combat: {
foo: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
bar: {
id: "foo",
flight_position: new LatLng(0, 0),
target_positions: [new LatLng(0, 1)],
footprint: null,
},
},
},
},
});
expect(mockPolyline).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<CombatLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockPolyline).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,52 @@
import { renderWithProviders } from "../../testutils";
import ControlPointsLayer from "./ControlPointsLayer";
import { LatLng } from "leaflet";
import { PropsWithChildren } from "react";
const mockMarker = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Marker: (props: any) => {
mockMarker(props);
},
}));
describe("ControlPointsLayer", () => {
it("renders each control point", () => {
renderWithProviders(<ControlPointsLayer />, {
preloadedState: {
controlPoints: {
controlPoints: {
foo: {
id: "foo",
name: "Foo",
blue: true,
position: new LatLng(0, 0),
mobile: false,
sidc: "",
},
bar: {
id: "bar",
name: "Bar",
blue: false,
position: new LatLng(1, 0),
mobile: false,
sidc: "",
},
},
},
},
});
expect(mockMarker).toBeCalledTimes(2);
});
it("renders LayerGroup but no contents if no combat", () => {
renderWithProviders(<ControlPointsLayer />);
expect(mockLayerGroup).toBeCalledTimes(1);
expect(mockMarker).not.toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,78 @@
import { renderWithProviders } from "../../testutils";
import CullingExclusionZones from "./CullingExclusionZones";
import { PropsWithChildren } from "react";
const mockCircle = jest.fn();
const mockLayerGroup = jest.fn();
const mockLayerControlOverlay = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
LayersControl: {
Overlay: (props: PropsWithChildren<any>) => {
mockLayerControlOverlay(props);
return <>{props.children}</>;
},
},
Circle: (props: any) => {
mockCircle(props);
},
}));
describe("CullingExclusionZones", () => {
it("is empty there are no exclusion zones", () => {
renderWithProviders(<CullingExclusionZones />);
expect(mockCircle).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
expect(mockLayerControlOverlay).toHaveBeenCalledTimes(1);
});
describe("zone circles", () => {
it("are drawn in the correct locations", () => {
renderWithProviders(<CullingExclusionZones />, {
preloadedState: {
unculledZones: {
zones: [
{
position: {
lat: 0,
lng: 0,
},
radius: 10,
},
{
position: {
lat: 1,
lng: 1,
},
radius: 2,
},
],
},
},
});
expect(mockCircle).toHaveBeenCalledTimes(2);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 0,
lng: 0,
},
radius: 10,
})
);
expect(mockCircle).toHaveBeenCalledWith(
expect.objectContaining({
center: {
lat: 1,
lng: 1,
},
radius: 2,
})
);
});
it("are not interactive", () => {});
});
});

View File

@@ -30,18 +30,10 @@ const CullingExclusionCircles = (props: CullingExclusionCirclesProps) => {
export default function CullingExclusionZones() {
const data = useAppSelector(selectUnculledZones).zones;
var cez = <></>;
if (!data) {
console.log("Empty response when loading culling exclusion zones");
} else {
cez = (
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
);
}
return (
<LayersControl.Overlay name="Culling exclusion zones">
{cez}
<CullingExclusionCircles zones={data}></CullingExclusionCircles>
</LayersControl.Overlay>
);
}

View File

@@ -0,0 +1,405 @@
import { renderWithProviders } from "../../testutils";
import FlightPlansLayer from "./FlightPlansLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FlightPlansLayer", () => {
describe("unselected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if wrong coalition", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn when only selected flights are to be drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: null,
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
describe("selected flights", () => {
it("are drawn", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
bar: {
id: "bar",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn twice", () => {
renderWithProviders(<FlightPlansLayer blue={true} />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: true,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toBeCalledTimes(1);
});
it("are not drawn if red", () => {
renderWithProviders(<FlightPlansLayer blue={false} selectedOnly />, {
preloadedState: {
flights: {
flights: {
foo: {
id: "foo",
blue: false,
sidc: "",
waypoints: [
{
name: "",
position: {
lat: 0,
lng: 0,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
{
name: "",
position: {
lat: 1,
lng: 1,
},
altitude_ft: 0,
altitude_reference: "MSL",
is_movable: true,
should_mark: false,
include_in_path: true,
timing: "",
},
],
},
},
selected: "foo",
},
},
});
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});
it("are not drawn if there are no flights", () => {
renderWithProviders(<FlightPlansLayer blue={true} />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toBeCalledTimes(1);
});
});

View File

@@ -0,0 +1,32 @@
import { renderWithProviders } from "../../testutils";
import FrontLine from "./FrontLine";
import { PolylineProps } from "react-leaflet";
const mockPolyline = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: PolylineProps) => {
mockPolyline(props);
},
}));
describe("FrontLine", () => {
it("is drawn in the correct location", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 0 },
];
renderWithProviders(
<FrontLine
front={{
id: "",
extents: extents,
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
});
});

View File

@@ -0,0 +1,56 @@
import { renderWithProviders } from "../../testutils";
import FrontLinesLayer from "./FrontLinesLayer";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polyline: (props: any) => {
mockPolyline(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("FrontLinesLayer", () => {
it("draws nothing when there are no front lines", () => {
renderWithProviders(<FrontLinesLayer />);
expect(mockPolyline).not.toHaveBeenCalled();
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws front lines", () => {
const extents = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 1 },
];
renderWithProviders(<FrontLinesLayer />, {
preloadedState: {
frontLines: {
fronts: {
foo: {
id: "foo",
extents: extents,
},
bar: {
id: "bar",
extents: extents,
},
},
},
},
});
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: extents,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -3,7 +3,7 @@ import { useAppSelector } from "../../app/hooks";
import FrontLine from "../frontline";
import { LayerGroup } from "react-leaflet";
export default function SupplyRoutesLayer() {
export default function FrontLinesLayer() {
const fronts = useAppSelector(selectFrontLines).fronts;
return (
<LayerGroup>

View File

@@ -0,0 +1,125 @@
import { renderWithProviders } from "../../testutils";
import NavMeshLayer from "./NavMeshLayer";
import { PropsWithChildren } from "react";
const mockPolygon = jest.fn();
const mockLayerGroup = jest.fn();
jest.mock("react-leaflet", () => ({
LayerGroup: (props: PropsWithChildren<any>) => {
mockLayerGroup(props);
return <>{props.children}</>;
},
Polygon: (props: any) => {
mockPolygon(props);
},
}));
// The waypoints in test data below should all use `should_make: false`. Markers
// need useMap() to check the zoom level to decide if they should be drawn or
// not, and we don't have good options here for mocking that behavior.
describe("NavMeshLayer", () => {
it("draws blue meshes", () => {
const poly1 = [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 1 },
{ lat: 1, lng: 0 },
],
];
const poly2 = [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: -1 },
{ lat: 1, lng: 0 },
],
];
renderWithProviders(<NavMeshLayer blue={true} />, {
preloadedState: {
navmeshes: {
blue: [
{
poly: poly1,
threatened: false,
},
{
poly: poly2,
threatened: true,
},
],
red: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 2 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
],
},
},
});
expect(mockPolygon).toHaveBeenCalledTimes(2);
expect(mockPolygon).toHaveBeenCalledWith(
expect.objectContaining({
fillColor: "#00ff00",
positions: poly1,
interactive: false,
})
);
expect(mockPolygon).toHaveBeenCalledWith(
expect.objectContaining({
fillColor: "#ff0000",
positions: poly2,
interactive: false,
})
);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
it("draws red navmesh", () => {
renderWithProviders(<NavMeshLayer blue={false} />, {
preloadedState: {
navmeshes: {
blue: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 1 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: -1 },
{ lat: 1, lng: 0 },
],
],
threatened: true,
},
],
red: [
{
poly: [
[
{ lat: -1, lng: 0 },
{ lat: 0, lng: 2 },
{ lat: 1, lng: 0 },
],
],
threatened: false,
},
],
},
},
});
expect(mockPolygon).toHaveBeenCalledTimes(1);
expect(mockLayerGroup).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,16 @@
import SplitLines from "./SplitLines";
import { screen } from "@testing-library/dom";
import { render } from "@testing-library/react";
describe("SplitLines", () => {
it("joins items with line break tags", () => {
render(
<div data-testid={"container"}>
<SplitLines items={["foo", "bar", "baz"]} />
</div>
);
const container = screen.getByTestId("container");
expect(container).toContainHTML("foo<br />bar<br />baz<br />");
});
});

View File

@@ -0,0 +1,159 @@
import { renderWithProviders } from "../../testutils";
import SupplyRoute, { RouteColor } from "./SupplyRoute";
import { screen } from "@testing-library/react";
import { PropsWithChildren } from "react";
const mockPolyline = jest.fn();
jest.mock("react-leaflet", () => ({
Polyline: (props: PropsWithChildren<any>) => {
mockPolyline(props);
return <>{props.children}</>;
},
Tooltip: (props: PropsWithChildren<any>) => {
return <p data-testid="tooltip">{props.children}</p>;
},
}));
describe("SupplyRoute", () => {
it("is red when inactive and owned by opfor", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: [],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Red,
})
);
});
it("is blue when inactive and owned by bluefor", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: true,
active_transports: [],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Blue,
})
);
});
it("is orange when contested", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: true,
is_sea: false,
blue: false,
active_transports: [],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Contested,
})
);
});
it("is highlighted when the route has active transports", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: ["foo"],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
color: RouteColor.Highlight,
})
);
});
it("is drawn in the right place", () => {
const points = [
{ lat: 0, lng: 0 },
{ lat: 1, lng: 1 },
];
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: points,
front_active: false,
is_sea: false,
blue: false,
active_transports: ["foo"],
}}
/>
);
expect(mockPolyline).toHaveBeenCalledTimes(2);
expect(mockPolyline).toHaveBeenCalledWith(
expect.objectContaining({
positions: points,
})
);
});
it("has a tooltip describing an inactive supply route", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: [],
}}
/>
);
const tooltip = screen.getByTestId("tooltip");
expect(tooltip).toHaveTextContent("This supply route is inactive.");
});
it("has a tooltip describing active supply routes", () => {
renderWithProviders(
<SupplyRoute
route={{
id: "",
points: [],
front_active: false,
is_sea: false,
blue: false,
active_transports: ["foo", "bar"],
}}
/>
);
const tooltip = screen.getByTestId("tooltip");
expect(tooltip).toContainHTML("foo<br />bar");
});
});

View File

@@ -4,6 +4,13 @@ import { Polyline as LPolyline } from "leaflet";
import { useEffect, useRef } from "react";
import { Polyline, Tooltip } from "react-leaflet";
export enum RouteColor {
Blue = "#2d3e50",
Contested = "#c85050",
Highlight = "#ffffff",
Red = "#8c1414",
}
interface SupplyRouteProps {
route: SupplyRouteModel;
}
@@ -26,18 +33,22 @@ function ActiveSupplyRouteHighlight(props: SupplyRouteProps) {
}
return (
<Polyline positions={props.route.points} color={"#ffffff"} weight={2} />
<Polyline
positions={props.route.points}
color={RouteColor.Highlight}
weight={2}
/>
);
}
function colorFor(route: SupplyRouteModel) {
if (route.front_active) {
return "#c85050";
return RouteColor.Contested;
}
if (route.blue) {
return "#2d3e50";
return RouteColor.Blue;
}
return "#8c1414";
return RouteColor.Red;
}
export default function SupplyRoute(props: SupplyRouteProps) {

View File

@@ -1,5 +1,5 @@
import App from "./App";
import { store } from "./app/store";
import { setupStore } from "./app/store";
import { SocketProvider } from "./components/socketprovider/socketprovider";
import "./index.css";
import * as serviceWorker from "./serviceWorker";
@@ -12,7 +12,7 @@ const root = ReactDOM.createRoot(
);
root.render(
<React.StrictMode>
<Provider store={store}>
<Provider store={setupStore()}>
<SocketProvider>
<App />
</SocketProvider>

View File

@@ -0,0 +1,30 @@
// https://redux.js.org/usage/writing-tests
import { setupStore } from "../app/store";
import type { AppStore, RootState } from "../app/store";
import type { PreloadedState } from "@reduxjs/toolkit";
import { render } from "@testing-library/react";
import type { RenderOptions } from "@testing-library/react";
import React, { PropsWithChildren } from "react";
import { Provider } from "react-redux";
// This type interface extends the default options for render from RTL, as well
// as allows the user to specify other things such as initialState, store.
interface ExtendedRenderOptions extends Omit<RenderOptions, "queries"> {
preloadedState?: PreloadedState<RootState>;
store?: AppStore;
}
export function renderWithProviders(
ui: React.ReactElement,
{
preloadedState = {},
// Automatically create a store instance if no store was passed in
store = setupStore(preloadedState),
...renderOptions
}: ExtendedRenderOptions = {}
) {
function Wrapper({ children }: PropsWithChildren<{}>): JSX.Element {
return <Provider store={store}>{children}</Provider>;
}
return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

View File

@@ -2,7 +2,7 @@ coverage:
status:
patch:
default:
informational: false
informational: true
project:
default:
informational: true

View File

@@ -9,7 +9,7 @@
project = "DCS Liberation"
copyright = "2023, DCS Liberation Team"
author = "DCS Liberation Team"
release = "7.0.0"
release = "8.1.0"
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration

View File

@@ -21,6 +21,7 @@ from .planningerror import PlanningError
from ..flightwaypointtype import FlightWaypointType
from ..starttype import StartType
from ..traveltime import GroundSpeed, TravelTime
from ...savecompat import has_save_compat_for
if TYPE_CHECKING:
from game.dcs.aircrafttype import FuelConsumption
@@ -62,6 +63,13 @@ class FlightPlan(ABC, Generic[LayoutT]):
def __init__(self, flight: Flight, layout: LayoutT) -> None:
self.flight = flight
self.layout = layout
self.tot_offset = self.default_tot_offset()
@has_save_compat_for(7)
def __setstate__(self, state: dict[str, Any]) -> None:
if "tot_offset" not in state:
state["tot_offset"] = self.default_tot_offset()
self.__dict__.update(state)
@property
def package(self) -> Package:
@@ -195,8 +203,7 @@ class FlightPlan(ABC, Generic[LayoutT]):
[meters(cp.position.distance_to_point(w.position)) for w in self.waypoints]
)
@property
def tot_offset(self) -> timedelta:
def default_tot_offset(self) -> timedelta:
"""This flight's offset from the package's TOT.
Positive values represent later TOTs. An offset of -2 minutes is used

View File

@@ -25,10 +25,6 @@ if TYPE_CHECKING:
class FormationAttackFlightPlan(FormationFlightPlan, ABC):
@property
def lead_time(self) -> timedelta:
return timedelta()
@property
def package_speed_waypoints(self) -> set[FlightWaypoint]:
return {
@@ -50,13 +46,6 @@ class FormationAttackFlightPlan(FormationFlightPlan, ABC):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.targets[0]
@property
def tot_offset(self) -> timedelta:
try:
return -self.lead_time
except AttributeError:
return timedelta()
@property
def target_area_waypoint(self) -> FlightWaypoint:
return FlightWaypoint(

View File

@@ -16,9 +16,8 @@ class SeadFlightPlan(FormationAttackFlightPlan):
def builder_type() -> Type[Builder]:
return Builder
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=1)
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=1)
class Builder(FormationAttackBuilder[SeadFlightPlan, FormationAttackLayout]):

View File

@@ -38,10 +38,6 @@ class SweepLayout(LoiterLayout):
class SweepFlightPlan(LoiterFlightPlan):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=5)
@staticmethod
def builder_type() -> Type[Builder]:
return Builder
@@ -54,9 +50,8 @@ class SweepFlightPlan(LoiterFlightPlan):
def tot_waypoint(self) -> FlightWaypoint:
return self.layout.sweep_end
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=5)
@property
def sweep_start_time(self) -> datetime:

View File

@@ -34,10 +34,6 @@ class TarCapLayout(PatrollingLayout):
class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
@property
def lead_time(self) -> timedelta:
return timedelta(minutes=2)
@property
def patrol_duration(self) -> timedelta:
# Note that this duration only has an effect if there are no
@@ -64,9 +60,8 @@ class TarCapFlightPlan(PatrollingFlightPlan[TarCapLayout]):
def combat_speed_waypoints(self) -> set[FlightWaypoint]:
return {self.layout.patrol_start, self.layout.patrol_end}
@property
def tot_offset(self) -> timedelta:
return -self.lead_time
def default_tot_offset(self) -> timedelta:
return -timedelta(minutes=2)
def depart_time_for_waypoint(self, waypoint: FlightWaypoint) -> datetime | None:
if waypoint == self.layout.patrol_end:

View File

@@ -133,6 +133,7 @@ class StateData:
+ data["kill_events"]
+ data["crash_events"]
+ data["dead_events"]
+ data["killed_ground_units"]
)
for unit in killed_units: # organize killed units into aircraft vs ground
if unit_map.flight(unit) is not None:

View File

@@ -5,7 +5,6 @@ import logging
import math
from collections.abc import Iterator
from datetime import date, datetime, time, timedelta
from enum import Enum
from typing import Any, List, TYPE_CHECKING, Type, Union, cast
from dcs.countries import Switzerland, USAFAggressors, UnitedNationsPeacekeepers
@@ -37,6 +36,7 @@ from .theater.theatergroundobject import (
)
from .theater.transitnetwork import TransitNetwork, TransitNetworkBuilder
from .timeofday import TimeOfDay
from .turnstate import TurnState
from .weather.conditions import Conditions
if TYPE_CHECKING:
@@ -81,12 +81,6 @@ AWACS_BUDGET_COST = 4
PLAYER_BUDGET_IMPORTANCE_LOG = 2
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2
class Game:
def __init__(
self,

View File

@@ -353,6 +353,14 @@ class MissionGenerator:
gen.generate()
def setup_combined_arms(self) -> None:
self.mission.groundControl.pilot_can_control_vehicles = COMBINED_ARMS_SLOTS > 0
self.mission.groundControl.blue_tactical_commander = COMBINED_ARMS_SLOTS
self.mission.groundControl.blue_observer = 1
self.mission.groundControl.blue_game_masters = (
self.game.settings.game_master_slots
)
self.mission.groundControl.blue_tactical_commander = (
self.game.settings.tactical_commander_slots
)
self.mission.groundControl.pilot_can_control_vehicles = (
self.mission.groundControl.blue_tactical_commander > 0
)
self.mission.groundControl.blue_jtac = self.game.settings.jtac_operator_slots
self.mission.groundControl.blue_observer = self.game.settings.observer_slots

View File

@@ -315,6 +315,10 @@ class MissileSiteGenerator(GroundObjectGenerator):
def generate(self) -> None:
super(MissileSiteGenerator, self).generate()
if not self.game.settings.generate_fire_tasks_for_missile_sites:
return
# Note : Only the SCUD missiles group can fire (V1 site cannot fire in game right now)
# TODO : Should be pre-planned ?
# TODO : Add delay to task to spread fire task over mission duration ?

View File

@@ -25,6 +25,7 @@ class SaveGameBundle:
MANUAL_SAVE_NAME = "player.liberation"
LAST_TURN_SAVE_NAME = "last_turn.liberation"
START_OF_TURN_SAVE_NAME = "start_of_turn.liberation"
PRE_SIM_CHECKPOINT_SAVE_NAME = "pre_sim_checkpoint.liberation"
def __init__(self, bundle_path: Path) -> None:
self.bundle_path = bundle_path
@@ -58,6 +59,19 @@ class SaveGameBundle:
game, self.START_OF_TURN_SAVE_NAME, copy_from=self
)
def save_pre_sim_checkpoint(self, game: Game) -> None:
"""Writes the save file for the state before beginning simulation.
This save is the state of the game after the player presses "TAKE OFF", but
before the fast-forward simulation begins. It is not practical to rewind, but
players commonly will want to cancel and continue planning after pressing that
button, so we make a checkpoint that we can reload on abort.
"""
with logged_duration("Saving pre-sim checkpoint"):
self._update_bundle_member(
game, self.PRE_SIM_CHECKPOINT_SAVE_NAME, copy_from=self
)
def load_player(self) -> Game:
"""Loads the save manually created by the player via save/save-as."""
return self._load_from(self.MANUAL_SAVE_NAME)
@@ -70,6 +84,10 @@ class SaveGameBundle:
"""Loads the save automatically created at the end of the last turn."""
return self._load_from(self.LAST_TURN_SAVE_NAME)
def load_pre_sim_checkpoint(self) -> Game:
"""Loads the save automatically created before the simulation began."""
return self._load_from(self.PRE_SIM_CHECKPOINT_SAVE_NAME)
def _load_from(self, name: str) -> Game:
with ZipFile(self.bundle_path) as zip_bundle:
with zip_bundle.open(name, "r") as save:

View File

@@ -51,6 +51,10 @@ class SaveManager:
with self._save_bundle_context() as bundle:
bundle.save_start_of_turn(self.game)
def save_pre_sim_checkpoint(self) -> None:
with self._save_bundle_context() as bundle:
bundle.save_pre_sim_checkpoint(self.game)
def set_loaded_from(self, bundle: SaveGameBundle) -> None:
"""Reconfigures this save manager based on the loaded game.
@@ -81,6 +85,9 @@ class SaveManager:
self.last_saved_bundle = previous_saved_bundle
raise
def load_pre_sim_checkpoint(self) -> Game:
return self.default_save_bundle.load_pre_sim_checkpoint()
@staticmethod
def load_last_turn(bundle_path: Path) -> Game:
return SaveGameBundle(bundle_path).load_last_turn()

View File

@@ -1,6 +1,7 @@
"""Runway information and selection."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from typing import Iterator, Optional, TYPE_CHECKING
@@ -51,7 +52,20 @@ class RunwayData:
atc = atc_radio.uhf
for beacon_data in airport.beacons:
beacon = Beacons.with_id(beacon_data.id, theater)
try:
beacon = Beacons.with_id(beacon_data.id, theater)
except KeyError:
# DCS data is not always correct. At time of writing, Hatzor in Sinai
# claims to have a beacon named airfield20_0, but the Sinai beacons.lua
# has no such beacon.
# See https://github.com/dcs-liberation/dcs_liberation/issues/3021.
logging.exception(
"Airport %s claims to have beacon %s but the map has no beacon "
"with that ID",
airport.name,
beacon_data.id,
)
continue
if beacon.is_tacan:
tacan = beacon.tacan_channel
tacan_callsign = beacon.callsign

View File

@@ -41,7 +41,8 @@ def has_save_compat_for(
"""
def decorator(func: Callable[..., ReturnT]) -> Callable[..., ReturnT]:
if major != MAJOR_VERSION:
# Allow current and previous version to ease cherry-picking.
if major not in {MAJOR_VERSION - 1, MAJOR_VERSION}:
raise DeprecatedSaveCompatError(func.__name__)
return func

View File

@@ -6,30 +6,16 @@ from starlette.responses import Response
from game import Game
from game.ato import Flight
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.server import GameContext
from game.server.leaflet import LeafletPoint
from game.server.waypoints.models import FlightWaypointJs
from game.sim import GameUpdateEvents
from game.utils import meters
router: APIRouter = APIRouter(prefix="/waypoints")
def waypoints_for_flight(flight: Flight) -> list[FlightWaypointJs]:
departure = FlightWaypointJs.for_waypoint(
FlightWaypoint(
"TAKEOFF",
FlightWaypointType.TAKEOFF,
flight.departure.position,
meters(0),
"RADIO",
),
flight,
0,
)
return [departure] + [
return [
FlightWaypointJs.for_waypoint(w, flight, i)
for i, w in enumerate(flight.flight_plan.waypoints, 1)
]
@@ -64,7 +50,7 @@ def set_position(
if waypoint_idx == 0:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
waypoint = flight.flight_plan.waypoints[waypoint_idx - 1]
waypoint = flight.flight_plan.waypoints[waypoint_idx]
waypoint.position = Point.from_latlng(
LatLng(position.lat, position.lng), game.theater.terrain
)

View File

@@ -42,6 +42,8 @@ HQ_AUTOMATION_SECTION = "HQ Automation"
MISSION_GENERATOR_PAGE = "Mission Generator"
COMMANDERS_SECTION = "Battlefield Commanders"
GAMEPLAY_SECTION = "Gameplay"
# TODO: Make sections a type and add headers.
@@ -310,6 +312,41 @@ class Settings:
reserves_procurement_target: int = 10
# Mission Generator
# Commanders
game_master_slots: int = bounded_int_option(
"Game master",
page=MISSION_GENERATOR_PAGE,
section=COMMANDERS_SECTION,
default=0,
min=0,
max=100,
)
tactical_commander_slots: int = bounded_int_option(
"Tactical commander",
page=MISSION_GENERATOR_PAGE,
section=COMMANDERS_SECTION,
default=1,
min=0,
max=100,
)
jtac_operator_slots: int = bounded_int_option(
"JTAC/Operator",
page=MISSION_GENERATOR_PAGE,
section=COMMANDERS_SECTION,
default=0,
min=0,
max=100,
)
observer_slots: int = bounded_int_option(
"Observer",
page=MISSION_GENERATOR_PAGE,
section=COMMANDERS_SECTION,
default=1,
min=0,
max=100,
)
# Gameplay
fast_forward_to_first_contact: bool = boolean_option(
"Fast forward mission to first contact (WIP)",
@@ -324,6 +361,17 @@ class Settings:
"modifications."
),
)
reload_pre_sim_checkpoint_on_abort: bool = boolean_option(
"Reset mission to pre-take off conditions on abort",
page=MISSION_GENERATOR_PAGE,
section=GAMEPLAY_SECTION,
default=True,
detail=(
"If enabled, the game will automatically reload a pre-take off save when "
"aborting take off. If this is disabled, you will need to manually re-load "
"your game after aborting take off."
),
)
player_mission_interrupts_sim_at: Optional[StartType] = choices_option(
"Player missions interrupt fast forward",
page=MISSION_GENERATOR_PAGE,
@@ -443,6 +491,16 @@ class Settings:
section=PERFORMANCE_SECTION,
default=True,
)
generate_fire_tasks_for_missile_sites: bool = boolean_option(
"Generate fire tasks for missile sites",
page=MISSION_GENERATOR_PAGE,
section=PERFORMANCE_SECTION,
detail=(
"If enabled, missile sites like V2s and Scuds will fire on random targets "
"at the start of the mission."
),
default=True,
)
perf_moving_units: bool = boolean_option(
"Moving ground units",
page=MISSION_GENERATOR_PAGE,
@@ -523,6 +581,10 @@ class Settings:
with settings_path.open(encoding="utf-8") as settings_file:
data = yaml.safe_load(settings_file)
if data is None:
logging.warning("Saved settings file %s is empty", settings_path)
return
expected_types = get_type_hints(Settings)
for key, value in data.items():
if key not in self.__dict__:

View File

@@ -11,12 +11,12 @@ from game.ato.flightstate import (
Uninitialized,
)
from .combat import CombatInitiator, FrozenCombat
from .gameupdateevents import GameUpdateEvents
from .simulationresults import SimulationResults
if TYPE_CHECKING:
from game import Game
from game.ato import Flight
from .gameupdateevents import GameUpdateEvents
class AircraftSimulation:
@@ -72,6 +72,7 @@ class AircraftSimulation:
def reset(self) -> None:
for flight in self.iter_flights():
flight.set_state(Uninitialized(flight, self.game.settings))
self.combats = []
def iter_flights(self) -> Iterator[Flight]:
packages = itertools.chain(

View File

@@ -39,6 +39,7 @@ class GameLoop:
def start(self) -> None:
if self.started:
raise RuntimeError("Cannot start game loop because it has already started")
self.game.save_manager.save_pre_sim_checkpoint()
self.started = True
self.sim.begin_simulation()

View File

@@ -5,7 +5,8 @@ import random
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Sequence, TYPE_CHECKING
from typing import Optional, Sequence, TYPE_CHECKING, Any
from uuid import uuid4, UUID
from faker import Faker
@@ -13,6 +14,7 @@ from game.ato import Flight, FlightType, Package
from game.settings import AutoAtoBehavior, Settings
from .pilot import Pilot, PilotStatus
from ..db.database import Database
from ..savecompat import has_save_compat_for
from ..utils import meters
if TYPE_CHECKING:
@@ -26,6 +28,8 @@ if TYPE_CHECKING:
@dataclass
class Squadron:
id: UUID = field(init=False, default_factory=uuid4)
name: str
nickname: Optional[str]
country: str
@@ -61,21 +65,24 @@ class Squadron:
untasked_aircraft: int = field(init=False, hash=False, compare=False, default=0)
pending_deliveries: int = field(init=False, hash=False, compare=False, default=0)
@has_save_compat_for(7)
def __setstate__(self, state: dict[str, Any]) -> None:
if "id" not in state:
state["id"] = uuid4()
self.__dict__.update(state)
def __str__(self) -> str:
if self.nickname is None:
return self.name
return f'{self.name} "{self.nickname}"'
def __hash__(self) -> int:
return hash(
(
self.name,
self.nickname,
self.country,
self.role,
self.aircraft,
)
)
return hash(self.id)
def __eq__(self, other: object) -> bool:
if not isinstance(other, Squadron):
return False
return self.id == other.id
@property
def player(self) -> bool:

View File

@@ -4,10 +4,17 @@ from functools import cached_property
from typing import Optional, Tuple, Union
import logging
from pathlib import Path
from typing import List
from shapely import geometry
from shapely.geometry import MultiPolygon, Polygon
from dcs.drawing.drawing import LineStyle, Rgba
from dcs.drawing.polygon import FreeFormPolygon
from dcs.mapping import Point
from dcs.mission import Mission
from dcs.terrain.terrain import Terrain
@dataclass(frozen=True)
class Landmap:
@@ -39,3 +46,94 @@ def load_landmap(filename: Path) -> Optional[Landmap]:
def poly_contains(x: float, y: float, poly: Union[MultiPolygon, Polygon]) -> bool:
return poly.contains(geometry.Point(x, y))
def to_miz(landmap: Landmap, terrain: Terrain, mission_filename: str) -> None:
"""
Writes landmap to .miz file so that zones can be visualized and edited in the
mission editor.
"""
def multi_polygon_to_miz(
mission: Mission,
terrain: Terrain,
multi_polygon: MultiPolygon,
color: Rgba,
prefix: str,
layer_index: int = 4,
layer_name: str = "Author",
) -> None:
reference_position = Point(0, 0, terrain)
for i in range(len(multi_polygon.geoms)):
polygon = multi_polygon.geoms[i]
if len(polygon.interiors) > 0:
raise ValueError(
f"Polygon hole found when trying to export {prefix} {i}. to_miz() does not support landmap zones with holes."
)
coordinates = polygon.exterior.xy
points = []
for j in range(len(coordinates[0])):
points.append(Point(coordinates[0][j], coordinates[1][j], terrain))
polygon_drawing = FreeFormPolygon(
visible=True,
position=reference_position,
name=f"{prefix}-{i}",
color=color,
layer_name=layer_name,
fill=color,
line_thickness=10,
line_style=LineStyle.Solid,
points=points,
)
mission.drawings.layers[layer_index].objects.append(polygon_drawing)
mission = Mission(terrain=terrain)
multi_polygon_to_miz(
mission, terrain, landmap.exclusion_zones, Rgba(255, 0, 0, 128), "Exclusion"
)
multi_polygon_to_miz(
mission, terrain, landmap.sea_zones, Rgba(0, 0, 255, 128), "Sea"
)
multi_polygon_to_miz(
mission, terrain, landmap.inclusion_zones, Rgba(0, 255, 0, 128), "Inclusion"
)
mission.save(mission_filename)
def from_miz(mission_filename: str, layer_index: int = 4) -> Landmap:
"""
Generate Landmap object from Free Form Polygons drawn in a .miz file.
Landmap.inclusion_zones are expected to be named Inclusion-<suffix>
Landmap.exclusion_zones are expected to be named Exclusion-<suffix>
Landmap.sea_zones are expected to be named Sea-<suffix>
"""
mission = Mission()
mission.load_file(mission_filename)
polygons: dict[str, List[Polygon]] = {"Inclusion": [], "Exclusion": [], "Sea": []}
for draw_object in mission.drawings.layers[layer_index].objects:
if type(draw_object) != FreeFormPolygon:
logging.debug(
f"Object {draw_object.name} is not a FreeFormPolygon, ignoring"
)
continue
name_split = draw_object.name.split(
"-"
) # names are in the format <Inclusion|Exclusion|Sea>-<suffix>
zone_type = name_split[0]
if len(name_split) != 2 or zone_type not in ("Exclusion", "Sea", "Inclusion"):
logging.debug(
f"Object name {draw_object.name} does not conform to expected format <Exclusion|Sea|Inclusion>-<suffix>, ignoring"
)
continue
polygon_points = []
for point in draw_object.points:
polygon_points.append(
(point.x + draw_object.position.x, point.y + draw_object.position.y)
)
polygons[zone_type].append(Polygon(polygon_points))
landmap = Landmap(
inclusion_zones=MultiPolygon(polygons["Inclusion"]),
exclusion_zones=MultiPolygon(polygons["Exclusion"]),
sea_zones=MultiPolygon(polygons["Sea"]),
)
return landmap

View File

@@ -434,6 +434,14 @@ class MissileSiteGroundObject(TheaterGroundObject):
def should_head_to_conflict(self) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType
if not self.is_friendly(for_player):
yield FlightType.BAI
for mission_type in super().mission_types(for_player):
yield mission_type
class CoastalSiteGroundObject(TheaterGroundObject):
def __init__(
@@ -466,6 +474,14 @@ class CoastalSiteGroundObject(TheaterGroundObject):
def should_head_to_conflict(self) -> bool:
return True
def mission_types(self, for_player: bool) -> Iterator[FlightType]:
from game.ato import FlightType
if not self.is_friendly(for_player):
yield FlightType.BAI
for mission_type in super().mission_types(for_player):
yield mission_type
class IadsGroundObject(TheaterGroundObject, ABC):
def __init__(

View File

@@ -14,6 +14,7 @@ from dcs.terrain import (
Nevada,
Normandy,
PersianGulf,
Sinai,
Syria,
TheChannel,
)
@@ -31,6 +32,7 @@ ALL_TERRAINS = [
MarianaIslands(),
Nevada(),
TheChannel(),
Sinai(),
Syria(),
]

9
game/turnstate.py Normal file
View File

@@ -0,0 +1,9 @@
from __future__ import annotations
from enum import Enum
class TurnState(Enum):
WIN = 0
LOSS = 1
CONTINUE = 2

View File

@@ -1,8 +1,8 @@
from pathlib import Path
MAJOR_VERSION = 7
MINOR_VERSION = 0
MAJOR_VERSION = 8
MINOR_VERSION = 1
MICRO_VERSION = 0
VERSION_NUMBER = ".".join(str(v) for v in (MAJOR_VERSION, MINOR_VERSION, MICRO_VERSION))
@@ -175,4 +175,14 @@ VERSION = _build_version_string()
#:
#: Version 10.7
#: * Support for defining squadron sizes.
CAMPAIGN_FORMAT_VERSION = (10, 7)
#:
#: Version 10.8
#: * Support for Normandy 2.
#:
#: Version 10.9
#: * Campaign is compatible with new squadron rules. The default air wing configuration
#: has enough parking available at each base when squadrons begin at full strength.
#:
#: Version 10.10
#: * Support for Sinai.
CAMPAIGN_FORMAT_VERSION = (10, 10)

View File

@@ -9,8 +9,14 @@ from pydcs_extensions.weapon_injector import inject_weapons
class WeaponsOV10A:
LAU_33A = {"clsid": "{LAU-33A}", "name": "LAU-33A", "weight": 155}
Mk4_mod_0 = {"clsid": "{MK4_Mod0_OV10}", "name": "Mk4 mod 0", "weight": 612.35}
OV10_SMOKE = {"clsid": "{OV10_SMOKE}", "name": "OV10_SMOKE", "weight": 1}
ParaTrooper = {"clsid": "{PARA}", "name": "ParaTrooper", "weight": 80}
OV10_Paratrooper = {
"clsid": "OV10_Paratrooper",
"name": "OV10_Paratrooper",
"weight": 400,
}
Fuel_Tank_150_gallons_ = {
"clsid": "{150gal}",
"name": "Fuel Tank 150 gallons",
@@ -47,6 +53,11 @@ class Bronco_OV_10A(PlaneType):
1,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
LAU_7_with_AIM_9B_Sidewinder_IR_AAM = (
1,
Weapons.LAU_7_with_AIM_9B_Sidewinder_IR_AAM,
)
LAU_33A = (1, Weapons.LAU_33A)
# ERRR {MK-81}
@@ -61,6 +72,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (2, Weapons.LAU3_HE5)
LAU3_HE151 = (2, Weapons.LAU3_HE151)
M260_HYDRA = (2, Weapons.M260_HYDRA)
M260_HYDRA_WP = (2, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
2,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -69,6 +81,62 @@ class Bronco_OV_10A(PlaneType):
2,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
2,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
2,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
# ERRR {MK-81}
@@ -83,6 +151,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (3, Weapons.LAU3_HE5)
LAU3_HE151 = (3, Weapons.LAU3_HE151)
M260_HYDRA = (3, Weapons.M260_HYDRA)
M260_HYDRA_WP = (3, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
3,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -91,6 +160,62 @@ class Bronco_OV_10A(PlaneType):
3,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
3,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
3,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
class Pylon4:
Fuel_Tank_150_gallons_ = (4, Weapons.Fuel_Tank_150_gallons_)
@@ -99,6 +224,7 @@ class Bronco_OV_10A(PlaneType):
Mk_82_Snakeye___500lb_GP_Bomb_HD = (4, Weapons.Mk_82_Snakeye___500lb_GP_Bomb_HD)
Mk_83___1000lb_GP_Bomb_LD = (4, Weapons.Mk_83___1000lb_GP_Bomb_LD)
M117___750lb_GP_Bomb_LD = (4, Weapons.M117___750lb_GP_Bomb_LD)
Mk4_mod_0 = (4, Weapons.Mk4_mod_0)
# ERRR {MK-81}
@@ -113,6 +239,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (5, Weapons.LAU3_HE5)
LAU3_HE151 = (5, Weapons.LAU3_HE151)
M260_HYDRA = (5, Weapons.M260_HYDRA)
M260_HYDRA_WP = (5, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
5,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -121,6 +248,62 @@ class Bronco_OV_10A(PlaneType):
5,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
5,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
5,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
5,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
# ERRR {MK-81}
@@ -135,6 +318,7 @@ class Bronco_OV_10A(PlaneType):
LAU3_HE5 = (6, Weapons.LAU3_HE5)
LAU3_HE151 = (6, Weapons.LAU3_HE151)
M260_HYDRA = (6, Weapons.M260_HYDRA)
M260_HYDRA_WP = (6, Weapons.M260_HYDRA_WP)
LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG = (
6,
Weapons.LAU_10R_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
@@ -143,15 +327,76 @@ class Bronco_OV_10A(PlaneType):
6,
Weapons.LAU_10_pod___4_x_127mm_ZUNI__UnGd_Rkts_Mk71__HE_FRAG,
)
LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
6,
Weapons.LAU_61R_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
6,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
6,
Weapons.LAU_61_pod___19_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos = (
6,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE = (
6,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk1__HE,
)
LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT = (
6,
Weapons.LAU_68_pod___7_x_2_75_FFAR__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M151__HE,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M156__Wht_Phos,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M257__Para_Illum,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_M274__Practice_Smk,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk1__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk5__HEAT,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_Mk61__Practice,
)
LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice = (
6,
Weapons.LAU_68_pod___7_x_2_75_Hydra__UnGd_Rkts_WTU_1_B__Practice,
)
class Pylon7:
LAU_7_with_AIM_9P_Sidewinder_IR_AAM = (
7,
Weapons.LAU_7_with_AIM_9P_Sidewinder_IR_AAM,
)
LAU_7_with_AIM_9B_Sidewinder_IR_AAM = (
7,
Weapons.LAU_7_with_AIM_9B_Sidewinder_IR_AAM,
)
LAU_33A = (7, Weapons.LAU_33A)
class Pylon8:
ParaTrooper = (8, Weapons.ParaTrooper)
OV10_Paratrooper = (8, Weapons.OV10_Paratrooper)
class Pylon9:
OV10_SMOKE = (9, Weapons.OV10_SMOKE)

28
qt_ui/cheatcontext.py Normal file
View File

@@ -0,0 +1,28 @@
from __future__ import annotations
from collections.abc import Iterator
from contextlib import contextmanager
from typing import TYPE_CHECKING
from game.server import EventStream
from game.turnstate import TurnState
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.gameoverdialog import GameOverDialog
if TYPE_CHECKING:
from game import Game
from game.sim import GameUpdateEvents
@contextmanager
def game_state_modifying_cheat_context(game: Game) -> Iterator[GameUpdateEvents]:
with EventStream.event_context() as events:
yield events
state = game.check_win_loss()
if state is not TurnState.CONTINUE:
dialog = GameOverDialog(won=state is TurnState.WIN)
dialog.exec()
else:
game.initialize_turn(events)
GameUpdateSignal.get_instance().updateGame(game)

View File

@@ -1,17 +1,19 @@
from __future__ import annotations
import argparse
import logging
import ntpath
import os
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Optional
import yaml
from PySide6 import QtWidgets
from PySide6.QtCore import Qt
from PySide6.QtGui import QPixmap
from PySide6.QtWidgets import QApplication, QCheckBox, QSplashScreen
from PySide6.QtWidgets import QApplication, QCheckBox, QSplashScreen, QDialog
from dcs.payloads import PayloadDirectories
from game import Game, VERSION, logging_config, persistence
@@ -34,6 +36,7 @@ from qt_ui import (
uiconstants,
)
from qt_ui.uiflags import UiFlags
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.QLiberationWindow import QLiberationWindow
from qt_ui.windows.preferences.QLiberationFirstStartWindow import (
@@ -67,7 +70,7 @@ def on_game_load(game: Game | None) -> None:
EventStream.put_nowait(GameUpdateEvents().game_loaded(game))
def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
def run_ui(create_game_params: CreateGameParams | None, ui_flags: UiFlags) -> None:
os.environ["QT_ENABLE_HIGHDPI_SCALING"] = "1" # Potential fix for 4K screens
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
@@ -108,8 +111,6 @@ def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
uiconstants.load_event_icons()
uiconstants.load_aircraft_icons()
uiconstants.load_vehicle_icons()
uiconstants.load_aircraft_banners()
uiconstants.load_vehicle_banners()
# Show warning if no DCS Installation directory was set
if liberation_install.get_dcs_install_directory() == "":
@@ -151,6 +152,11 @@ def run_ui(game: Game | None, ui_flags: UiFlags) -> None:
GameUpdateSignal()
GameUpdateSignal.get_instance().game_loaded.connect(on_game_load)
game: Game | None = None
if create_game_params is not None:
with logged_duration("New game creation"):
game = create_game(create_game_params)
# Start window
window = QLiberationWindow(game, ui_flags)
window.showMaximized()
@@ -253,6 +259,12 @@ def parse_args() -> argparse.Namespace:
"--advanced-iads", action="store_true", help="Enable advanced IADS."
)
new_game.add_argument(
"--show-air-wing-config",
action="store_true",
help="Show the air wing configuration dialog after generating the game.",
)
lint_weapons = subparsers.add_parser("lint-weapons")
lint_weapons.add_argument("aircraft", help="Name of the aircraft variant to lint.")
@@ -261,60 +273,68 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()
def create_game(
campaign_path: Path,
blue: str,
red: str,
supercarrier: bool,
auto_procurement: bool,
inverted: bool,
cheats: bool,
start_date: datetime,
restrict_weapons_by_date: bool,
advanced_iads: bool,
use_new_squadron_rules: bool,
) -> Game:
first_start = liberation_install.init()
if first_start:
sys.exit(
"Cannot generate campaign without configuring DCS Liberation. Start the UI "
"for the first run configuration."
@dataclass(frozen=True)
class CreateGameParams:
campaign_path: Path
blue: str
red: str
supercarrier: bool
auto_procurement: bool
inverted: bool
cheats: bool
start_date: datetime
restrict_weapons_by_date: bool
advanced_iads: bool
use_new_squadron_rules: bool
show_air_wing_config: bool
@staticmethod
def from_args(args: argparse.Namespace) -> CreateGameParams | None:
if args.subcommand != "new-game":
return None
return CreateGameParams(
args.campaign,
args.blue,
args.red,
args.supercarrier,
args.auto_procurement,
args.inverted,
args.cheats,
args.date,
args.restrict_weapons_by_date,
args.advanced_iads,
args.use_new_squadron_rules,
args.show_air_wing_config,
)
# This needs to run before the pydcs payload cache is created, which happens
# extremely early. It's not a problem that we inject these paths twice because we'll
# get the same answers each time.
#
# Without this, it is not possible to use next turn (or anything that needs to check
# for loadouts) without saving the generated campaign and reloading it the normal
# way.
inject_custom_payloads(Path(persistence.base_path()))
campaign = Campaign.from_file(campaign_path)
theater = campaign.load_theater(advanced_iads)
def create_game(params: CreateGameParams) -> Game:
campaign = Campaign.from_file(params.campaign_path)
theater = campaign.load_theater(params.advanced_iads)
faction_loader = Factions.load()
lua_plugin_manager = LuaPluginManager.load()
lua_plugin_manager.merge_player_settings()
generator = GameGenerator(
faction_loader.get_by_name(blue),
faction_loader.get_by_name(red),
faction_loader.get_by_name(params.blue),
faction_loader.get_by_name(params.red),
theater,
campaign.load_air_wing_config(theater),
Settings(
supercarrier=supercarrier,
automate_runway_repair=auto_procurement,
automate_front_line_reinforcements=auto_procurement,
automate_aircraft_reinforcements=auto_procurement,
enable_frontline_cheats=cheats,
enable_base_capture_cheat=cheats,
restrict_weapons_by_date=restrict_weapons_by_date,
enable_squadron_aircraft_limits=use_new_squadron_rules,
supercarrier=params.supercarrier,
automate_runway_repair=params.auto_procurement,
automate_front_line_reinforcements=params.auto_procurement,
automate_aircraft_reinforcements=params.auto_procurement,
enable_frontline_cheats=params.cheats,
enable_base_capture_cheat=params.cheats,
restrict_weapons_by_date=params.restrict_weapons_by_date,
enable_squadron_aircraft_limits=params.use_new_squadron_rules,
),
GeneratorSettings(
start_date=start_date,
start_date=params.start_date,
start_time=campaign.recommended_start_time,
player_budget=DEFAULT_BUDGET,
enemy_budget=DEFAULT_BUDGET,
inverted=inverted,
inverted=params.inverted,
advanced_iads=theater.iads_network.advanced_iads,
no_carrier=False,
no_lha=False,
@@ -334,7 +354,10 @@ def create_game(
lua_plugin_manager,
)
game = generator.generate()
game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
if params.show_air_wing_config:
if AirWingConfigurationDialog(game, None).exec() == QDialog.DialogCode.Rejected:
sys.exit("Aborted air wing configuration")
game.begin_turn_0(squadrons_start_full=params.use_new_squadron_rules)
return game
@@ -405,8 +428,6 @@ def main():
"Installation path contains non-ASCII characters. This is known to cause problems."
)
game: Optional[Game] = None
args = parse_args()
# TODO: Flesh out data and then make unconditional.
@@ -415,21 +436,6 @@ def main():
load_mods()
if args.subcommand == "new-game":
with logged_duration("New game creation"):
game = create_game(
args.campaign,
args.blue,
args.red,
args.supercarrier,
args.auto_procurement,
args.inverted,
args.cheats,
args.date,
args.restrict_weapons_by_date,
args.advanced_iads,
args.use_new_squadron_rules,
)
if args.subcommand == "lint-weapons":
lint_weapon_data_for_aircraft(AircraftType.named(args.aircraft))
return
@@ -438,7 +444,10 @@ def main():
return
with Server().run_in_thread():
run_ui(game, UiFlags(args.dev, args.show_sim_speed_controls))
run_ui(
CreateGameParams.from_args(args),
UiFlags(args.dev, args.show_sim_speed_controls),
)
if __name__ == "__main__":

View File

@@ -8,15 +8,12 @@ from .liberation_theme import get_theme_icons
LABELS_OPTIONS = ["Full", "Abbreviated", "Dot Only", "Neutral Dot", "Off"]
SKILL_OPTIONS = ["Average", "Good", "High", "Excellent"]
AIRCRAFT_BANNERS: Dict[str, QPixmap] = {}
AIRCRAFT_ICONS: Dict[str, QPixmap] = {}
VEHICLE_BANNERS: Dict[str, QPixmap] = {}
VEHICLES_ICONS: Dict[str, QPixmap] = {}
ICONS: Dict[str, QPixmap] = {}
def load_icons():
ICONS["New"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/new.png")
ICONS["Open"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/open.png")
ICONS["Save"] = QPixmap("./resources/ui/misc/" + get_theme_icons() + "/save.png")
@@ -205,6 +202,7 @@ def load_aircraft_icons():
AIRCRAFT_ICONS[f1] = AIRCRAFT_ICONS["Mirage-F1C-200"]
AIRCRAFT_ICONS["Mirage-F1M-CE"] = AIRCRAFT_ICONS["Mirage-F1CE"]
AIRCRAFT_ICONS["MB-339A"] = AIRCRAFT_ICONS["MB-339A PAN"]
AIRCRAFT_ICONS["F-15ESE"] = AIRCRAFT_ICONS["F-15E"]
def load_vehicle_icons():
@@ -213,25 +211,3 @@ def load_vehicle_icons():
VEHICLES_ICONS[vehicle[:-7]] = QPixmap(
os.path.join("./resources/ui/units/vehicles/icons/", vehicle)
)
def load_aircraft_banners():
for aircraft in os.listdir("./resources/ui/units/aircrafts/banners/"):
if aircraft.endswith(".jpg"):
AIRCRAFT_BANNERS[aircraft[:-7]] = QPixmap(
os.path.join("./resources/ui/units/aircrafts/banners/", aircraft)
)
variants = ["Mirage-F1CT", "Mirage-F1EE", "Mirage-F1M-EE", "Mirage-F1EQ"]
for f1 in variants:
AIRCRAFT_BANNERS[f1] = AIRCRAFT_BANNERS["Mirage-F1C-200"]
variants = ["Mirage-F1CE", "Mirage-F1M-CE"]
for f1 in variants:
AIRCRAFT_BANNERS[f1] = AIRCRAFT_BANNERS["Mirage-F1C"]
def load_vehicle_banners():
for aircraft in os.listdir("./resources/ui/units/vehicles/banners/"):
if aircraft.endswith(".jpg"):
VEHICLE_BANNERS[aircraft[:-7]] = QPixmap(
os.path.join("./resources/ui/units/vehicles/banners/", aircraft)
)

View File

@@ -1,5 +1,5 @@
from datetime import datetime
from typing import List, Optional
from typing import List, Optional, Callable
from PySide6.QtWidgets import (
QDialog,
@@ -25,19 +25,22 @@ from qt_ui.widgets.QFactionsInfos import QFactionsInfos
from qt_ui.widgets.QIntelBox import QIntelBox
from qt_ui.widgets.clientslots import MaxPlayerCount
from qt_ui.widgets.simspeedcontrols import SimSpeedControls
from qt_ui.windows.AirWingDialog import AirWingDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
from qt_ui.windows.QWaitingForMissionResultWindow import QWaitingForMissionResultWindow
class QTopPanel(QFrame):
def __init__(
self, game_model: GameModel, sim_controller: SimController, ui_flags: UiFlags
self,
game_model: GameModel,
sim_controller: SimController,
ui_flags: UiFlags,
reset_to_pre_sim_checkpoint: Callable[[], None],
) -> None:
super(QTopPanel, self).__init__()
self.game_model = game_model
self.sim_controller = sim_controller
self.reset_to_pre_sim_checkpoint = reset_to_pre_sim_checkpoint
self.dialog: Optional[QDialog] = None
self.setMaximumHeight(70)
@@ -64,24 +67,8 @@ class QTopPanel(QFrame):
self.factionsInfos = QFactionsInfos(self.game)
self.air_wing = QPushButton("Air Wing")
self.air_wing.setDisabled(True)
self.air_wing.setProperty("style", "btn-primary")
self.air_wing.clicked.connect(self.open_air_wing)
self.transfers = QPushButton("Transfers")
self.transfers.setDisabled(True)
self.transfers.setProperty("style", "btn-primary")
self.transfers.clicked.connect(self.open_transfers)
self.intel_box = QIntelBox(self.game)
self.buttonBox = QGroupBox("Misc")
self.buttonBoxLayout = QHBoxLayout()
self.buttonBoxLayout.addWidget(self.air_wing)
self.buttonBoxLayout.addWidget(self.transfers)
self.buttonBox.setLayout(self.buttonBoxLayout)
self.proceedBox = QGroupBox("Proceed")
self.proceedBoxLayout = QHBoxLayout()
if ui_flags.show_sim_speed_controls:
@@ -97,7 +84,6 @@ class QTopPanel(QFrame):
self.layout.addWidget(self.conditionsWidget)
self.layout.addWidget(self.budgetBox)
self.layout.addWidget(self.intel_box)
self.layout.addWidget(self.buttonBox)
self.layout.addStretch(1)
self.layout.addWidget(self.proceedBox)
@@ -116,9 +102,6 @@ class QTopPanel(QFrame):
if game is None:
return
self.air_wing.setEnabled(True)
self.transfers.setEnabled(True)
self.conditionsWidget.setCurrentTurn(game.turn, game.conditions)
if game.conditions.weather.clouds:
@@ -142,14 +125,6 @@ class QTopPanel(QFrame):
else:
self.proceedButton.setEnabled(True)
def open_air_wing(self):
self.dialog = AirWingDialog(self.game_model, self.window())
self.dialog.show()
def open_transfers(self):
self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show()
def passTurn(self):
with logged_duration("Skipping turn"):
self.game.pass_turn(no_action=True)
@@ -293,7 +268,9 @@ class QTopPanel(QFrame):
persistence.mission_path_for("liberation_nextturn.miz")
)
waiting = QWaitingForMissionResultWindow(self.game, self.sim_controller, self)
waiting = QWaitingForMissionResultWindow(
self.game, self.sim_controller, self.reset_to_pre_sim_checkpoint, self
)
waiting.exec_()
def budget_update(self, game: Game):

View File

@@ -24,7 +24,8 @@ class LiverySelector(QComboBox):
for idx, livery in enumerate(
squadron.aircraft.dcs_unit_type.iter_liveries_for_country(
dcs.countries.get_by_name(squadron.country)
)
),
1, # First entry is "Default".
):
self.addItem(livery.name, livery)
if squadron.livery == livery.id:

View File

@@ -1,3 +1,5 @@
import textwrap
from collections import defaultdict
from typing import Iterable, Iterator, Optional
from PySide6.QtCore import (
@@ -150,6 +152,49 @@ class SquadronSizeSpinner(QSpinBox):
# return size
class AirWingConfigParkingTracker(QWidget):
allocation_changed = Signal()
def __init__(self, game: Game) -> None:
super().__init__()
self.theater = game.theater
self.by_cp: dict[ControlPoint, set[Squadron]] = defaultdict(set)
for coalition in game.coalitions:
for squadron in coalition.air_wing.iter_squadrons():
self.add_squadron(squadron)
def add_squadron(self, squadron: Squadron) -> None:
self.by_cp[squadron.location].add(squadron)
self.signal_change()
def remove_squadron(self, squadron: Squadron) -> None:
self.by_cp[squadron.location].remove(squadron)
self.signal_change()
def relocate_squadron(
self,
squadron: Squadron,
prior_location: ControlPoint,
new_location: ControlPoint,
) -> None:
self.by_cp[prior_location].remove(squadron)
self.by_cp[new_location].add(squadron)
squadron.relocate_to(new_location)
self.signal_change()
def used_parking_at(self, control_point: ControlPoint) -> int:
return sum(s.max_size for s in self.by_cp[control_point])
def iter_overfull(self) -> Iterator[tuple[ControlPoint, int, list[Squadron]]]:
for control_point in self.theater.controlpoints:
used = self.used_parking_at(control_point)
if used > control_point.total_aircraft_parking:
yield control_point, used, list(self.by_cp[control_point])
def signal_change(self) -> None:
self.allocation_changed.emit()
class SquadronConfigurationBox(QGroupBox):
remove_squadron_signal = Signal(Squadron)
@@ -158,11 +203,13 @@ class SquadronConfigurationBox(QGroupBox):
game: Game,
coalition: Coalition,
squadron: Squadron,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.squadron = squadron
self.parking_tracker = parking_tracker
columns = QHBoxLayout()
self.setLayout(columns)
@@ -200,6 +247,7 @@ class SquadronConfigurationBox(QGroupBox):
left_column.addLayout(size_column)
size_column.addWidget(QLabel("Max size:"))
self.max_size_selector = SquadronSizeSpinner(self.squadron.max_size, self)
self.max_size_selector.valueChanged.connect(self.update_max_size)
size_column.addWidget(self.max_size_selector)
task_column = QVBoxLayout()
@@ -214,8 +262,14 @@ class SquadronConfigurationBox(QGroupBox):
squadron.location,
squadron.aircraft,
)
self.base_selector.currentIndexChanged.connect(self.relocate_squadron)
left_column.addWidget(self.base_selector)
self.parking_label = QLabel()
self.update_parking_label()
self.parking_tracker.allocation_changed.connect(self.update_parking_label)
left_column.addWidget(self.parking_label)
if not squadron.player and squadron.aircraft.flyable:
player_label = QLabel("Player slots not available for opfor")
elif not squadron.aircraft.flyable:
@@ -266,9 +320,26 @@ class SquadronConfigurationBox(QGroupBox):
self.player_list.setText(
"<br />".join(p.name for p in self.claim_players_from_squadron())
)
self.update_parking_label()
finally:
self.blockSignals(old_state)
def update_parking_label(self) -> None:
self.parking_label.setText(
f"{self.parking_tracker.used_parking_at(self.squadron.location)}/"
f"{self.squadron.location.total_aircraft_parking}"
)
def update_max_size(self) -> None:
self.squadron.max_size = self.max_size_selector.value()
self.parking_tracker.signal_change()
def relocate_squadron(self) -> None:
location = self.base_selector.currentData()
self.parking_tracker.relocate_squadron(
self.squadron, self.squadron.location, location
)
def remove_from_squadron_config(self) -> None:
self.remove_squadron_signal.emit(self.squadron)
@@ -321,6 +392,7 @@ class SquadronConfigurationBox(QGroupBox):
self.squadron = new_squadron
self.bind_data()
self.mission_types.replace_squadron(self.squadron)
self.parking_tracker.signal_change()
def reset_title(self) -> None:
self.setTitle(f"{self.name_edit.text()} - {self.squadron.aircraft}")
@@ -361,11 +433,13 @@ class SquadronConfigurationLayout(QVBoxLayout):
game: Game,
coalition: Coalition,
squadrons: list[Squadron],
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.squadron_configs = []
self.parking_tracker = parking_tracker
for squadron in squadrons:
self.add_squadron(squadron)
@@ -376,6 +450,7 @@ class SquadronConfigurationLayout(QVBoxLayout):
return keep_squadrons
def remove_squadron(self, squadron: Squadron) -> None:
self.parking_tracker.remove_squadron(squadron)
for squadron_config in self.squadron_configs:
if squadron_config.squadron == squadron:
squadron_config.deleteLater()
@@ -386,23 +461,32 @@ class SquadronConfigurationLayout(QVBoxLayout):
return
def add_squadron(self, squadron: Squadron) -> None:
squadron_config = SquadronConfigurationBox(self.game, self.coalition, squadron)
squadron_config = SquadronConfigurationBox(
self.game, self.coalition, squadron, self.parking_tracker
)
squadron_config.remove_squadron_signal.connect(self.remove_squadron)
self.squadron_configs.append(squadron_config)
self.addWidget(squadron_config)
self.parking_tracker.add_squadron(squadron)
class AircraftSquadronsPage(QWidget):
remove_squadron_page = Signal(AircraftType)
def __init__(
self, game: Game, coalition: Coalition, squadrons: list[Squadron]
self,
game: Game,
coalition: Coalition,
squadrons: list[Squadron],
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
layout = QVBoxLayout()
self.setLayout(layout)
self.squadrons_config = SquadronConfigurationLayout(game, coalition, squadrons)
self.squadrons_config = SquadronConfigurationLayout(
game, coalition, squadrons, parking_tracker
)
self.squadrons_config.config_changed.connect(self.on_squadron_config_changed)
scrolling_widget = QWidget()
@@ -430,10 +514,16 @@ class AircraftSquadronsPage(QWidget):
class AircraftSquadronsPanel(QStackedLayout):
page_removed = Signal(AircraftType)
def __init__(self, game: Game, coalition: Coalition) -> None:
def __init__(
self,
game: Game,
coalition: Coalition,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
self.game = game
self.coalition = coalition
self.parking_tracker = parking_tracker
self.squadrons_pages: dict[AircraftType, AircraftSquadronsPage] = {}
for aircraft, squadrons in self.air_wing.squadrons.items():
self.new_page_for_type(aircraft, squadrons)
@@ -453,7 +543,9 @@ class AircraftSquadronsPanel(QStackedLayout):
def new_page_for_type(
self, aircraft_type: AircraftType, squadrons: list[Squadron]
) -> None:
page = AircraftSquadronsPage(self.game, self.coalition, squadrons)
page = AircraftSquadronsPage(
self.game, self.coalition, squadrons, self.parking_tracker
)
page.remove_squadron_page.connect(self.remove_page_for_type)
self.addWidget(page)
self.squadrons_pages[aircraft_type] = page
@@ -539,14 +631,77 @@ class AircraftTypeList(QListView):
self.update(self.selectionModel().currentIndex())
def describe_overfull_airbases(
overfull: Iterable[tuple[ControlPoint, int, list[Squadron]]]
) -> str:
string_builder = []
for (
control_point,
used_parking,
squadrons,
) in overfull:
capacity = control_point.total_aircraft_parking
base_description = f"{control_point.name} {used_parking}/{capacity}"
string_builder.append(f"<p><strong>{base_description}</strong></p>")
squadron_descriptions = []
for squadron in squadrons:
squadron_details = (
f"{squadron.aircraft} {squadron.name} {squadron.max_size} aircraft"
)
squadron_descriptions.append(f"<li>{squadron_details}</li>")
string_builder.append(f"<ul>{''.join(squadron_descriptions)}</ul>")
if not string_builder:
string_builder.append("All airbases are within parking limits.")
return "".join(string_builder)
class OverfullAirbasesDisplay(QGroupBox):
def __init__(
self,
parking_tracker: AirWingConfigParkingTracker,
parent: QWidget | None = None,
) -> None:
super().__init__("Overfull airbases", parent)
self.setMaximumHeight(200)
self.parking_tracker = parking_tracker
self.parking_tracker.allocation_changed.connect(self.on_allocation_changed)
layout = QVBoxLayout()
self.setLayout(layout)
self.label = QLabel()
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(self.label)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
layout.addWidget(scroll)
self.on_allocation_changed()
def on_allocation_changed(self) -> None:
self.label.setText(
describe_overfull_airbases(self.parking_tracker.iter_overfull())
)
class AirWingConfigurationTab(QWidget):
def __init__(self, coalition: Coalition, game: Game) -> None:
def __init__(
self,
coalition: Coalition,
game: Game,
parking_tracker: AirWingConfigParkingTracker,
) -> None:
super().__init__()
layout = QGridLayout()
self.setLayout(layout)
self.game = game
self.coalition = coalition
self.parking_tracker = parking_tracker
self.type_list = AircraftTypeList(coalition.air_wing)
@@ -556,7 +711,9 @@ class AirWingConfigurationTab(QWidget):
add_button.clicked.connect(lambda state: self.add_squadron())
layout.addWidget(add_button, 2, 1, 1, 1)
self.squadrons_panel = AircraftSquadronsPanel(game, coalition)
self.squadrons_panel = AircraftSquadronsPanel(
game, coalition, self.parking_tracker
)
self.squadrons_panel.page_removed.connect(self.type_list.remove_aircraft_type)
layout.addLayout(self.squadrons_panel, 1, 3, 2, 1)
@@ -630,6 +787,9 @@ class AirWingConfigurationDialog(QDialog):
def __init__(self, game: Game, parent) -> None:
super().__init__(parent)
self.game = game
self.parking_tracker = AirWingConfigParkingTracker(game)
self.setMinimumSize(1024, 768)
self.setWindowTitle(f"Air Wing Configuration")
# TODO: self.setWindowIcon()
@@ -651,11 +811,18 @@ class AirWingConfigurationDialog(QDialog):
self.tabs = []
for coalition in game.coalitions:
coalition_tab = AirWingConfigurationTab(coalition, game)
coalition_tab = AirWingConfigurationTab(
coalition, game, self.parking_tracker
)
name = "Blue" if coalition.player else "Red"
self.tab_widget.addTab(coalition_tab, name)
self.tabs.append(coalition_tab)
self.overfull_airbases_display = OverfullAirbasesDisplay(
self.parking_tracker, self
)
layout.addWidget(self.overfull_airbases_display)
buttons_layout = QHBoxLayout()
apply_button = QPushButton("Accept Changes && Start Campaign")
apply_button.setProperty("style", "btn-accept")
@@ -671,7 +838,29 @@ class AirWingConfigurationDialog(QDialog):
for tab in self.tabs:
tab.revert()
def can_continue(self) -> bool:
if not self.game.settings.enable_squadron_aircraft_limits:
return True
overfull = list(self.parking_tracker.iter_overfull())
if not overfull:
return True
description = (
"<p>The following airbases are over capacity:</p>"
f"{describe_overfull_airbases(overfull)}"
)
QMessageBox().critical(
self,
"Cannot continue with overfull bases",
description,
QMessageBox.Ok,
)
return False
def accept(self) -> None:
if not self.can_continue():
return
for tab in self.tabs:
tab.apply()
super().accept()
@@ -679,8 +868,16 @@ class AirWingConfigurationDialog(QDialog):
def reject(self) -> None:
result = QMessageBox.information(
None,
"Discard changes?",
"Are you sure you want to discard your changes and start the campaign?",
"Abort new game?",
"<br />".join(
textwrap.wrap(
"Are you sure you want to cancel air wing configuration and "
"return to the new game wizard? If you instead want to revert your "
"air wing changes and continue, use the revert and accept buttons "
"below.",
width=55,
)
),
QMessageBox.Yes,
QMessageBox.No,
)

View File

@@ -16,6 +16,7 @@ class GameUpdateSignal(QObject):
debriefingReceived = Signal(Debriefing)
game_loaded = Signal(Game)
game_generated = Signal(Game)
def __init__(self):
super(GameUpdateSignal, self).__init__()

View File

@@ -24,6 +24,7 @@ from game.persistence import SaveManager
from game.server import EventStream, GameContext
from game.server.dependencies import QtCallbacks, QtContext
from game.theater import ControlPoint, MissionTarget, TheaterGroundObject
from game.turnstate import TurnState
from qt_ui import liberation_install
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
@@ -33,10 +34,13 @@ 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
from qt_ui.windows.AirWingDialog import AirWingDialog
from qt_ui.windows.BugReportDialog import BugReportDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.PendingTransfersDialog import PendingTransfersDialog
from qt_ui.windows.QDebriefingWindow import QDebriefingWindow
from qt_ui.windows.basemenu.QBaseMenu2 import QBaseMenu2
from qt_ui.windows.gameoverdialog import GameOverDialog
from qt_ui.windows.groundobject.QGroundObjectMenu import QGroundObjectMenu
from qt_ui.windows.infos.QInfoPanel import QInfoPanel
from qt_ui.windows.logs.QLogsWindow import QLogsWindow
@@ -133,7 +137,14 @@ class QLiberationWindow(QMainWindow):
vbox = QVBoxLayout()
vbox.setContentsMargins(0, 0, 0, 0)
vbox.addWidget(QTopPanel(self.game_model, self.sim_controller, ui_flags))
vbox.addWidget(
QTopPanel(
self.game_model,
self.sim_controller,
ui_flags,
self.reset_to_pre_sim_checkpoint,
)
)
vbox.addWidget(hbox)
central_widget = QWidget()
@@ -143,6 +154,7 @@ class QLiberationWindow(QMainWindow):
def connectSignals(self):
GameUpdateSignal.get_instance().gameupdated.connect(self.setGame)
GameUpdateSignal.get_instance().debriefingReceived.connect(self.onDebriefing)
GameUpdateSignal.get_instance().game_generated.connect(self.onGameGenerated)
def initActions(self):
self.newGameAction = QAction("&New Game", self)
@@ -214,6 +226,12 @@ class QLiberationWindow(QMainWindow):
self.openNotesAction.setIcon(CONST.ICONS["Notes"])
self.openNotesAction.triggered.connect(self.showNotesDialog)
self.openAirWingAction = QAction("Air Wing", self)
self.openAirWingAction.triggered.connect(self.showAirWingDialog)
self.openTransfersAction = QAction("Transfers", self)
self.openTransfersAction.triggered.connect(self.showTransfersDialog)
self.importTemplatesAction = QAction("Import Layouts", self)
self.importTemplatesAction.triggered.connect(self.import_templates)
@@ -246,6 +264,8 @@ class QLiberationWindow(QMainWindow):
self.actions_bar.addAction(self.openSettingsAction)
self.actions_bar.addAction(self.openStatsAction)
self.actions_bar.addAction(self.openNotesAction)
self.actions_bar.addAction(self.openAirWingAction)
self.actions_bar.addAction(self.openTransfersAction)
def initMenuBar(self):
self.menu = self.menuBar()
@@ -315,7 +335,6 @@ class QLiberationWindow(QMainWindow):
def newGame(self):
wizard = NewGameWizard(self)
wizard.show()
wizard.accepted.connect(lambda: self.onGameGenerated(wizard.generatedGame))
def openFile(self):
if (
@@ -340,6 +359,23 @@ class QLiberationWindow(QMainWindow):
except Exception:
logging.exception("Error loading save game %s", file[0])
def reset_to_pre_sim_checkpoint(self) -> None:
"""Loads the game that was saved before pressing the take-off button.
A checkpoint will be saved when the player presses take-off to save their state
before the mission simulation begins. If the mission is aborted, we usually want
to reset to the pre-simulation state to allow players to effectively "rewind",
since they probably aborted so that they could make changes. Implementing rewind
for real is impractical, but checkpoints are easy.
"""
if self.game is None:
raise RuntimeError(
"Cannot reset to pre-sim checkpoint when no game is loaded"
)
GameUpdateSignal.get_instance().game_loaded.emit(
self.game.save_manager.load_pre_sim_checkpoint()
)
def saveGame(self):
logging.info("Saving game")
@@ -502,6 +538,14 @@ class QLiberationWindow(QMainWindow):
self.dialog = QNotesWindow(self.game)
self.dialog.show()
def showAirWingDialog(self) -> None:
self.dialog = AirWingDialog(self.game_model, self)
self.dialog.show()
def showTransfersDialog(self) -> None:
self.dialog = PendingTransfersDialog(self.game_model)
self.dialog.show()
def import_templates(self):
LAYOUTS.import_templates()
@@ -516,7 +560,14 @@ class QLiberationWindow(QMainWindow):
def onDebriefing(self, debrief: Debriefing):
logging.info("On Debriefing")
self.debriefing = QDebriefingWindow(debrief)
self.debriefing.show()
self.debriefing.exec()
state = self.game.check_win_loss()
if state is not TurnState.CONTINUE:
GameOverDialog(won=state is TurnState.WIN, parent=self).exec()
else:
self.game.pass_turn()
GameUpdateSignal.get_instance().updateGame(self.game)
def open_tgo_info_dialog(self, tgo: TheaterGroundObject) -> None:
QGroundObjectMenu(self, tgo, tgo.control_point, self.game).show()

View File

@@ -1,14 +1,48 @@
from __future__ import annotations
from pathlib import Path
from PySide6.QtCore import Qt
from PySide6.QtGui import QIcon
from PySide6.QtGui import QIcon, QPixmap
from PySide6.QtWidgets import QDialog, QFrame, QGridLayout, QLabel, QTextBrowser
from game.dcs.aircrafttype import AircraftType
from game.dcs.groundunittype import GroundUnitType
from game.dcs.unittype import UnitType
from game.game import Game
from qt_ui.uiconstants import AIRCRAFT_BANNERS, VEHICLE_BANNERS
AIRCRAFT_BANNERS_BASE = Path("resources/ui/units/aircrafts/banners")
VEHICLE_BANNERS_BASE = Path("resources/ui/units/vehicles/banners")
MISSING_BANNER_PATH = AIRCRAFT_BANNERS_BASE / "Missing.jpg"
def aircraft_banner_for(unit_type: AircraftType) -> Path:
if unit_type.dcs_id in {
"Mirage-F1CT",
"Mirage-F1EE",
"Mirage-F1M-EE",
"Mirage-F1EQ",
}:
name = "Mirage-F1C-200"
elif unit_type.dcs_id in {"Mirage-F1CE", "Mirage-F1M-CE"}:
name = "Mirage-F1C"
elif unit_type.dcs_id == "F-15ESE":
name = "F-15E"
else:
name = unit_type.dcs_id
return AIRCRAFT_BANNERS_BASE / f"{name}.jpg"
def vehicle_banner_for(unit_type: GroundUnitType) -> Path:
return VEHICLE_BANNERS_BASE / f"{unit_type.dcs_id}.jpg"
def banner_path_for(unit_type: UnitType) -> Path:
if isinstance(unit_type, AircraftType):
return aircraft_banner_for(unit_type)
if isinstance(unit_type, GroundUnitType):
return vehicle_banner_for(unit_type)
raise NotImplementedError(f"Unhandled UnitType subclass: {unit_type.__class__}")
class QUnitInfoWindow(QDialog):
@@ -29,14 +63,10 @@ class QUnitInfoWindow(QDialog):
header = QLabel(self)
header.setGeometry(0, 0, 720, 360)
pixmap = None
if isinstance(self.unit_type, AircraftType):
pixmap = AIRCRAFT_BANNERS.get(self.unit_type.dcs_id)
elif isinstance(self.unit_type, GroundUnitType):
pixmap = VEHICLE_BANNERS.get(self.unit_type.dcs_id)
if pixmap is None:
pixmap = AIRCRAFT_BANNERS.get("Missing")
banner_path = banner_path_for(unit_type)
if not banner_path.exists():
banner_path = MISSING_BANNER_PATH
pixmap = QPixmap(banner_path)
header.setPixmap(pixmap.scaled(header.width(), header.height()))
self.layout.addWidget(header, 0, 0)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import logging
from pathlib import Path
from typing import Optional
from typing import Optional, Callable
from PySide6 import QtCore
from PySide6.QtCore import QObject, Signal
@@ -52,12 +52,14 @@ class QWaitingForMissionResultWindow(QDialog):
self,
game: Game,
sim_controller: SimController,
reset_to_pre_sim_checkpoint: Callable[[], None],
parent: Optional[QWidget] = None,
) -> None:
super(QWaitingForMissionResultWindow, self).__init__(parent=parent)
self.setWindowModality(QtCore.Qt.WindowModal)
self.game = game
self.sim_controller = sim_controller
self.reset_to_pre_sim_checkpoint = reset_to_pre_sim_checkpoint
self.setWindowTitle("Waiting for mission completion.")
self.setWindowIcon(QIcon("./resources/icon.png"))
self.setMinimumHeight(570)
@@ -111,7 +113,7 @@ class QWaitingForMissionResultWindow(QDialog):
self.manually_submit.clicked.connect(self.submit_manually)
self.actions_layout.addWidget(self.manually_submit)
self.cancel = QPushButton("Abort mission")
self.cancel.clicked.connect(self.close)
self.cancel.clicked.connect(self.reject)
self.actions_layout.addWidget(self.cancel)
self.gridLayout.addWidget(self.actions, 2, 0)
@@ -122,7 +124,7 @@ class QWaitingForMissionResultWindow(QDialog):
self.manually_submit2.clicked.connect(self.submit_manually)
self.actions2_layout.addWidget(self.manually_submit2)
self.cancel2 = QPushButton("Abort mission")
self.cancel2.clicked.connect(self.close)
self.cancel2.clicked.connect(self.reject)
self.actions2_layout.addWidget(self.cancel2)
self.proceed = QPushButton("Accept results")
self.proceed.setProperty("style", "btn-success")
@@ -133,6 +135,11 @@ class QWaitingForMissionResultWindow(QDialog):
self.layout.addLayout(self.gridLayout, 1, 0)
self.setLayout(self.layout)
def reject(self) -> None:
if self.game.settings.reload_pre_sim_checkpoint_on_abort:
self.reset_to_pre_sim_checkpoint()
super().reject()
@staticmethod
def add_update_row(description: str, count: int, layout: QGridLayout) -> None:
row = layout.rowCount()
@@ -213,11 +220,8 @@ class QWaitingForMissionResultWindow(QDialog):
def process_debriefing(self):
with logged_duration("Turn processing"):
self.sim_controller.process_results(self.debriefing)
self.game.pass_turn()
GameUpdateSignal.get_instance().sendDebriefing(self.debriefing)
GameUpdateSignal.get_instance().updateGame(self.game)
self.close()
self.accept()
def closeEvent(self, evt):
super(QWaitingForMissionResultWindow, self).closeEvent(evt)

View File

@@ -9,19 +9,17 @@ from PySide6.QtWidgets import (
QVBoxLayout,
QWidget,
)
from dcs.ships import Stennis, KUZNECOW
from game import Game
from game.ato.flighttype import FlightType
from game.config import RUNWAY_REPAIR_COST
from game.server import EventStream
from game.sim import GameUpdateEvents
from game.theater import (
AMMO_DEPOT_FRONTLINE_UNIT_CONTRIBUTION,
ControlPoint,
ControlPointType,
FREE_FRONTLINE_UNIT_SUPPLY,
)
from qt_ui.cheatcontext import game_state_modifying_cheat_context
from qt_ui.dialogs import Dialog
from qt_ui.models import GameModel
from qt_ui.uiconstants import EVENT_ICONS
@@ -119,13 +117,11 @@ class QBaseMenu2(QDialog):
return self.game_model.game.settings.enable_base_capture_cheat
def cheat_capture(self) -> None:
events = GameUpdateEvents()
self.cp.capture(self.game_model.game, events, for_player=not self.cp.captured)
# Reinitialized ground planners and the like. The ATO needs to be reset because
# missions planned against the flipped base are no longer valid.
self.game_model.game.initialize_turn(events)
EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game_model.game)
with game_state_modifying_cheat_context(self.game_model.game) as events:
self.cp.capture(
self.game_model.game, events, for_player=not self.cp.captured
)
self.close()
@property
def has_transfer_destinations(self) -> bool:

View File

@@ -3,10 +3,8 @@ from collections.abc import Callable
from PySide6.QtWidgets import QGroupBox, QLabel, QPushButton, QVBoxLayout
from game import Game
from game.server import EventStream
from game.sim.gameupdateevents import GameUpdateEvents
from game.theater import ControlPoint
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.cheatcontext import game_state_modifying_cheat_context
from qt_ui.windows.basemenu.ground_forces.QGroundForcesStrategySelector import (
QGroundForcesStrategySelector,
)
@@ -52,15 +50,12 @@ class QGroundForcesStrategy(QGroupBox):
self.setLayout(layout)
def cheat_alter_front_line(self, enemy_point: ControlPoint, advance: bool) -> None:
amount = 0.2
if not advance:
amount *= -1
self.cp.base.affect_strength(amount)
enemy_point.base.affect_strength(-amount)
front_line = self.cp.front_line_with(enemy_point)
front_line.update_position()
events = GameUpdateEvents().update_front_line(front_line)
# Clear the ATO to replan missions affected by the front line.
self.game.initialize_turn(events)
EventStream.put_nowait(events)
GameUpdateSignal.get_instance().updateGame(self.game)
with game_state_modifying_cheat_context(self.game) as events:
amount = 0.2
if not advance:
amount *= -1
self.cp.base.affect_strength(amount)
enemy_point.base.affect_strength(-amount)
front_line = self.cp.front_line_with(enemy_point)
front_line.update_position()
events.update_front_line(front_line)

View File

@@ -0,0 +1,43 @@
from __future__ import annotations
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
QLabel,
QHBoxLayout,
QPushButton,
QWidget,
)
from qt_ui.windows.newgame.QNewGameWizard import NewGameWizard
class GameOverDialog(QDialog):
def __init__(self, won: bool, parent: QWidget | None = None) -> None:
super().__init__(parent)
self.setModal(True)
self.setWindowTitle("Game Over")
layout = QVBoxLayout()
self.setLayout(layout)
layout.addWidget(
QLabel(
f"<strong>You {'won' if won else 'lost'}!</strong><br />"
"<br />"
"Click below to start a new game."
)
)
button_row = QHBoxLayout()
layout.addLayout(button_row)
button_row.addStretch()
new_game = QPushButton("New Game")
new_game.clicked.connect(self.on_new_game)
button_row.addWidget(new_game)
def on_new_game(self) -> None:
wizard = NewGameWizard(self)
wizard.show()
wizard.accepted.connect(self.accept)

View File

@@ -29,6 +29,7 @@ class QEditFlightDialog(QDialog):
self.setWindowTitle("Edit flight")
self.setWindowIcon(EVENT_ICONS["strike"])
self.setModal(True)
layout = QVBoxLayout()

View File

@@ -4,6 +4,8 @@ from PySide6.QtWidgets import (
QFrame,
QLabel,
QVBoxLayout,
QScrollArea,
QWidget,
)
from game import Game
@@ -35,6 +37,16 @@ class QFlightPayloadTab(QFrame):
layout = QVBoxLayout()
scroll_content = QWidget()
scrolling_layout = QVBoxLayout()
scroll_content.setLayout(scrolling_layout)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setWidget(scroll_content)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
layout.addWidget(scroll)
# Docs Link
docsText = QLabel(
'<a href="https://github.com/dcs-liberation/dcs_liberation/wiki/Custom-Loadouts"><span style="color:#FFFFFF;">How to create your own default loadout</span></a>'
@@ -42,12 +54,12 @@ class QFlightPayloadTab(QFrame):
docsText.setAlignment(Qt.AlignCenter)
docsText.setOpenExternalLinks(True)
layout.addLayout(PropertyEditor(self.flight))
scrolling_layout.addLayout(PropertyEditor(self.flight))
self.loadout_selector = DcsLoadoutSelector(flight)
self.loadout_selector.currentIndexChanged.connect(self.on_new_loadout)
layout.addWidget(self.loadout_selector)
layout.addWidget(self.payload_editor)
layout.addWidget(docsText)
scrolling_layout.addWidget(self.loadout_selector)
scrolling_layout.addWidget(self.payload_editor)
scrolling_layout.addWidget(docsText)
self.setLayout(layout)

View File

@@ -1,6 +1,16 @@
import logging
from datetime import timedelta
from PySide6.QtWidgets import QGroupBox, QLabel, QMessageBox, QVBoxLayout
from PySide6.QtCore import QTime
from PySide6.QtWidgets import (
QGroupBox,
QLabel,
QMessageBox,
QVBoxLayout,
QTimeEdit,
QHBoxLayout,
QCheckBox,
)
from game import Game
from game.ato.flight import Flight
@@ -10,9 +20,9 @@ from qt_ui.widgets.QLabeledWidget import QLabeledWidget
from qt_ui.widgets.combos.QArrivalAirfieldSelector import QArrivalAirfieldSelector
class FlightAirfieldDisplay(QGroupBox):
class FlightPlanPropertiesGroup(QGroupBox):
def __init__(self, game: Game, package_model: PackageModel, flight: Flight) -> None:
super().__init__("Departure/Arrival")
super().__init__("Flight plan properties")
self.game = game
self.package_model = package_model
self.flight = flight
@@ -28,6 +38,31 @@ class FlightAirfieldDisplay(QGroupBox):
self.package_model.tot_changed.connect(self.update_departure_time)
self.update_departure_time()
tot_offset_layout = QHBoxLayout()
layout.addLayout(tot_offset_layout)
delay = int(self.flight.flight_plan.tot_offset.total_seconds())
negative = delay < 0
if negative:
delay = -delay
hours = delay // 3600
minutes = delay // 60 % 60
seconds = delay % 60
tot_offset_layout.addWidget(QLabel("TOT Offset (minutes:seconds)"))
tot_offset_layout.addStretch()
negative_offset_checkbox = QCheckBox("Ahead of package")
negative_offset_checkbox.setChecked(negative)
negative_offset_checkbox.toggled.connect(self.toggle_negative_offset)
tot_offset_layout.addWidget(negative_offset_checkbox)
self.tot_offset_spinner = QTimeEdit(QTime(hours, minutes, seconds))
self.tot_offset_spinner.setMaximumTime(QTime(59, 0))
self.tot_offset_spinner.setDisplayFormat("mm:ss")
self.tot_offset_spinner.timeChanged.connect(self.set_tot_offset)
self.tot_offset_spinner.setToolTip("Flight TOT offset from package TOT")
tot_offset_layout.addWidget(self.tot_offset_spinner)
layout.addWidget(
QLabel(
"Determined based on the package TOT. Edit the "
@@ -58,7 +93,7 @@ class FlightAirfieldDisplay(QGroupBox):
# is an invalid state for calling anything in TotEstimator.
return
self.departure_time.setText(
f"At {self.flight.flight_plan.startup_time():%H:%M%S}"
f"At {self.flight.flight_plan.startup_time():%H:%M:%S}"
)
def set_divert(self, index: int) -> None:
@@ -76,3 +111,13 @@ class FlightAirfieldDisplay(QGroupBox):
QMessageBox.critical(
self, "Could not update flight plan", str(ex), QMessageBox.Ok
)
def set_tot_offset(self, offset: QTime) -> None:
self.flight.flight_plan.tot_offset = timedelta(
hours=offset.hour(), minutes=offset.minute(), seconds=offset.second()
)
self.update_departure_time()
def toggle_negative_offset(self) -> None:
self.flight.flight_plan.tot_offset = -self.flight.flight_plan.tot_offset
self.update_departure_time()

View File

@@ -4,15 +4,15 @@ from PySide6.QtWidgets import QFrame, QGridLayout, QVBoxLayout
from game import Game
from game.ato.flight import Flight
from qt_ui.models import PackageModel
from qt_ui.windows.mission.flight.settings.FlightAirfieldDisplay import (
FlightAirfieldDisplay,
from qt_ui.windows.mission.flight.settings.FlightPlanPropertiesGroup import (
FlightPlanPropertiesGroup,
)
from qt_ui.windows.mission.flight.settings.QCustomName import QFlightCustomName
from qt_ui.windows.mission.flight.settings.QFlightSlotEditor import QFlightSlotEditor
from qt_ui.windows.mission.flight.settings.QFlightStartType import QFlightStartType
from qt_ui.windows.mission.flight.settings.QFlightTypeTaskInfo import (
QFlightTypeTaskInfo,
)
from qt_ui.windows.mission.flight.settings.QCustomName import QFlightCustomName
class QGeneralFlightSettingsTab(QFrame):
@@ -23,7 +23,7 @@ class QGeneralFlightSettingsTab(QFrame):
layout = QGridLayout()
layout.addWidget(QFlightTypeTaskInfo(flight), 0, 0)
layout.addWidget(FlightAirfieldDisplay(game, package_model, flight), 1, 0)
layout.addWidget(FlightPlanPropertiesGroup(game, package_model, flight), 1, 0)
layout.addWidget(QFlightSlotEditor(package_model, flight, game), 2, 0)
layout.addWidget(QFlightStartType(package_model, flight), 3, 0)
layout.addWidget(QFlightCustomName(flight), 4, 0)

View File

@@ -1,14 +1,35 @@
from PySide6.QtCore import QItemSelectionModel, QPoint
from PySide6.QtCore import QItemSelectionModel, QPoint, QModelIndex
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QHeaderView, QTableView
from PySide6.QtWidgets import (
QHeaderView,
QTableView,
QStyledItemDelegate,
QDoubleSpinBox,
QWidget,
QStyleOptionViewItem,
)
from game.ato.flight import Flight
from game.ato.flightwaypoint import FlightWaypoint
from game.ato.flightwaypointtype import FlightWaypointType
from game.ato.package import Package
from game.utils import Distance
from qt_ui.windows.mission.flight.waypoints.QFlightWaypointItem import QWaypointItem
HEADER_LABELS = ["Name", "Alt (ft)", "Alt Type", "TOT/DEPART"]
class AltitudeEditorDelegate(QStyledItemDelegate):
def createEditor(
self, parent: QWidget, option: QStyleOptionViewItem, index: QModelIndex
) -> QDoubleSpinBox:
editor = QDoubleSpinBox(parent)
editor.setMinimum(0)
editor.setMaximum(40000)
return editor
class QFlightWaypointList(QTableView):
def __init__(self, package: Package, flight: Flight):
super().__init__()
@@ -16,8 +37,9 @@ class QFlightWaypointList(QTableView):
self.flight = flight
self.model = QStandardItemModel(self)
self.model.itemChanged.connect(self.on_changed)
self.setModel(self.model)
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
self.model.setHorizontalHeaderLabels(HEADER_LABELS)
header = self.horizontalHeader()
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
@@ -27,27 +49,52 @@ class QFlightWaypointList(QTableView):
self.indexAt(QPoint(1, 1)), QItemSelectionModel.Select
)
def update_list(self):
# We need to keep just the row and rebuild the index later because the
# QModelIndex will not be valid after the model is cleared.
current_index = self.currentIndex().row()
self.model.clear()
self.altitude_editor_delegate = AltitudeEditorDelegate(self)
self.setItemDelegateForColumn(1, self.altitude_editor_delegate)
self.model.setHorizontalHeaderLabels(["Name", "Alt", "TOT/DEPART"])
def update_list(self) -> None:
# ignore signals when updating list so on_changed does not fire
self.model.blockSignals(True)
try:
# We need to keep just the row and rebuild the index later because the
# QModelIndex will not be valid after the model is cleared.
current_index = self.currentIndex().row()
self.model.clear()
waypoints = self.flight.flight_plan.waypoints
for row, waypoint in enumerate(waypoints):
self.add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(
self.model.index(current_index, 0), QItemSelectionModel.Select
)
self.resizeColumnsToContents()
total_column_width = self.verticalHeader().width() + self.lineWidth()
for i in range(0, self.model.columnCount()):
total_column_width += self.columnWidth(i) + self.lineWidth()
self.setFixedWidth(total_column_width)
self.model.setHorizontalHeaderLabels(HEADER_LABELS)
def add_waypoint_row(
waypoints = self.flight.flight_plan.waypoints
# Why [1:]? Qt starts indexing at 1 rather than 0, whereas DCS numbers
# waypoints starting with 0, and for whatever reason Qt crashes whenever I
# set the vertical labels manually.
#
# Starting with the second waypoint is a bit of a hack, but it's also the
# historical behavior anyway. This view used to have waypoints starting at 1
# and just didn't show the departure waypoint because the departure waypoint
# wasn't actually part of the flight plan tracked by Liberation. That
# changed at some point, so now we need to skip it manually to preserve that
# behavior.
#
# It really ought to show the departure waypoint and start indexing at 0,
# but since this all pending a move to React anyway, it's not worth fighting
# the Qt crashes for now.
#
# https://github.com/dcs-liberation/dcs_liberation/issues/3037
for row, waypoint in enumerate(waypoints[1:]):
self._add_waypoint_row(row, self.flight, waypoint)
self.selectionModel().setCurrentIndex(
self.model.index(current_index, 0), QItemSelectionModel.Select
)
self.resizeColumnsToContents()
total_column_width = self.verticalHeader().width() + self.lineWidth()
for i in range(0, self.model.columnCount()):
total_column_width += self.columnWidth(i) + self.lineWidth()
self.setFixedWidth(total_column_width)
finally:
# stop ignoring signals
self.model.blockSignals(False)
def _add_waypoint_row(
self, row: int, flight: Flight, waypoint: FlightWaypoint
) -> None:
self.model.insertRow(self.model.rowCount())
@@ -55,15 +102,25 @@ class QFlightWaypointList(QTableView):
self.model.setItem(row, 0, QWaypointItem(waypoint, row))
altitude = int(waypoint.alt.feet)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
altitude_item = QStandardItem(f"{altitude} ft {altitude_type}")
altitude_item.setEditable(False)
altitude_item = QStandardItem(f"{altitude}")
altitude_item.setEditable(True)
self.model.setItem(row, 1, altitude_item)
altitude_type = "AGL" if waypoint.alt_type == "RADIO" else "MSL"
altitude_type_item = QStandardItem(f"{altitude_type}")
altitude_type_item.setEditable(False)
self.model.setItem(row, 2, altitude_type_item)
tot = self.tot_text(flight, waypoint)
tot_item = QStandardItem(tot)
tot_item.setEditable(False)
self.model.setItem(row, 2, tot_item)
self.model.setItem(row, 3, tot_item)
def on_changed(self) -> None:
for i in range(self.model.rowCount()):
altitude = self.model.item(i, 1).text()
altitude_feet = float(altitude)
self.flight.flight_plan.waypoints[i].alt = Distance.from_feet(altitude_feet)
def tot_text(self, flight: Flight, waypoint: FlightWaypoint) -> str:
if waypoint.waypoint_type == FlightWaypointType.TAKEOFF:

View File

@@ -1,6 +1,7 @@
from __future__ import unicode_literals
import logging
import textwrap
from datetime import datetime, timedelta
from typing import List
@@ -13,6 +14,7 @@ from PySide6.QtWidgets import (
QTextEdit,
QVBoxLayout,
QWidget,
QDialog,
)
from jinja2 import Environment, FileSystemLoader, select_autoescape
@@ -26,6 +28,7 @@ from game.theater.start_generator import GameGenerator, GeneratorSettings, ModSe
from qt_ui.widgets.QLiberationCalendar import QLiberationCalendar
from qt_ui.widgets.spinsliders import CurrencySpinner, FloatSpinSlider, TimeInputs
from qt_ui.windows.AirWingConfigurationDialog import AirWingConfigurationDialog
from qt_ui.windows.GameUpdateSignal import GameUpdateSignal
from qt_ui.windows.newgame.QCampaignList import QCampaignList
jinja_env = Environment(
@@ -39,7 +42,6 @@ jinja_env = Environment(
lstrip_blocks=True,
)
"""
Possible time periods for new games
@@ -86,9 +88,14 @@ TIME_PERIODS = {
}
def wrap_label_text(text: str, width: int = 100) -> str:
return "<br />".join(textwrap.wrap(text, width=width))
class NewGameWizard(QtWidgets.QWizard):
def __init__(self, parent=None):
super(NewGameWizard, self).__init__(parent)
self.setModal(True)
# The wizard should probably be refactored to edit this directly, but for now we
# just create a Settings object so that we can load the player's preserved
@@ -114,7 +121,9 @@ class NewGameWizard(QtWidgets.QWizard):
self.addPage(self.theater_page)
self.addPage(self.faction_selection_page)
self.addPage(GeneratorOptions(default_settings, mod_settings))
self.difficulty_page = DifficultyAndAutomationOptions(default_settings)
self.difficulty_page = DifficultyAndAutomationOptions(
default_settings, self.theater_page.campaignList.selected_campaign
)
self.plugins_page = PluginsPage(self.lua_plugin_manager)
# Update difficulty page on campaign select
@@ -132,7 +141,6 @@ class NewGameWizard(QtWidgets.QWizard):
self.setWizardStyle(QtWidgets.QWizard.ModernStyle)
self.setWindowTitle("New Game")
self.generatedGame = None
def accept(self):
logging.info("New Game Wizard accept")
@@ -221,11 +229,14 @@ class NewGameWizard(QtWidgets.QWizard):
mod_settings,
self.lua_plugin_manager,
)
self.generatedGame = generator.generate()
game = generator.generate()
AirWingConfigurationDialog(self.generatedGame, self).exec_()
if AirWingConfigurationDialog(game, self).exec() == QDialog.DialogCode.Rejected:
logging.info("Aborted air wing configuration")
return
self.generatedGame.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
game.begin_turn_0(squadrons_start_full=use_new_squadron_rules)
GameUpdateSignal.get_instance().game_generated.emit(game)
super(NewGameWizard, self).accept()
@@ -567,8 +578,39 @@ class BudgetInputs(QtWidgets.QGridLayout):
self.addWidget(self.starting_money, 1, 1)
class NewSquadronRulesWarning(QLabel):
def __init__(
self, campaign: Campaign | None, parent: QWidget | None = None
) -> None:
super().__init__(parent)
self.set_campaign(campaign)
def set_campaign(self, campaign: Campaign | None) -> None:
if campaign is None:
self.setText("No campaign selected")
return
if campaign.version >= (10, 9):
text = f"{campaign.name} is compatible with the new squadron rules."
elif campaign.version >= (10, 7):
text = (
f"{campaign.name} has been updated since the new squadron rules were "
"introduced, but support for those rules was still optional. You may "
"need to remove, resize, or relocate squadrons before beginning the "
"game."
)
else:
text = (
f"{campaign.name} has not been updated since the new squadron rules. "
"Were introduced. You may need to remove, resize, or relocate "
"squadrons before beginning the game."
)
self.setText(wrap_label_text(text))
class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
def __init__(self, default_settings: Settings, parent=None) -> None:
def __init__(
self, default_settings: Settings, current_campaign: Campaign | None, parent=None
) -> None:
super().__init__(parent)
self.setTitle("Difficulty and automation options")
@@ -609,10 +651,15 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
new_squadron_rules.setChecked(default_settings.enable_squadron_aircraft_limits)
self.registerField("use_new_squadron_rules", new_squadron_rules)
economy_layout.addWidget(new_squadron_rules)
self.new_squadron_rules_warning = NewSquadronRulesWarning(current_campaign)
economy_layout.addWidget(self.new_squadron_rules_warning)
economy_layout.addWidget(
QLabel(
"With new squadron rules enabled, squadrons will not be able to exceed a maximum number of aircraft "
"(configurable), and the campaign will begin with all squadrons at full strength."
wrap_label_text(
"With new squadron rules enabled, squadrons will not be able to "
"exceed a maximum number of aircraft (configurable), and the "
"campaign will begin with all squadrons at full strength."
)
)
)
@@ -650,6 +697,7 @@ class DifficultyAndAutomationOptions(QtWidgets.QWizardPage):
self.enemy_income.spinner.setValue(
int(campaign.recommended_enemy_income_multiplier * 10)
)
self.new_squadron_rules_warning.set_campaign(campaign)
class PluginOptionCheckbox(QCheckBox):

View File

@@ -32,8 +32,8 @@ platformdirs==2.6.2
pluggy==1.0.0
pre-commit==2.21.0
pydantic==1.10.7
git+https://github.com/pydcs/dcs@8fdeda106ba7e847a5d0a1ed358a1463636b513d#egg=pydcs
pyinstaller==5.7.0
git+https://github.com/pydcs/dcs@e006f0df6db933fa34b2d5cb04db41653537503e#egg=pydcs
pyinstaller==5.12.0
pyinstaller-hooks-contrib==2022.14
pyproj==3.4.1
PySide6==6.4.1

View File

@@ -9,7 +9,7 @@ description:
pushing south.</p>
miz: battle_of_abu_dhabi.miz
performance: 2
version: "10.2"
version: "10.9"
squadrons:
# Blue CPs:
# The default faction is Iran, but the F-14B is given higher precedence so
@@ -41,16 +41,20 @@ squadrons:
- F-16CM Fighting Falcon (Block 50)
- F-4E Phantom II
- primary: AEW&C
size: 2
aircraft:
- E-3A
- primary: Refueling
size: 2
aircraft:
- KC-135 Stratotanker
- primary: Transport
size: 4
aircraft:
- C-17A
- primary: Strike
secondary: air-to-ground
size: 4
aircraft:
- B-1B Lancer
- Su-24MK Fencer-D
@@ -72,6 +76,7 @@ squadrons:
- Su-25 Frogfoot
- primary: BAI
secondary: air-to-ground
size: 8
aircraft:
- F-16CM Fighting Falcon (Block 50)
- Su-24MK Fencer-D
@@ -102,6 +107,7 @@ squadrons:
- F/A-18C Hornet (Lot 20)
- F-14A Tomcat (Block 135-GR Late)
- primary: Refueling
size: 2
aircraft:
- S-3B Tanker
@@ -111,6 +117,7 @@ squadrons:
aircraft:
- AV-8B Harrier II Night Attack
- primary: CAS
size: 8
secondary: air-to-ground
aircraft:
- UH-1H Iroquois

View File

@@ -9,7 +9,7 @@ recommended_enemy_faction: Russia 2010
recommended_start_date: 2004-01-07
miz: black_sea.miz
performance: 2
version: "10.7"
version: "10.9"
squadrons:
# Anapa-Vityazevo
12:
@@ -148,6 +148,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- UH-1H Iroquois
size: 8
Red CV:
- primary: BARCAP
secondary: air-to-air
@@ -164,3 +165,4 @@ squadrons:
secondary: air-to-ground
- primary: CAS
secondary: air-to-ground
size: 8

View File

@@ -12,89 +12,61 @@ recommended_enemy_faction: Germany 1944
recommended_start_date: 1944-07-04
miz: caen_to_evreux.miz
performance: 1
version: "10.0"
version: "10.9"
squadrons:
# Evreux
26:
- primary: BARCAP
aircraft:
- Bf 109 K-4 Kurfürst
- primary: BARCAP
aircraft:
- Fw 190 A-8 Anton
- primary: BARCAP
- primary: Escort
aircraft:
- Fw 190 D-9 Dora
size: 12
- primary: Strike
secondary: air-to-ground
aircraft:
- Ju 88 A-4
- primary: AEW&C
- primary: Refueling
- primary: Transport
size: 12
# Conches
40:
- primary: BARCAP
secondary: any
aircraft:
- Bf 109 K-4 Kurfürst
- primary: BARCAP
aircraft:
- Fw 190 A-8 Anton
- primary: BARCAP
aircraft:
- Fw 190 D-9 Dora
- primary: SEAD
secondary: any
- primary: DEAD
secondary: any
size: 4
# Carpiquet
19:
- primary: BARCAP
- primary: CAS
secondary: any
aircraft:
- Thunderbolt Mk.II (Late)
- P-47D-40 Thunderbolt
size: 12
- primary: BARCAP
aircraft:
- Mustang Mk.IV (Late)
- P-51D-30-NA Mustang
- primary: BARCAP
secondary: any
aircraft:
- Spitfire LF Mk IX
- primary: BARCAP
aircraft:
- Spitfire LF Mk IX (Clipped Wings)
- primary: Strike
size: 12
- primary: BAI
secondary: air-to-ground
aircraft:
- MosquitoFBMkVI
- primary: SEAD
secondary: any
- primary: DEAD
secondary: any
size: 12
- primary: OCA/Runway
secondary: air-to-ground
aircraft:
- Boston Mk.III
- A-20G Havoc
size: 10
# Ford_AF
31:
- primary: BARCAP
aircraft:
- Thunderbolt Mk.II (Mid)
- P-47D-30 Thunderbolt (Late)
- primary: BARCAP
aircraft:
- Thunderbolt Mk.II (Early)
- P-47D-30 Thunderbolt (Early)
- primary: BARCAP
- primary: Escort
secondary: air-to-air
aircraft:
- Mustang Mk.IV (Early)
- P-51D-25-NA Mustang
- primary: Strike
secondary: air-to-ground
aircraft:
- Boston Mk.III
- A-20G Havoc
size: 10
- primary: Strike
secondary: air-to-ground
aircraft:
- Fortress Mk.III
- B-17G Flying Fortress
- primary: AEW&C
- primary: Refueling
- primary: Transport
size: 10

Binary file not shown.

View File

@@ -0,0 +1,133 @@
---
name: Sinai - Exercise Bright Star
theater: Sinai
authors: Starfire
recommended_player_faction: Bluefor Modern
recommended_enemy_faction: Egypt 2000s
description: <p>For over 4 decades, the United States and Egypt have run a series of biannual joint military exercises called Bright Star. Over the years, the number of participating countries has grown substantially. Exercise Bright Star 2025 boasts 8 participant nations and 14 observer nations. The United States and a portion of the exercise coalition will play the part of a fictional hostile nation dubbed Orangeland, staging a mock invasion against Cairo. Israel, having for the first time accepted the invitation to observe, is hosting the aggressor faction of the exercise coalition at its airfields.</p>
miz: exercise_bright_star.miz
performance: 1
recommended_start_date: 2025-09-01
version: "10.9"
squadrons:
# Hatzerim (141)
7:
- primary: SEAD
secondary: any
aircraft:
- F/A-18C Hornet (Lot 20)
size: 24
- primary: TARCAP
secondary: any
aircraft:
- F-15E Strike Eagle (Suite 4+)
size: 20
- primary: Strike
secondary: air-to-ground
aircraft:
- F-15E Strike Eagle
size: 12
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: BAI
secondary: any
aircraft:
- JF-17 Thunder
size: 16
- primary: BARCAP
secondary: any
aircraft:
- Mirage 2000C
size: 12
# Kedem
12:
- primary: Transport
aircraft:
- CH-47D
size: 20
- primary: Air Assault
secondary: air-to-ground
aircraft:
- UH-1H Iroquois
size: 4
# Nevatim (106)
8:
- primary: AEW&C
aircraft:
- E-3A
size: 2
- primary: Refueling
aircraft:
- KC-135 Stratotanker
size: 1
- primary: Refueling
aircraft:
- KC-135 Stratotanker MPRS
size: 1
- primary: CAS
secondary: air-to-ground
aircraft:
- A-10C Thunderbolt II (Suite 7)
size: 8
# Melez (30)
5:
- primary: CAS
secondary: air-to-ground
aircraft:
- Ka-50 Hokum (Blackshark 3)
size: 4
- primary: TARCAP
secondary: air-to-air
aircraft:
- Mirage 2000C
size: 12
- primary: Strike
secondary: air-to-ground
aircraft:
- Mirage 2000C
size: 12
# Wadi al Jandali (72)
13:
- primary: AEW&C
aircraft:
- E-2C Hawkeye
size: 2
- primary: SEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: DEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 20
- primary: Air Assault
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
size: 4
- primary: OCA/Aircraft
secondary: air-to-ground
aircraft:
- SA 342L Gazelle
size: 4
# Cairo West (95)
18:
- primary: Transport
aircraft:
- C-130
size: 8
- primary: Escort
secondary: air-to-air
aircraft:
- MiG-29S Fulcrum-C
size: 20
- primary: BARCAP
secondary: any
aircraft:
- J-7B
size: 20

View File

@@ -8,14 +8,14 @@ description: <p>Welcome to Vegas Nerve, an asymmetrical Red Flag Exercise scenar
miz: exercise_vegas_nerve.miz
performance: 1
recommended_start_date: 2011-02-24
version: "10.7"
version: "10.9"
squadrons:
# Tonopah Airport
17:
- primary: BARCAP
secondary: air-to-air
- primary: TARCAP
secondary: any
aircraft:
- F-15C Eagle
- F-15E Strike Eagle (Suite 4+)
size: 12
- primary: Strike
secondary: air-to-ground

Binary file not shown.

View File

@@ -0,0 +1,175 @@
---
name: Normandy - The Final Countdown II
theater: Normandy
authors: Starfire
recommended_player_faction:
country: Combined Joint Task Forces Blue
name: D-Day Allied Forces 1944 and 1990
authors: Starfire
description:
<p>Faction for Final Countdown II</p>
locales:
- en_US
aircrafts:
- Boston Mk.III
- Fortress Mk.III
- Mustang Mk.IV (Late)
- Spitfire LF Mk IX
- Thunderbolt Mk.II (Late)
- MosquitoFBMkVI
- F-14B Tomcat
- F/A-18C Hornet (Lot 20)
- SH-60B Seahawk
awacs:
- E-2C Hawkeye
tankers:
- S-3B Tanker
frontline_units:
- A17 Light Tank Mk VII Tetrarch
- A22 Infantry Tank MK IV Churchill VII
- A27L Cruiser Tank MK VIII Centaur IV
- A27M Cruiser Tank MK VIII Cromwell IV
- Daimler Armoured Car Mk I
- M2A1 Half-Track
- QF 40 mm Mark III
- Sherman Firefly VC
- Sherman III
artillery_units:
- M12 Gun Motor Carriage
logistics_units:
- Truck Bedford
- Truck GMC "Jimmy" 6x6 Truck
infantry_units:
- Infantry M1 Garand
naval_units:
- DDG Arleigh Burke IIa
- CG Ticonderoga
- CVN-74 John C. Stennis
missiles: []
air_defense_units:
- Bofors 40 mm Gun
preset_groups:
- Ally Flak
requirements:
WW2 Asset Pack: https://www.digitalcombatsimulator.com/en/products/other/wwii_assets_pack/
carrier_names:
- CVN-71 Theodore Roosevelt
has_jtac: true
jtac_unit: MQ-9 Reaper
unrestricted_satnav: true
doctrine: ww2
building_set: ww2ally
recommended_enemy_faction: Germany 1944
description:
<p>While enroute to the Persian Gulf for Operation Desert Shield, the USS Theodore Roosevelt and its carrier strike group are engufled by an electrical vortex and transported through time and space to the English channel on the morning of the Normandy Landings - June 6th 1944. Seeking to reduce the cost in lives to the Allied Forces about to storm the beaches, the captain of the Roosevelt has elected to provide air support for the landings.</p><p><strong>Note:</strong> This campaign has a custom faction that combines modern US naval forces with WW2 Allied forces. To play it as intended, you should carefully ration your use of modern aircraft and not replenish them if shot down (as you cannot get new Tomcats and Hornets in 1944). You can also choose to play it as a purely WW2 campaign by switching to one of the WW2 Ally factions.</p>
miz: final_countdown_2.miz
performance: 2
recommended_start_date: 1944-06-06
version: "10.9"
squadrons:
#Blue CV (90)
Blue-CV:
- primary: TARCAP
secondary: any
aircraft:
- F-14B Tomcat
size: 24
- primary: DEAD
secondary: any
aircraft:
- F/A-18C Hornet (Lot 20)
size: 24
- primary: AEW&C
aircraft:
- E-2C Hawkeye
size: 2
- primary: Refueling
aircraft:
- S-3B Tanker
size: 2
- primary: Air Assault
secondary: any
aircraft:
- SH-60B Seahawk
size: 4
#Stoney Cross (39)
58:
- primary: OCA/Runway
secondary: air-to-ground
aircraft:
- A-20G Havoc
- Boston Mk.III
size: 20
#Needs Oar Point (55)
28:
- primary: BARCAP
secondary: any
aircraft:
- Spitfire LF Mk IX
size: 20
- primary: DEAD
secondary: air-to-ground
aircraft:
- MosquitoFBMkVI
size: 20
#RAF Grafton Underwood (1000)
From RAF Grafton Underwood:
- primary: Strike
secondary: air-to-ground
aircraft:
- B-17G Flying Fortress
- Fortress Mk.III
size: 20
#Lymington (56)
37:
- primary: Escort
secondary: any
aircraft:
- P-51D-30-NA Mustang
- Mustang Mk.IV (Late)
size: 20
- primary: BAI
secondary: any
aircraft:
- P-47D-40 Thunderbolt
- Thunderbolt Mk.II (Late)
size: 20
#Carpiquet (47)
19:
- primary: TARCAP
secondary: air-to-air
aircraft:
- Fw 190 D-9 Dora
size: 12
- primary: CAS
secondary: air-to-ground
aircraft:
- Ju 88 A-4
size: 8
#Broglie (32)
68:
- primary: Escort
secondary: any
aircraft:
- Bf 109 K-4 Kurfürst
size: 24
#Saint-Andre-de-lEure (30)
70:
- primary: BAI
secondary: air-to-ground
aircraft:
- Ju 88 A-4
size: 12
- primary: Strike
secondary: air-to-ground
aircraft:
- Ju 88 A-4
size: 12
#Vilacoublay (76)
42:
- primary: BARCAP
secondary: any
aircraft:
- Fw 190 A-8 Anton
size: 20

View File

@@ -7,7 +7,7 @@ recommended_enemy_faction: Syria 2011
description: <p>In this scenario, you start in Israel and the conflict is focused around the golan heights, an historically disputed territory.<br/><br/>This scenario is designed to be performance and helicopter friendly.</p>
miz: golan_heights_lite.miz
performance: 1
version: "10.5"
version: "10.9"
advanced_iads: true # Campaign has connection_nodes / power_sources / command_centers
iads_config:
- LHA-1 Tarawa # A Naval Group without connections but still participating as EWR
@@ -102,28 +102,31 @@ squadrons:
- primary: AEW&C
aircraft:
- E-3A
size: 1
- primary: Refueling
aircraft:
- KC-135 Stratotanker
- primary: Transport
aircraft:
- C-130
- primary: BARCAP
secondary: any
size: 1
- primary: Escort
secondary: air-to-air
aircraft:
- F-15C Eagle
size: 10
- primary: BARCAP
secondary: any
aircraft:
- F-4E Phantom II
size: 10
- primary: Strike
secondary: air-to-ground
aircraft:
- F-15E Strike Eagle
size: 10
- primary: SEAD
secondary: any
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 10
# Golan South
Golan South:
- primary: CAS
@@ -132,65 +135,67 @@ squadrons:
female_pilot_percentage: 15
aircraft:
- AH-1W SuperCobra
- primary: CAS
size: 4
- primary: BAI
secondary: air-to-ground
nickname: Defenders of Golan
female_pilot_percentage: 25
aircraft:
- AH-64D Apache Longbow
- primary: Transport
size: 6
- primary: Air Assault
secondary: any
aircraft:
- UH-1H Iroquois
size: 2
# Golan North
Golan North:
- primary: CAS
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
size: 4
- primary: CAS
aircraft:
- SA 342M Gazelle
- primary: Transport
secondary: air-to-ground
aircraft:
- SA 342M Gazelle
size: 4
- primary: Air Assault
secondary: any
aircraft:
- Mi-8MTV2 Hip
size: 2
# Marj Ruhayyil
23:
- primary: BARCAP
- primary: BAI
secondary: any
aircraft:
- MiG-21bis Fishbed-N
- primary: BARCAP
size: 12
- primary: Escort
secondary: any
aircraft:
- MiG-23MLD Flogger-K
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
- primary: Strike
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
- MiG-21bis Fishbed-N
size: 12
# Damascus
7:
- primary: BARCAP
secondary: any
aircraft:
- MiG-29S Fulcrum-C
- primary: BARCAP
secondary: any
aircraft:
- MiG-21bis Fishbed-N
- primary: BARCAP
- MiG-23MLD Flogger-K
size: 12
- primary: TARCAP
secondary: any
aircraft:
- MiG-25PD Foxbat-E
size: 12
- primary: SEAD
secondary: air-to-ground
aircraft:
- Su-24M Fencer-D
size: 12
- primary: Strike
secondary: air-to-ground
aircraft:
- Su-17M4 Fitter-K
size: 12

View File

@@ -16,7 +16,7 @@ description:
miz: grabthars_hammer.miz
performance: 2
recommended_start_date: 1999-12-25
version: "10.7"
version: "10.9"
squadrons:
#Mount Pleasant
2:
@@ -42,9 +42,9 @@ squadrons:
#San Julian
11:
- primary: BAI
secondary: air-to-ground
secondary: any
aircraft:
- F-15E Strike Eagle
- F-15E Strike Eagle (Suite 4+)
size: 8
- primary: Air Assault
secondary: any

View File

@@ -14,7 +14,7 @@ description:
fighting to the west, a USN battle group is dispatched from the east coast of
the US to clear the Chinese forces from the continent and crush their carrier
group.</p>
version: "10.4"
version: "10.9"
recommended_player_faction: US Navy 2005
recommended_enemy_faction: China 2010
miz: gran_polvorin.miz
@@ -38,16 +38,20 @@ squadrons:
- primary: AEW&C
aircraft:
- VAW-125
size: 2
- primary: Refueling
aircraft:
- VS-35 (Tanker)
size: 4
- primary: Anti-ship
secondary: air-to-ground
aircraft:
- VS-35
size: 8
- primary: Transport
aircraft:
- HSM-40
size: 2
# BLUFOR LHA
Naval-3:
- primary: BAI
@@ -58,6 +62,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- HMLA-169 (UH-1H)
size: 4
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -106,9 +111,6 @@ squadrons:
secondary: air-to-ground
aircraft:
- H-6J Badger
- primary: Refueling
aircraft:
- IL-78M
# Rio Gallegos
7:
- primary: BARCAP

View File

@@ -176,7 +176,7 @@ description:
northern border. With the arrival of a US carrier group, Israel prepares its
counterattack. The US Navy will handle the Beirut region's coastal arena,
while the IAF will push through Damascus and the inland mountain ranges.</p>
version: "10.6"
version: "10.9"
miz: operation_allied_sword.miz
performance: 2
recommended_start_date: 2004-07-17
@@ -198,16 +198,20 @@ squadrons:
- primary: AEW&C
aircraft:
- VAW-125
size: 2
- primary: Refueling
aircraft:
- VS-35 (Tanker)
size: 4
- primary: Anti-ship
secondary: air-to-ground
aircraft:
- VS-35
size: 8
- primary: Transport
aircraft:
- HSM-40
size: 2
#BLUFOR LHA
Naval-3:
- primary: BAI
@@ -218,6 +222,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- HMLA-269 (UH-1H)
size: 4
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -257,6 +262,7 @@ squadrons:
- primary: Refueling
aircraft:
- VMGR-352
size: 2
- primary: Strike
secondary: any
aircraft:
@@ -319,9 +325,6 @@ squadrons:
secondary: air-to-ground
aircraft:
- Su-24M Fencer-D
- primary: Refueling
aircraft:
- IL-78M
# Bassel Al-Assad
21:
- primary: TARCAP
@@ -335,12 +338,15 @@ squadrons:
- primary: Refueling
aircraft:
- IL-78M
size: 1
- primary: Transport
aircraft:
- IL-76MD
size: 1
- primary: AEW&C
aircraft:
- A-50
size: 1
- primary: Strike
secondary: air-to-ground
aircraft:
@@ -389,3 +395,4 @@ squadrons:
secondary: air-to-ground
aircraft:
- Mi-8MTV2 Hip
size: 2

View File

@@ -3,7 +3,7 @@ name: Syria - Operation Blackball
theater: Syria
authors: Fuzzle
description: <p>A lightweight fictional showcase of Cyprus for the Syria terrain. A US Navy force must deploy from a carrier group to push through the island. <strong>This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take the first FOB. Ensure you soften it up enough first!</strong></p><p><strong>Backstory:</strong> The world is at war. With the help of her eastern allies Russia has taken the Suez Canal and deployed a large naval force to the Mediterranean, trapping a US carrier group near the Turkish-Syrian border. Now they must break out by taking Cyprus back.</p>
version: "10.1"
version: "10.9"
recommended_player_faction: US Navy 2005
recommended_enemy_faction: Russia 2010
miz: operation_blackball.miz
@@ -27,16 +27,20 @@ squadrons:
- primary: AEW&C
aircraft:
- VAW-125
size: 2
- primary: Refueling
aircraft:
- VS-35 (Tanker)
size: 4
- primary: Anti-ship
secondary: air-to-ground
aircraft:
- VS-35
size: 8
- primary: Transport
aircraft:
- HSM-40
size: 2
# BLUFOR LHA
Naval-2:
- primary: BAI
@@ -47,6 +51,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- HMLA-269 (UH-1H)
size: 4
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -118,12 +123,15 @@ squadrons:
- primary: AEW&C
aircraft:
- A-50
size: 1
- primary: Refueling
aircraft:
- IL-78M
size: 1
- primary: Transport
aircraft:
- IL-78MD
size: 1
# OPFOR First FOB
FOB Gecitkale:
- primary: CAS
@@ -136,6 +144,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- Mi-8MTV2 Hip
size: 2
- primary: CAS
secondary: air-to-ground
aircraft:

View File

@@ -8,7 +8,7 @@ description: <p>This is a semi-fictional what-if scenario for Operation Peace Sp
miz: operation_peace_spring.miz
performance: 1
recommended_start_date: 2019-12-23
version: "10.7"
version: "10.9"
squadrons:
# Ramat David
30:
@@ -43,10 +43,10 @@ squadrons:
aircraft:
- AV-8B Harrier II Night Attack
size: 8
- primary: BARCAP
secondary: air-to-air
- primary: TARCAP
secondary: any
aircraft:
- F-15C Eagle
- F-15E Strike Eagle (Suite 4+)
size: 12
- primary: CAS
secondary: air-to-ground

View File

@@ -8,7 +8,7 @@ description: <p>United Nations Observer Mission in Georgia (UNOMIG) observers st
miz: operation_vectrons_claw.miz
performance: 1
recommended_start_date: 2008-08-08
version: "10.7"
version: "10.9"
squadrons:
Blue CV-1:
- primary: BARCAP
@@ -66,6 +66,11 @@ squadrons:
aircraft:
- F-16CM Fighting Falcon (Block 50)
size: 16
- primary: BAI
secondary: any
aircraft:
- F-15E Strike Eagle (Suite 4+)
size: 12
- primary: BAI
secondary: air-to-ground
aircraft:

View File

@@ -3,7 +3,7 @@ name: Marianas - Pacific Repartee
theater: MarianaIslands
authors: Fuzzle
description: <p>A naval campaign where a US carrier group must retake Guam, Saipan and the Marianas Islands from the Chinese. <strong>This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take FOBs/airbases. Ensure you soften them up enough first!</strong></p><p><strong>Backstory:</strong> After an escalation in the South China Sea, the PLAN has taken the US by surprise and invaded Guam, setting up supporting positions throughout the Marianas island chain. With the rest of the US Navy engaged near Japan, a carrier task group must push through China's forces, assist a small Marine contingent holding out on Farallon de Pajaros and liberate Guam.</p>
version: "10.4"
version: "10.9"
recommended_player_faction: US Navy 2005
recommended_enemy_faction: China 2010
miz: pacific_repartee.miz
@@ -27,16 +27,20 @@ squadrons:
- primary: AEW&C
aircraft:
- VAW-125
size: 2
- primary: Refueling
aircraft:
- VS-35 (Tanker)
size: 4
- primary: Anti-ship
secondary: air-to-ground
aircraft:
- VS-35
size: 8
- primary: Transport
aircraft:
- HSM-40
size: 2
# BLUFOR LHA
Naval-2:
- primary: BAI
@@ -47,6 +51,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- HMLA-169 (UH-1H)
size: 4
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -92,6 +97,7 @@ squadrons:
- primary: Transport
aircraft:
- IL-76MD
size: 2
- primary: BARCAP
secondary: any
aircraft:
@@ -105,6 +111,7 @@ squadrons:
- primary: Refueling
aircraft:
- IL-78M
size: 2
# Andersen AFB
6:
- primary: TARCAP
@@ -122,6 +129,7 @@ squadrons:
- primary: Transport
aircraft:
- IL-76MD
size: 2
# Antonio B. Won Pat Intl
4:
- primary: TARCAP

View File

@@ -3,7 +3,7 @@ name: Persian Gulf - Scenic Route 2 - Dust To Dust
theater: Persian Gulf
authors: Fuzzle
description: <p>A continuation of Scenic Route. A NATO coalition pushes inland along a protracted axis of advance. Built with helicopters/FOB-based gameplay in mind. <p><strong>Backstory:</strong> With Iran's coastal defences pacified and their forces pushed inland, a beleaguered US Navy is reinforced by a NATO coalition task force. The going will not be easy however; Iran has assembled the full might of its armoured and mechanized divisions alongside rotary support to defend their heartland. The conflict intensifies.</p>
version: "10.1"
version: "10.9"
advanced_iads: true
recommended_player_faction: NATO OIF
recommended_enemy_faction: Iran 2015
@@ -37,6 +37,7 @@ squadrons:
aircraft:
- 101st Combat Aviation Brigade
#US Army UH-60
size: 6
# Havadarya
9:
- primary: BARCAP
@@ -62,9 +63,11 @@ squadrons:
- primary: AEW&C
aircraft:
- VAW-125
size: 2
- primary: Refueling
aircraft:
- VS-35 (Tanker)
size: 4
# BLUFOR LHA
BLUFOR LHA:
- primary: BAI
@@ -75,6 +78,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- HMLA-169 (UH-1H)
size: 4
# BLUFOR Start FOB
FOB Anguran:
- primary: CAS
@@ -87,6 +91,7 @@ squadrons:
aircraft:
- Wolfpack, 1-82 ARB
#US Army Apache AH-64D
size: 2
# OPFOR L1F1
FOB Tang-e Dalan:
- primary: CAS
@@ -140,9 +145,11 @@ squadrons:
- primary: AEW&C
aircraft:
- A-50
size: 2
- primary: Refueling
aircraft:
- IL-78M
size: 2
- primary: BARCAP
secondary: any
aircraft:
@@ -159,6 +166,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- Mi-24P Hind-F
size: 4
- primary: SEAD
secondary: air-to-ground
aircraft:
@@ -228,12 +236,15 @@ squadrons:
- primary: AEW&C
aircraft:
- A-50
size: 2
- primary: Refueling
aircraft:
- IL-78M
size: 2
- primary: Transport
aircraft:
- IL-78MD
size: 2
- primary: BARCAP
secondary: any
aircraft:

View File

@@ -3,7 +3,7 @@ name: Persian Gulf - Scenic Route
theater: Persian Gulf
authors: Fuzzle
description: <p>A lightweight naval campaign involving a US Navy carrier group pushing across the coast of Iran. <strong>This is a purely naval campaign, meaning you will need to use the Air Assault mission type with transports to take the first FOB. Ensure you soften it up enough first!</strong></p><p><strong>Backstory:</strong> Iran has declared war on all US forces in the Gulf resulting in all local allies withdrawing their support for American troops. A lone carrier group must pacify the southern coast of Iran and hold out until backup can arrive lest the US and her interests be ejected from the region permanently.</p>
version: "10.4"
version: "10.9"
advanced_iads: true
recommended_player_faction: US Navy 2005
recommended_enemy_faction: Iran 2015
@@ -28,16 +28,20 @@ squadrons:
- primary: AEW&C
aircraft:
- VAW-125
size: 2
- primary: Refueling
aircraft:
- VS-35 (Tanker)
size: 4
- primary: Anti-ship
secondary: air-to-ground
aircraft:
- VS-35
size: 8
- primary: Transport
aircraft:
- HSM-40
size: 2
# BLUFOR LHA
Naval-2:
- primary: BAI
@@ -48,6 +52,7 @@ squadrons:
secondary: air-to-ground
aircraft:
- HMLA-169 (UH-1H)
size: 4
- primary: CAS
secondary: air-to-ground
aircraft:
@@ -72,12 +77,7 @@ squadrons:
- primary: AEW&C
aircraft:
- A-50
- primary: Refueling
aircraft:
- IL-78M
- primary: Transport
aircraft:
- IL-78MD
size: 1
- primary: BARCAP
secondary: any
aircraft:

View File

@@ -7,7 +7,7 @@ recommended_player_faction: USA 2005
recommended_enemy_faction: Iraq 1991
miz: tripoint_hostility.miz
performance: 2
version: "10.1"
version: "10.9"
recommended_start_date: 2006-08-03
recommended_player_money: 900
recommended_enemy_money: 1200
@@ -24,6 +24,7 @@ squadrons:
aircraft:
- 960th AAC Squadron
#USAF E-3A
size: 2
# King Hussein Air College, BLUFOR start
19:
- primary: BARCAP
@@ -46,6 +47,7 @@ squadrons:
aircraft:
- 101st Combat Aviation Brigade
#US Army UH-60
size: 4
# FOB Tha'lah, BLUFOR 1st FOB north
FOB Tha'lah:
- primary: CAS

View File

@@ -9,7 +9,7 @@ local unitPayloads = {
["num"] = 3,
},
[2] = {
["CLSID"] = "{M261_M282}",
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["num"] = 4,
},
[3] = {
@@ -17,7 +17,7 @@ local unitPayloads = {
["num"] = 2,
},
[4] = {
["CLSID"] = "{M261_M282}",
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["num"] = 1,
},
},
@@ -30,19 +30,19 @@ local unitPayloads = {
["name"] = "Liberation BAI",
["pylons"] = {
[1] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 3,
},
[2] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 4,
},
[3] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 2,
},
[4] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["CLSID"] = "{M299_4xAGM_114L}",
["num"] = 1,
},
},
@@ -54,21 +54,21 @@ local unitPayloads = {
["name"] = "Liberation OCA/Aircraft",
["pylons"] = {
[1] = {
["CLSID"] = "{M261_M229}",
["num"] = 4,
},
[2] = {
["CLSID"] = "{M261_M229}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["num"] = 3,
},
[4] = {
[2] = {
["CLSID"] = "{88D18A5E-99C8-4B04-B40B-1C02F2018B6E}",
["num"] = 2,
},
[3] = {
["CLSID"] = "{M261_M229}",
["num"] = 4,
},
[4] = {
["CLSID"] = "{M261_M229}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 31,

View File

@@ -0,0 +1,560 @@
local unitPayloads = {
["name"] = "F-15ESE",
["payloads"] = {
[1] = {
["displayName"] = "Liberation Strike",
["name"] = "Liberation Strike",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_MK84LD_x_2}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 8,
["settings"] = {
["GUI_fuze_type"] = 1,
["arm_delay_ctrl_FMU139CB_LD"] = 1,
["function_delay_ctrl_FMU139CB_LD"] = 0,
},
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_MK84LD_x_2}",
["num"] = 4,
},
[10] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[11] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 32,
},
},
[2] = {
["name"] = "Liberation BAI",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[2] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[3] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[4] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[5] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[6] = {
["CLSID"] = "{CFT_L_CBU_97_x_6}",
["num"] = 4,
},
[7] = {
["CLSID"] = "{CFT_R_CBU_97_x_6}",
["num"] = 12,
},
[8] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
},
["tasks"] = {
[1] = 32,
},
},
[3] = {
["name"] = "Liberation BARCAP",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[4] = {
["displayName"] = "Liberation Escort",
["name"] = "Liberation Escort",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[5] = {
["displayName"] = "Liberation OCA/Runway",
["name"] = "Liberation OCA/Runway",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_MK84LD_x_2}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 8,
["settings"] = {
["GUI_fuze_type"] = 1,
["arm_delay_ctrl_FMU139CB_LD"] = 1,
["function_delay_ctrl_FMU139CB_LD"] = 0,
},
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_MK84LD_x_2}",
["num"] = 4,
},
[10] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[11] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 32,
},
},
[6] = {
["displayName"] = "Liberation OCA/Aircraft",
["name"] = "Liberation OCA/Aircraft",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_MK82LD_x_6}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["num"] = 8,
["settings"] = {
["GUI_fuze_type"] = 1,
["arm_delay_ctrl_FMU139CB_LD"] = 1,
["function_delay_ctrl_FMU139CB_LD"] = 0,
},
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_MK82LD_x_6}",
["num"] = 4,
},
[10] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[11] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
},
["tasks"] = {
[1] = 32,
},
},
[7] = {
["displayName"] = "Liberation Fighter Sweep",
["name"] = "Liberation Fighter Sweep",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[8] = {
["displayName"] = "Liberation DEAD",
["name"] = "Liberation DEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[6] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[7] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[8] = {
["CLSID"] = "{CFT_L_CBU_97_x_6}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{CFT_R_CBU_97_x_6}",
["num"] = 12,
},
},
["tasks"] = {
[1] = 32,
},
},
[9] = {
["displayName"] = "Liberation TARCAP",
["name"] = "Liberation TARCAP",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 14,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 11,
},
[5] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 10,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 6,
},
[10] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 5,
},
[11] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[12] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 2,
},
[13] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
},
["tasks"] = {
[1] = 32,
},
},
[10] = {
["displayName"] = "Liberation CAS",
["name"] = "Liberation CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 15,
},
[2] = {
["CLSID"] = "{40EF17B7-F508-45de-8566-6FFECC0C1AB8}",
["num"] = 1,
},
[3] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 13,
},
[4] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["num"] = 3,
},
[5] = {
["CLSID"] = "{CFT_R_CBU_97_x_6}",
["num"] = 12,
},
[6] = {
["CLSID"] = "{F-15E_AAQ-13_LANTIRN}",
["num"] = 9,
},
[7] = {
["CLSID"] = "{F15E_EXTTANK}",
["num"] = 8,
},
[8] = {
["CLSID"] = "{F-15E_AAQ-14_LANTIRN}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{CFT_L_CBU_97_x_6}",
["num"] = 4,
},
},
["tasks"] = {
[1] = 32,
},
},
},
["tasks"] = {
},
["unitType"] = "F-15ESE",
}
return unitPayloads

View File

@@ -5,15 +5,15 @@ local unitPayloads = {
["name"] = "CAP",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
["CLSID"] = "{8D399DDA-FF81-4F14-904D-099B34FE7918}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 8,
},
[3] = {
["CLSID"] = "{8D399DDA-FF81-4F14-904D-099B34FE7918}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 3,
},
[4] = {
@@ -25,14 +25,21 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{6CEB49FC-DED8-4DED-B053-E1F033FF72D3}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
},
[8] = {
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
[9] = {
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
}, },
["tasks"] = {
[1] = 11,
},
@@ -41,7 +48,7 @@ local unitPayloads = {
["name"] = "CAS",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
@@ -61,19 +68,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},
@@ -85,15 +92,15 @@ local unitPayloads = {
["name"] = "STRIKE",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 8,
},
[3] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{AB8B8299-F1CC-4359-89B5-2172E0CF4A5A}",
["num"] = 3,
},
[4] = {
@@ -105,19 +112,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{BCE4E030-38E9-423E-98ED-24BE3DA87C32}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},
@@ -129,7 +136,7 @@ local unitPayloads = {
["name"] = "ANTISHIP",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
@@ -149,19 +156,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},
@@ -173,7 +180,7 @@ local unitPayloads = {
["name"] = "SEAD",
["pylons"] = {
[1] = {
["CLSID"] = "{6D21ECEA-F85B-4E8D-9D51-31DC9B8AA4EF}",
["CLSID"] = "ALQ_184",
["num"] = 6,
},
[2] = {
@@ -181,7 +188,7 @@ local unitPayloads = {
["num"] = 8,
},
[3] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{E6A6262A-CA08-4B3D-B030-E1A993B98452}",
["num"] = 3,
},
[4] = {
@@ -193,19 +200,19 @@ local unitPayloads = {
["num"] = 9,
},
[6] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 1,
},
[7] = {
["CLSID"] = "{AIS_ASQ_T50}",
["CLSID"] = "{C8E06185-7CD6-4C90-959F-044679E90751}",
["num"] = 10,
},
[8] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 7,
},
[9] = {
["CLSID"] = "{444BA8AE-82A7-4345-842E-76154EFCCA46}",
["CLSID"] = "{F376DBEE-4CAE-41BA-ADD9-B2910AC95DEC}",
["num"] = 4,
},
},

View File

@@ -78,7 +78,7 @@
},
"airfield32_3": {
"name": "Beslan",
"callsign": "",
"callsign": "ICH",
"beacon_type": 14,
"hertz": 110500000,
"channel": null

View File

@@ -167,6 +167,13 @@
"hertz": 1000000,
"channel": 31
},
"airfield20_0": {
"name": "BIO",
"callsign": "BIO",
"beacon_type": 9,
"hertz": 205000000,
"channel": null
},
"airfield11_0": {
"name": "San Julian",
"callsign": "",

View File

@@ -1,18 +1,193 @@
{
"airfield22_0": {
"name": "ABUDHABI",
"callsign": "ADV",
"beacon_type": 1,
"hertz": 114250000,
"world_0": {
"name": "Kish",
"callsign": "KIS",
"beacon_type": 3,
"hertz": 117400000,
"channel": 121
},
"world_1": {
"name": "DohaAirport",
"callsign": "DIA",
"beacon_type": 3,
"hertz": 112400000,
"channel": 71
},
"world_2": {
"name": "HamadInternationalAirport",
"callsign": "DOH",
"beacon_type": 3,
"hertz": 114400000,
"channel": 91
},
"world_3": {
"name": "DezfulAirport",
"callsign": "DZF",
"beacon_type": 8,
"hertz": 293000,
"channel": null
},
"airfield22_1": {
"world_4": {
"name": "AbadanIntAirport",
"callsign": "ABD",
"beacon_type": 3,
"hertz": 115100000,
"channel": 98
},
"world_5": {
"name": "AhvazIntAirport",
"callsign": "AWZ",
"beacon_type": 3,
"hertz": 114000000,
"channel": 87
},
"world_6": {
"name": "AghajariAirport",
"callsign": "AJR",
"beacon_type": 3,
"hertz": 114900000,
"channel": 96
},
"world_7": {
"name": "BirjandIntAirport",
"callsign": "BJD",
"beacon_type": 3,
"hertz": 113500000,
"channel": 82
},
"world_8": {
"name": "BushehrIntAirport",
"callsign": "BUZ",
"beacon_type": 3,
"hertz": 117450000,
"channel": 121
},
"world_9": {
"name": "KonarakAirport",
"callsign": "CBH",
"beacon_type": 3,
"hertz": 115600000,
"channel": 103
},
"world_10": {
"name": "IsfahanIntAirport",
"callsign": "ISN",
"beacon_type": 3,
"hertz": 113200000,
"channel": 79
},
"world_11": {
"name": "KhoramabadAirport",
"callsign": "KRD",
"beacon_type": 3,
"hertz": 113750000,
"channel": 84
},
"world_12": {
"name": "PersianGulfIntAirport",
"callsign": "PRG",
"beacon_type": 3,
"hertz": 112100000,
"channel": 58
},
"world_13": {
"name": "YasoujAirport",
"callsign": "YSJ",
"beacon_type": 3,
"hertz": 116550000,
"channel": 112
},
"world_14": {
"name": "BamAirport",
"callsign": "BAM",
"beacon_type": 3,
"hertz": 114900000,
"channel": 96
},
"world_15": {
"name": "MahshahrAirport",
"callsign": "MAH",
"beacon_type": 3,
"hertz": 115800000,
"channel": 105
},
"world_16": {
"name": "IranShahrAirport",
"callsign": "ISR",
"beacon_type": 3,
"hertz": 117000000,
"channel": 117
},
"world_17": {
"name": "LamerdAirport",
"callsign": "LAM",
"beacon_type": 3,
"hertz": 117000000,
"channel": 117
},
"world_18": {
"name": "SirjanAirport",
"callsign": "SRJ",
"beacon_type": 3,
"hertz": 114600000,
"channel": 93
},
"world_19": {
"name": "YazdIntAirport",
"callsign": "YZD",
"beacon_type": 3,
"hertz": 117700000,
"channel": 124
},
"world_20": {
"name": "ZabolAirport",
"callsign": "ZAL",
"beacon_type": 3,
"hertz": 113100000,
"channel": 78
},
"world_21": {
"name": "ZahedanIntAirport",
"callsign": "ZDN",
"beacon_type": 3,
"hertz": 116000000,
"channel": 107
},
"world_22": {
"name": "RafsanjanAirport",
"callsign": "RAF",
"beacon_type": 3,
"hertz": 112300000,
"channel": 70
},
"world_23": {
"name": "SaravanAirport",
"callsign": "SRN",
"beacon_type": 3,
"hertz": 114100000,
"channel": 88
},
"world_24": {
"name": "BuHasa",
"callsign": "BH",
"beacon_type": 2,
"hertz": 309000000,
"channel": null
},
"airfield22_0": {
"name": "AbuDhabiInt",
"callsign": "ADV",
"beacon_type": 2,
"hertz": 114250000,
"channel": 119
},
"airfield22_1": {
"name": "ABUDHABI",
"callsign": "ADV",
"beacon_type": 1,
"hertz": 114250000,
"channel": null
},
"airfield1_0": {
"name": "Abumusa",
"callsign": "ABM",
@@ -530,180 +705,5 @@
"beacon_type": 4,
"hertz": 114200000,
"channel": 89
},
"world_0": {
"name": "Kish",
"callsign": "KIS",
"beacon_type": 3,
"hertz": 117400000,
"channel": 121
},
"world_1": {
"name": "DohaAirport",
"callsign": "DIA",
"beacon_type": 3,
"hertz": 112400000,
"channel": 71
},
"world_2": {
"name": "HamadInternationalAirport",
"callsign": "DOH",
"beacon_type": 3,
"hertz": 114400000,
"channel": 91
},
"world_3": {
"name": "DezfulAirport",
"callsign": "DZF",
"beacon_type": 8,
"hertz": 293000,
"channel": null
},
"world_4": {
"name": "AbadanIntAirport",
"callsign": "ABD",
"beacon_type": 3,
"hertz": 115100000,
"channel": 98
},
"world_5": {
"name": "AhvazIntAirport",
"callsign": "AWZ",
"beacon_type": 3,
"hertz": 114000000,
"channel": 87
},
"world_6": {
"name": "AghajariAirport",
"callsign": "AJR",
"beacon_type": 3,
"hertz": 114900000,
"channel": 96
},
"world_7": {
"name": "BirjandIntAirport",
"callsign": "BJD",
"beacon_type": 3,
"hertz": 113500000,
"channel": 82
},
"world_8": {
"name": "BushehrIntAirport",
"callsign": "BUZ",
"beacon_type": 3,
"hertz": 117450000,
"channel": 121
},
"world_9": {
"name": "KonarakAirport",
"callsign": "CBH",
"beacon_type": 3,
"hertz": 115600000,
"channel": 103
},
"world_10": {
"name": "IsfahanIntAirport",
"callsign": "ISN",
"beacon_type": 3,
"hertz": 113200000,
"channel": 79
},
"world_11": {
"name": "KhoramabadAirport",
"callsign": "KRD",
"beacon_type": 3,
"hertz": 113750000,
"channel": 84
},
"world_12": {
"name": "PersianGulfIntAirport",
"callsign": "PRG",
"beacon_type": 3,
"hertz": 112100000,
"channel": 58
},
"world_13": {
"name": "YasoujAirport",
"callsign": "YSJ",
"beacon_type": 3,
"hertz": 116550000,
"channel": 112
},
"world_14": {
"name": "BamAirport",
"callsign": "BAM",
"beacon_type": 3,
"hertz": 114900000,
"channel": 96
},
"world_15": {
"name": "MahshahrAirport",
"callsign": "MAH",
"beacon_type": 3,
"hertz": 115800000,
"channel": 105
},
"world_16": {
"name": "IranShahrAirport",
"callsign": "ISR",
"beacon_type": 3,
"hertz": 117000000,
"channel": 117
},
"world_17": {
"name": "LamerdAirport",
"callsign": "LAM",
"beacon_type": 3,
"hertz": 117000000,
"channel": 117
},
"world_18": {
"name": "SirjanAirport",
"callsign": "SRJ",
"beacon_type": 3,
"hertz": 114600000,
"channel": 93
},
"world_19": {
"name": "YazdIntAirport",
"callsign": "YZD",
"beacon_type": 3,
"hertz": 117700000,
"channel": 124
},
"world_20": {
"name": "ZabolAirport",
"callsign": "ZAL",
"beacon_type": 3,
"hertz": 113100000,
"channel": 78
},
"world_21": {
"name": "ZahedanIntAirport",
"callsign": "ZDN",
"beacon_type": 3,
"hertz": 116000000,
"channel": 107
},
"world_22": {
"name": "RafsanjanAirport",
"callsign": "RAF",
"beacon_type": 3,
"hertz": 112300000,
"channel": 70
},
"world_23": {
"name": "SaravanAirport",
"callsign": "SRN",
"beacon_type": 3,
"hertz": 114100000,
"channel": 88
},
"world_24": {
"name": "BuHasa",
"callsign": "BH",
"beacon_type": 2,
"hertz": 309000000,
"channel": null
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ country: Combined Joint Task Forces Blue
name: NATO Desert Storm
authors: Hawkmoon
description:
<p>A faction to recreate the actual unit lineup during Desert Storm as
closely as possible</p>
<p>A faction to recreate the actual unit lineup during Desert Storm as closely
as possible</p>
aircrafts:
- A-10A Thunderbolt II
- AH-64A Apache
@@ -18,6 +18,7 @@ aircrafts:
- F-14B Tomcat
- F-15C Eagle
- F-15E Strike Eagle
- F-15E Strike Eagle (Suite 4+)
- F-16CM Fighting Falcon (Block 50)
- F-4E Phantom II
- F/A-18C Hornet (Lot 20)
@@ -72,7 +73,7 @@ naval_units:
- CVN-74 John C. Stennis
missiles: []
air_defense_units:
- SAM Patriot STR
- EWR AN/FPS-117 Radar
- M163 Vulcan Air Defense System
- M1097 Heavy HMMWV Avenger
- M48 Chaparral

View File

@@ -3,8 +3,8 @@ country: Combined Joint Task Forces Blue
name: NATO OIF
authors: Fuzzle
description:
<p>A more modern NATO mixed faction reflecting the units involved in
Operation Iraqi Freedom.</p>
<p>A more modern NATO mixed faction reflecting the units involved in Operation
Iraqi Freedom.</p>
aircrafts:
- A-10C Thunderbolt II (Suite 3)
- AH-64D Apache Longbow
@@ -19,6 +19,7 @@ aircrafts:
- F-14B Tomcat
- F-15C Eagle
- F-15E Strike Eagle
- F-15E Strike Eagle (Suite 4+)
- F-16CM Fighting Falcon (Block 50)
- F-22A Raptor
- F/A-18C Hornet (Lot 20)
@@ -75,7 +76,7 @@ naval_units:
- CVN-74 John C. Stennis
missiles: []
air_defense_units:
- SAM Patriot STR
- EWR AN/FPS-117 Radar
- M163 Vulcan Air Defense System
- M1097 Heavy HMMWV Avenger
- M48 Chaparral

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