Compare commits

...

187 Commits

Author SHA1 Message Date
walterroach
6f65637f6b Fix bug #353 2020-11-27 17:47:19 -06:00
Dan Albert
cceb3da693 Resurrect force multiplier option.
Fixes https://github.com/Khopa/dcs_liberation/issues/440

(cherry picked from commit 611f04ab5a)
2020-11-25 14:13:54 -08:00
walterroach
a357bf3c08 Fix bug #400 2020-11-22 17:32:05 -06:00
walterroach
3f251c38d8 Set AGL altitude on target waypoints 2020-11-22 16:42:22 -06:00
walterroach
01702f046e Add missing P-47 icons 2020-11-22 02:12:15 -06:00
Ignacio Muñoz Fernandez
d394d01ea8 fix: conditional value at passTurn on QTopPanel 2020-11-20 17:12:13 -08:00
Ignacio Muñoz Fernandez
70d982b0ed fix: Pass Turn and Take Off Buttons when no game is loaded 2020-11-20 17:12:13 -08:00
Ignacio Muñoz Fernandez
c3b028ef4b fix: fixes #325 for version 2.2.x 2020-11-20 03:50:58 -08:00
Dan Albert
206d09f7f8 Maybe correct fishbed radios.
Maybe fixes https://github.com/Khopa/dcs_liberation/issues/377
2020-11-20 00:20:01 -08:00
Khopa
87248fec53 Changelog update 2020-11-19 22:00:56 +01:00
Khopa
b7634a8ac3 About dialog update 2020-11-19 21:54:33 +01:00
walterroach
434755a620 Update README.md 2020-11-19 14:23:43 -06:00
walterroach
3eb2529b0b Fix #402 2020-11-19 13:20:05 -06:00
Khopa
e6e4cca076 Made it possible to setup custom liveries in faction files. 2020-11-19 20:18:26 +01:00
Khopa
8d57bbc777 Changelog update 2.2.1 2020-11-19 20:17:11 +01:00
Khopa
483db564f9 Added F-14A support 2020-11-19 20:14:21 +01:00
Khopa
63d5862319 Pydcs module update 2020-11-19 20:13:15 +01:00
Dan Albert
365b379798 Update changelog for 2.2.1. 2020-11-19 00:45:03 -08:00
Dan Albert
fd473f0a46 Fix custom waypoints.
Like with deleting waypoints, these will degrade the flight plan to the
2.1 behavior.

Ascend/descend points aren't in use any more, so I removed those.
2020-11-19 00:36:43 -08:00
Dan Albert
a816877d08 Allow deleting waypoints.
In almost every case this leaves us with a flight plan we can't reason
about, so it gets degraded to `CustomFlightPlan`. The exception is when
deleting a target point when there are other target points remaining.
This probably gets people using this feature back to what they want
though, which is essentially the 2.1 behavior.

Fixes https://github.com/Khopa/dcs_liberation/issues/393
2020-11-18 23:43:01 -08:00
Dan Albert
2d56ae1cb6 Avoid cases where empty flights could be created.
Fixes https://github.com/Khopa/dcs_liberation/issues/373
2020-11-18 22:04:24 -08:00
Dan Albert
216adcc35a Add infor about delayed flights to the start page.
Fixes https://github.com/Khopa/dcs_liberation/issues/398
2020-11-18 21:27:19 -08:00
Dan Albert
8d485d5fa2 Delay player CV flight when their settings say so.
Fixes https://github.com/Khopa/dcs_liberation/issues/375

This also fixes a problem where we're spawning non-cold start planes in
an uncontrolled state. The ME won't let us do this, so we probably
shouldn't be doing that.
2020-11-18 21:27:18 -08:00
Dan Albert
fa549fcf94 Don't delay player flights with short delays.
Not much point in delaying humans 8 seconds.

Fixes https://github.com/Khopa/dcs_liberation/issues/397
2020-11-18 21:27:18 -08:00
Dan Albert
b7d160631a Further improve split/join positioning.
(cherry picked from commit dc235f36c8)
2020-11-16 19:15:41 -08:00
walterroach
d05897edcb Change default CAS loadout for Viggen
Reported that AI can't hit the broad side of a barn with the rockets.
2020-11-15 22:59:46 -06:00
walterroach
1d98432c57 Briefing tweak
Fixes frontline info repeating when player has no vehicles.
2020-11-15 22:59:40 -06:00
Dan Albert
21cd764f66 Improve hold/split/join point positioning.
This also removes ascend/descend waypoints. They don't seem to be
helping at all. The AI already gets an implicit ascend waypoint (they
won't go to waypoint one until they've climbed sufficiently), and
forcing unnecessary sharp turns toward the possibly mispredicted ascent
direction can mess with the AI. It's also yet another variable to
contend with when planning hold points, and hold points do essentially
the same thing.

Fixes https://github.com/Khopa/dcs_liberation/issues/352.
2020-11-15 18:53:17 -08:00
Khopa
dfc31dfd5c Changelog update
(cherry picked from commit 8ffbf32677)
2020-11-15 18:53:13 -08:00
Khopa
2e067aada6 Added full persian gulf map by Plob 2020-11-15 15:57:36 +01:00
Khopa
78cd60f3df Added factions made by Discord user HerrTom 2020-11-15 15:50:29 +01:00
Dan Albert
96c401e1b9 Fix pyinstaller spec for release.
final and buildnumber are optional files. Move them into resources to
avoid naming them explicitly.

(cherry picked from commit fae9650f56)
2020-11-14 13:06:59 -08:00
Dan Albert
fad132dcca Fix versioning for release builds.
(cherry picked from commit 9019cbfd2b)
2020-11-14 13:06:01 -08:00
Khopa
696710bf41 Merge remote-tracking branch 'khopa/master' into develop_2_2_x
# Conflicts:
#	changelog.md
#	game/operation/operation.py
#	gen/aircraft.py
#	gen/groundobjectsgen.py
#	qt_ui/uiconstants.py
2020-11-14 21:40:07 +01:00
Khopa
f8735927bf Version string 2.2.0 2020-11-14 21:32:27 +01:00
Khopa
16cfc4e945 Changelog update for 2.2.0 2020-11-14 21:32:05 +01:00
Khopa
3987274764 Pulled latest pydcs version 2020-11-14 21:24:15 +01:00
Khopa
73a97f9c2a Added High Digit Sams mods pydcs export extensions, maybe for later use. 2020-11-14 20:27:38 +01:00
walterroach
169fba9ab8 fixes viggen client waypoints
Only one target waypoint created when flight is Viggen client flight.

    M Waypoints not explicitly set anymore
      (they don't need to be when waypoint has a TOT)
2020-11-14 12:17:15 -06:00
Khopa
0b902e19ee Adding Persian gulf started mizdata files before release 2020-11-14 14:48:48 +01:00
Khopa
1f43fbe16e A-20G won't level bomb unit groups 2020-11-14 14:17:07 +01:00
Dan Albert
f29cb99530 Update the README with a more recent screenshot.
(cherry picked from commit ef84703da9)
2020-11-14 01:39:47 -08:00
Dan Albert
9e32ea7413 Handle inventory when selling aircraft.
This still leaves a bit to be desired, namely that selling aircraft
happens immediately but buying aircraft takes a turn. However, that's
how this behaved before, so this restores the 2.1 behavior. Worth
investigating further in the future.

(cherry picked from commit 75769df8e2)
2020-11-14 00:12:02 -08:00
Dan Albert
95fd4cab05 Fix error box in flight creation.
(cherry picked from commit a81254cd18)
2020-11-13 21:01:30 -08:00
Dan Albert
c4d08fa7b7 Fix handling of non-AA units in AA groups.
Some units in pydcs have detection_range and threat_range
defined, but explicitly set to None.
2020-11-13 02:57:51 -08:00
Dan Albert
cec28351e7 Remove dead code. 2020-11-13 01:20:59 -08:00
Khopa
9620ac7e7e Updated mizdata on Caucasus map 2020-11-12 21:08:44 +01:00
Dan Albert
c0bfdbf4bb Update client counts when packages are changed.
Fixes https://github.com/Khopa/dcs_liberation/issues/345
2020-11-11 15:06:57 -08:00
Dan Albert
efb544a303 Fix bad argument type. 2020-11-11 15:05:58 -08:00
Dan Albert
adfc4b7244 Don't plan DEAD missions against scuds.
Fixes https://github.com/Khopa/dcs_liberation/issues/355
2020-11-11 14:57:39 -08:00
Khopa
7a5ce98569 Added mizdata preset locations for the channel map. 2020-11-11 14:27:13 +01:00
Khopa
1fcceb0901 Mizdata for the channel map 2020-11-11 02:53:12 +01:00
Khopa
22c552053f Made Patriot and Sa-10 sites more compact, so it's easier to find valid position to generate them on the map. 2020-11-11 01:03:57 +01:00
Khopa
818c679d4f Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2020-11-10 23:48:45 +01:00
Khopa
3ff36c45aa Ground Object and Sam sites locations are chosen from a set of preset location when possible. 2020-11-10 23:48:27 +01:00
Dan Albert
623d461b06 Fix delays of AI flights.
Fixes https://github.com/Khopa/dcs_liberation/issues/354
2020-11-10 13:18:06 -08:00
Dan Albert
7535013848 Fix #85: update the max aircraft available.
https://github.com/Khopa/dcs_liberation/issues/85
2020-11-10 00:52:19 -08:00
Dan Albert
a63bac8826 Don't display scud sites as SAM threat range. 2020-11-09 21:51:55 -08:00
Dan Albert
d2c831c4ee Handle inventory for changing flight size. 2020-11-09 21:29:47 -08:00
walterroach
840b5ce071 Change CAS ROE 2020-11-09 20:36:07 -06:00
walterroach
be6abc0025 Don't display attack button for dead base groups 2020-11-09 15:48:34 -06:00
walterroach
ef585c59dd Merge pull request #343 from walterroach/missed_events
Missed Debrief Events
2020-11-09 15:36:09 -06:00
walterroach
16d9c1ccad Add EWR to destruction events in debrief
Also impacted reinforcement generation on turn after EWR destroyed.
2020-11-09 14:49:44 -06:00
walterroach
9a9ef78583 Fix statics not being killed in debrief 2020-11-09 13:34:56 -06:00
Dan Albert
fe7ee5b610 Add more to the changelog. 2020-11-09 00:55:57 -08:00
walterroach
680804040a Update changelog. 2020-11-09 00:43:53 -08:00
Dan Albert
0d4fe73daa Add ships to skynet EWR. 2020-11-08 23:51:28 -08:00
Dan Albert
407190c6c5 Use EWRs for EWR. 2020-11-08 22:17:06 -08:00
Dan Albert
73998dbde0 Show EWRs as detection range, not threat range. 2020-11-08 19:03:47 -08:00
Dan Albert
8827f7df34 Cleanup Lua plugin implementation.
* Move the UI code out of the plugin logic.
* Add types where needed.
* Move into game package.
* Improve error handling.
* Simplify settings behavior.
* Don't load disabled plugins.
* Remove knowledge of non-base plugins from game generation.

Fixes https://github.com/Khopa/dcs_liberation/issues/311
2020-11-08 17:53:52 -08:00
Khopa
4c394a9e2d Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2020-11-08 23:24:24 +01:00
Khopa
5946fc7404 Improved campaign selection screen in new game wizard. 2020-11-08 23:24:10 +01:00
Dan Albert
5b8ecb2c14 Add radio data for the MiG-21.
Fixes https://github.com/Khopa/dcs_liberation/issues/49
2020-11-08 13:25:58 -08:00
Khopa
61253e4d4d Show name instead of Id of ground units in faction selection screen. 2020-11-08 22:02:16 +01:00
Khopa
2c0ca5803f Improved the faction selection screen in the new game wizard. 2020-11-08 21:57:26 +01:00
Khopa
690f3d0f13 B-52 default strike loadout use Mk-84 2020-11-08 13:52:43 +01:00
walterroach
42c259bc58 Fix briefing whitespace 2020-11-08 01:28:28 -08:00
walterroach
11426a0713 Revert "more explicity waypoint namespace"
This reverts commit 314be482f9.
2020-11-07 21:16:10 -08:00
walterroach
e6af1b8645 more explicity waypoint namespace 2020-11-07 21:16:10 -08:00
walterroach
103f18191d Move viggen tot check down to pydcs waypoint only. 2020-11-07 21:16:10 -08:00
walterroach
fb312236a2 rework special waypoints 2020-11-07 21:16:10 -08:00
Dan Albert
c850c0095d Add DEAD to the flight recreate types. 2020-11-07 17:07:45 -08:00
Dan Albert
58481268f7 Fix mypy issue. 2020-11-07 16:33:00 -08:00
Dan Albert
11604671f8 Fix bad stagger interval calculation.
Was using the interval from mission start to latest rather than from
earliest to latest, so this could sometimes be off by a bit and cause
us to not generate enough start times.
2020-11-07 16:25:56 -08:00
Dan Albert
e8feded4c3 Add EWR generation.
Fixes https://github.com/Khopa/dcs_liberation/issues/66
2020-11-07 16:20:58 -08:00
walterroach
18f9b38d25 Keep only last base capture per base in debrief 2020-11-07 15:14:02 -08:00
Dan Albert
676eea3ccc Don't plan missions against dead targets.
Fixes https://github.com/Khopa/dcs_liberation/issues/314
2020-11-07 13:59:52 -08:00
Dan Albert
ee113d080e Merge branch 'theater-refactor' into develop 2020-11-07 13:39:35 -08:00
Dan Albert
8bc69415a7 Clean up base defense generation. 2020-11-07 13:38:58 -08:00
Dan Albert
3979ee57ff Cleanup oddities of CP generation.
* Don't spawn missile sites or ground locations for CVs/LHAs.
* Don't spawn ground locations around CVs/LHAs.
* Spawn AA sites even if the faction has no buildings defined.
2020-11-07 13:38:58 -08:00
Dan Albert
c2ee169d16 Fix regeneration of base defenses on capture.
I messed up the counting here and was counting *every* object rather
than just the base defenses, so it was very unlikely that we'd hit
base defenses on the second object, which is the only index valid for
SAM generation. That logic maybe needs to be different, but this
restores the previous behavior.
2020-11-07 13:38:58 -08:00
Dan Albert
56b51c85bb Fix targeting SAMs with strike missions.
The changes for Skynet unfortunately broke this because the names used
by the TGO and the name of the group itself were no longer in sync.
This deserves a larger cleanup where we decouple that naming
requirement (TGOs don't need a lot of the data they currently have),
but this works until we have the time to do that.

Fixes https://github.com/Khopa/dcs_liberation/issues/298
2020-11-07 13:37:52 -08:00
Dan Albert
853ee5aac4 Cleanup Theater Ground Object.
A whole bunch of this data is redundant.
2020-11-07 13:10:06 -08:00
Dan Albert
4cf406aefa Defer game load to after UI initialization.
This removes both the double load that happens on startup and also
makes it possible to get the UI to create a new game if the existing
default.liberation is not compatible.
2020-11-07 13:03:25 -08:00
Khopa
1abb341cb6 Added SCUD missiles sites 2020-11-07 19:20:08 +01:00
Khopa
44dce9598c Added support for Tetrarch and Daimler Armoured car. (New WW2 units) 2020-11-07 17:43:30 +01:00
Khopa
aefc8685a1 Added new FLAK units to WW2 allies factions 2020-11-07 17:40:26 +01:00
Khopa
26761342f5 Changelog update 2020-11-07 16:25:23 +01:00
Khopa
ca777bcebb Tuned base defense location generator for big airbases. (Some would end up without any base defenses before) 2020-11-07 16:23:46 +01:00
Khopa
5742075ff2 Syria terrain update for Incirlik airbase exlusions zones. 2020-11-07 16:22:59 +01:00
Khopa
c7a6ec9691 Changelog update 2020-11-07 03:27:03 +01:00
Khopa
e0153cfa6a Added Freya EWR sites to WW2 german factions 2020-11-07 03:21:53 +01:00
Khopa
3fd5e1bae7 Added newest P-47 variants to WW2 factions 2020-11-07 02:31:29 +01:00
Khopa
d9511a7edd Improved Strike mission generation.
- B52, A-20, and Tu-22 will level bomb targets
- When there is an unit group as target, all the units are now engaged instead of only the first unit of the group
2020-11-07 02:13:17 +01:00
C. Perreau
040db055fd Merge pull request #309 from DanAlbert/theater-refactor
Refactor game and ground object generation.
2020-11-06 20:33:19 +01:00
C. Perreau
b9f8cfd10d Merge pull request #319 from walterroach/fix_flight_edit
Fixes #312
2020-11-06 20:18:33 +01:00
Khopa
ff46556927 Fixed russia 1990 faction. Added Tu_22M3. 2020-11-06 18:56:25 +01:00
walterroach
d1815a3d6e Fixes #312 2020-11-06 10:38:29 -06:00
Dan Albert
fdfa4827ab Remove unused and broken scripts. 2020-11-05 21:39:49 -08:00
Dan Albert
5d579ccef9 Add hints for creating packages. 2020-11-05 17:08:34 -08:00
Dan Albert
4145d5578e Refactor game and ground object generation.
No real functional improvements yet, just reorganizing to make
improvements easier.
2020-11-05 16:09:34 -08:00
Dan Albert
43eb041bb8 Fix escort planning for racetracks. 2020-11-05 15:17:45 -08:00
Dan Albert
0b8ac8fc47 Move to hold points more slowly.
This should give the AI a better chance to climb before reaching the
waypoint.
2020-11-05 15:17:45 -08:00
Dan Albert
de3ba5908f Round waypoint TOTs in the UI. 2020-11-05 15:17:45 -08:00
C. Perreau
bbb6251aa9 Merge pull request #308 from walterroach/briefing_jinja
Briefing jinja
2020-11-05 23:31:58 +01:00
walterroach
6f71d92a7b Merge branch 'develop' into briefing_jinja 2020-11-05 16:08:16 -06:00
walterroach
a8b59cc567 Move briefing string literals to template 2020-11-05 16:07:44 -06:00
walterroach
f4d3660eac split sead and dead
change SEAD to engage in zone
2020-11-05 13:42:44 -08:00
walterroach
1f165835c6 type hinting and comment cleanup 2020-11-05 13:20:54 -08:00
walterroach
9087f3487d Single ShipGroupGenerator added 2020-11-05 13:20:54 -08:00
Walter
4ca92ea22d Fix ships being wrong unit type 2020-11-05 13:20:54 -08:00
Khopa
e2682d633f Added payloads for bombers. 2020-11-05 20:43:49 +01:00
Khopa
e6cb1b5970 Updated pydcs submodule commit reference 2020-11-05 19:59:21 +01:00
Khopa
de2d548139 Added Flak guns to most coldwar factions 2020-11-05 00:34:57 +01:00
Khopa
4d1a0b85e4 Added free version of WW2 factions that will not require the WW2 Asset Pack. 2020-11-04 22:25:24 +01:00
Khopa
5cfbd8c3ad Added building set for WW2 units that does not require WW2 asset pack. 2020-11-04 22:01:28 +01:00
Khopa
95f72be8eb Fixed issue with duplicated WW2 germany faction IDs 2020-11-04 21:51:09 +01:00
Khopa
95c4dfa52f Added new Flak site configuration (now that flak 18 and bofors guns do not require WW2 asset pack) 2020-11-04 21:50:25 +01:00
Khopa
b72a2f4a5f Fixed error in new game wizard with units and mods required not being displayed correctly in some cases. 2020-11-04 21:09:48 +01:00
Khopa
844f8595d1 Fixed ID of some units to match new pydcs data-export. 2020-11-04 21:07:28 +01:00
Khopa
1c9d9be667 Using latest pydcs data-export 2020-11-04 21:02:01 +01:00
Khopa
2a02a743a4 Do not display manage button on base defense group for enemy cp. 2020-11-04 18:53:18 +01:00
Khopa
968d9365d6 Re-added F-15E to bluefor modern. 2020-11-03 20:38:04 +01:00
Walter
cdb16cc591 Fixes #268
Changes red base info to show defenses first, and adds an attack option
2020-11-02 18:06:10 -08:00
Walter
86bc41c15c Add base menu to new package dialog for red cp 2020-11-02 18:06:10 -08:00
Dan Albert
ac05c7cfaa Fix recreate as CAP for ground targets. 2020-11-02 02:33:09 -08:00
Dan Albert
9c07fe5963 Remove source of error in package waypoint timing.
We were calculating the TOT based on travel time to the *flight's*
target, but the ingress point based on the travel time to the target
area. If the difference in travel time between the center of the
target area and the first target were different then we'd calculate
the start time incorrectly even for single flight packages.

Seems to fix https://github.com/Khopa/dcs_liberation/issues/295
2020-11-02 02:26:07 -08:00
Dan Albert
ed05f995b5 Refactor strike formation timing calculations. 2020-11-02 02:25:33 -08:00
Dan Albert
85491dca20 Fix front line CAP patrol end time. 2020-11-02 01:34:35 -08:00
Dan Albert
465399f803 Add radio configuration for the UH-1H and Ka-50. 2020-11-01 17:12:42 -08:00
Dan Albert
3550c8a8f6 Remove afterburner restrictions.
The AI often needs afterburner to recover from high AoA.

Fixes https://github.com/Khopa/dcs_liberation/issues/205
2020-11-01 15:12:05 -08:00
Dan Albert
739c0f8f52 Add radio data for MiG-15bis and MiG-19P.
Fixes https://github.com/Khopa/dcs_liberation/issues/233.
2020-11-01 14:50:23 -08:00
Dan Albert
49aa79c612 Fix mypy issues. 2020-11-01 14:49:50 -08:00
Dan Albert
cdde75b517 Add option to avoid delaying player flights.
Fixes https://github.com/Khopa/dcs_liberation/issues/227
2020-11-01 14:13:06 -08:00
Dan Albert
dde74af6b5 Fix client/player detection.
Client needs to be used if there are other player slots in *any*
flight, not just the same group.

Fixes https://github.com/Khopa/dcs_liberation/issues/297
2020-11-01 14:02:32 -08:00
Dan Albert
eff9c77c9a Fix departure time in the kneeboard.
We don't have the departure time set until after we create the initial
FlightData object. Populate the value after it is determined.

Fixes https://github.com/Khopa/dcs_liberation/issues/290
2020-11-01 13:31:56 -08:00
Dan Albert
5ba633c8a1 Round TOT/start time as needed.
The increased precision that we had everywhere except the UI and the
interface with DCS was causing issues with ASAP creating barely
negative start times. The main cause of this was that we'd compute the
earliest possible TOT, it would result in, for example, 23:10.002.
When we then set the QTimeEdit for the TOT, we have to round because
it does not support (nor do we really want to display) sub-second
values, which then caused the previously 0 start time to be -0.002.

Instead, since the sub-second values aren't really interesting anyway,
we now just round TOTs up and start times down. This should prevent
negative start times from occurring (except when they've been manually
planned as such), and also prevents start times of 00:00:01.

Also rounds the package waypoint times to avoid the same issues, but
it's not really important which direction we round these.

Fixes https://github.com/Khopa/dcs_liberation/issues/295
2020-11-01 13:31:10 -08:00
Dan Albert
ab67a38ca5 Remove rounding from waypoint timing in the UI.
This is behaving strangely on some machines. Stop hiding the details
in the UI while we debug.
2020-11-01 01:42:58 -08:00
Dan Albert
08f0c9d30a Fix timedelta -> timedelta conversion.
Fixes https://github.com/Khopa/dcs_liberation/issues/293
2020-11-01 00:58:35 -07:00
Dan Albert
9d747a9f9b Fix kneeboard ground speeds. 2020-10-31 20:28:07 -07:00
walterroach
31ca121498 Add github build number to title bar for preview
builds
2020-10-31 20:10:58 -07:00
walterroach
44b5f5acf1 added notify call back 2020-10-31 20:09:20 -07:00
walterroach
eb4878dfc4 remove useless props 2020-10-31 20:09:20 -07:00
walterroach
3dc7dc3d1a Dan'ts notes
commit 35ab9103cebf823bab85fbb1c9ff4bc2b9c5701a
Author: walterroach <37820425+walterroach@users.noreply.github.com>
Date:   Fri Oct 30 22:25:42 2020 -0500

    more cleanup

commit d3d008bc6b29f328ad48063bd4b0520e1b819d68
Author: walterroach <37820425+walterroach@users.noreply.github.com>
Date:   Fri Oct 30 22:17:59 2020 -0500

    More briefinggen cleanup

commit b2033f091d7191aecefb86fecb5cb060c074706e
Author: walterroach <37820425+walterroach@users.noreply.github.com>
Date:   Fri Oct 30 22:08:48 2020 -0500

    briefinggen cleanup

commit 72ea0196c22d9493df078765800f3fafb9c054dc
Author: walterroach <37820425+walterroach@users.noreply.github.com>
Date:   Fri Oct 30 21:57:52 2020 -0500

    Add notifier method back to Operation

commit efd39a9e03d02b9d581637d0d1c289af68e749c3
Author: walterroach <37820425+walterroach@users.noreply.github.com>
Date:   Fri Oct 30 21:34:37 2020 -0500

    Revert "Move kneeboard and briefing gen to unified"
    Removes properties added to Operation
    This reverts commit 941f2af770.
2020-10-31 20:09:20 -07:00
Walter
6a6133e5cd template cleanup 2020-10-31 20:09:20 -07:00
Walter
65c85d7f0b init args change 2020-10-31 20:09:20 -07:00
Walter
d519dfa5da docstrings and cleanup 2020-10-31 20:09:20 -07:00
Walter
73ea83bbdd Move kneeboard and briefing gen to unified
interface
2020-10-31 20:09:20 -07:00
Walter
235a5ec538 cleanup 2020-10-31 20:09:20 -07:00
walterroach
f81a3d03c0 working briefinggen refactor 2020-10-31 20:09:20 -07:00
walterroach
6878b57fba refactor frontline brief generation 2020-10-31 20:09:20 -07:00
walterroach
0143e5641f var change 2020-10-31 20:09:20 -07:00
walterroach
5adc92c601 Properly get enum instead of value on strategy
selecter
2020-10-31 20:09:20 -07:00
walterroach
0b2fbddbc5 refactor frontline situation briefing 2020-10-31 20:09:20 -07:00
Walter
28035bf02b linting and formatting 2020-10-31 20:09:20 -07:00
Walter
6c9a9de3f3 parent f03121af5a
first pass briefing refactor

briefing fixes

briefing fixes

Stop briefing generate being called twice

Stop frontline advantage string being appended
when there are no units.

jinja template

always return enum instance in Strategy Selector

For some reason on DEFENSE, enum is appended to control point stance,
but on all other the enum.value is added instead.

I don't see any case where the value is used, but there are many
cases that the enum instance is evaluated against.

type issue

junja's not a thing

swap mapping with dict

jinja template

always return enum instance in Strategy Selector

For some reason on DEFENSE, enum is appended to control point stance,
but on all other the enum.value is added instead.

I don't see any case where the value is used, but there are many
cases that the enum instance is evaluated against.

type issue

Update build.yml

junja's not a thing

swap mapping with dict

Restore build job
2020-10-31 20:09:20 -07:00
Walter
62139fc4eb Fix Nevada landmap not loading
logging on open try except block
2020-10-31 19:55:26 -07:00
Dan Albert
88b9ed29ba Reorganize flight planning.
Previously we were trying to make every potential flight plan look
just like a strike mission's flight plan. This led to a lot of special
case behavior in several places that was causing us to misplan TOTs.

I've reorganized this such that there's now an explicit `FlightPlan`
class, and any specialized behavior is handled by the subclasses.

I've also taken the opportunity to alter the behavior of CAS and
front-line CAP missions. These no longer involve the usual formation
waypoints. Instead the CAP will aim to be on station at the time that
the CAS mission reaches its ingress point, and leave at its egress
time. Both flights fly directly to the point with a start time
configured for a rendezvous.

It might be worth adding hold points back to every flight plan just to
ensure that non-formation flights don't end up with a very low speed
enroute to the target if they perform ground ops quicker than
expected.
2020-10-31 19:29:24 -07:00
Dan Albert
d94c57afd6 Fix error displaying flight edit dialog.
If no airfield was selected (as sometimes happens, usually when there
are no aircraft available anywhere), report that no aircraft are
available.
2020-10-31 18:39:06 -07:00
Dan Albert
b6421646ff Name the set TOT ASAP button better. 2020-10-30 17:28:04 -07:00
Dan Albert
611b6fc272 Fix mypy. 2020-10-30 16:43:19 -07:00
Dan Albert
9cdbef9faf Revert "Run tests in GitHub actions."
Was pushed by accident.

This reverts commit f5047fc0cc.
2020-10-30 16:14:38 -07:00
Dan Albert
b34de70fc7 Fix logging.
Same problem as last time: we were logging during initialization
before the log handlers could be initialized.
2020-10-30 16:13:34 -07:00
Dan Albert
f5047fc0cc Run tests in GitHub actions. 2020-10-30 15:09:20 -07:00
Dan Albert
258c34e61d Make test runnable from command line.
`pytest tests` works now. I can't explain why `pytest` alone does not,
but it could have something to do with us not being a real Python
package.

With just `pytest` I get:

    E   ModuleNotFoundError: No module named 'tests.test_factions'

But `python -c "import tests.test_factions"` works fine.
2020-10-30 15:05:44 -07:00
Dan Albert
f365487fd6 Remove user-specific config file. 2020-10-30 13:51:18 -07:00
Khopa
8f65b7ee7c Fixed and added many ground unit icons 2020-10-30 16:59:13 +01:00
Khopa
9397f1f39c Fixed issue with Russian 1955/65/75 factions 2020-10-30 16:16:46 +01:00
Walter
f03121af5a fixed wrong conditionals 2020-10-28 16:27:18 -07:00
C. Perreau
41445c3092 Merge pull request #187 from Khopa/develop_2_1_x
2.1.5
2020-10-05 19:45:30 +02:00
Khopa
2a3bf9821b Fixed EPLRS cherry pick merge. 2020-10-05 19:33:48 +02:00
Khopa
819d775282 EPLRS for 2.1.5 2020-10-05 19:30:43 +02:00
Khopa
efbc6fe3ae Enable EPLRS for ground units that can use it. 2020-10-05 19:29:34 +02:00
Khopa
262ba6c113 Chery picked another fix for this release, so updated changelog 2020-10-05 19:28:55 +02:00
Khopa
c41ecb6735 Fix aircrafts landing point 2020-10-05 19:27:03 +02:00
Khopa
f5c32c6b98 Version 2.1.5 2020-10-05 19:26:07 +02:00
Dan Albert
a1886e37f8 Fix save issues after aborting mission.
When the mission is aborted the pending mission is still in the event
list, which is part of the game option. That event has a reference to
the operation, which in turn contains all the mission generator
objects. Two of these objects are the radio/TACAN allocators, which
use a generator to track the next free channel. Generators cannot be
picked, so because these are transitively part of the game object the
game cannot be saved.

Aside from the briefing generator, none of those objects are
actually needed outside the generation function itself, so just make
them locals instead.

This probably needs a larger refactor at some point. It doesn't look
like we need so many calls into the operation type (it has an
initialize, a prepare, and a generate, and it doesn't seem to need
anything but the last one). The only reason breifinggen needs to
remain a part of the class is because the briefing title and
description are filled in from the derived class, where title and
description should probably be overridden properties instead. I'm also
not sure if we need to make the event list a part of game at all, and
also don't think that the mission needs to be one of these events.
2020-10-05 19:20:57 +02:00
288 changed files with 6602 additions and 2790 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2
game/plugins/__init__.py Normal file
View File

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

180
game/plugins/luaplugin.py Normal file
View File

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

50
game/plugins/manager.py Normal file
View File

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

View File

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

18
game/version.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

30
gen/missiles/scud_site.py Normal file
View File

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

View File

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

29
gen/sam/aaa_flak18.py Normal file
View File

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

View File

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

72
gen/sam/cold_war_flak.py Normal file
View File

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

98
gen/sam/ewrs.py Normal file
View File

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

39
gen/sam/freya_ewr.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,6 @@ class PatriotGenerator(GenericSamGroupGenerator):
# Short range protection for high value site
num_launchers = random.randint(3, 4)
positions = self.get_circular_position(num_launchers, launcher_distance=300, coverage=360)
positions = self.get_circular_position(num_launchers, launcher_distance=200, coverage=360)
for i, position in enumerate(positions):
self.add_unit(AirDefence.AAA_Vulcan_M163, "SPAAA#" + str(i), position[0], position[1], position[2])

View File

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

View File

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

View File

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

View File

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

View File

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

2
pydcs

Submodule pydcs updated: c12733a471...2883be31c2

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,4 +6,5 @@ Pillow~=7.2.0
tabulate~=0.8.7
mypy==0.782
mypy-extensions==0.4.3
mypy-extensions==0.4.3
jinja2>=2.11.2

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