Compare commits

..

1160 Commits
1.07 ... 2.2.1

Author SHA1 Message Date
C. Perreau
6524286f04 Merge pull request #403 from Khopa/develop_2_2_x
Release 2.2.1
2020-11-20 00:29:15 +01: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
2891649531 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:13:18 -08:00
Dan Albert
4b40739918 Fix versioning for release builds.
(cherry picked from commit 9019cbfd2b)
2020-11-14 13:13:18 -08: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
C. Perreau
e26e7f53c5 Merge pull request #367 from Khopa/develop_2_2_x
Release 2.2.0
2020-11-14 21:46:59 +01: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
Dan Albert
239b9f8234 Use python, not py.
py is a shortcut that launches the *latest* version of Python on the
machine. https://stackoverflow.com/a/50896577/632035

The build machines were updated to include python 3.9, so we were
doing everything with 3.9 instead of 3.8. pyproj doesn't have a binary
wheel for 3.9 on pypi yet, so we were falling back to building it from
source, which we aren't able to do, breaking the build.
2020-10-28 00:06:12 -07:00
Dan Albert
1620c602cf Fix mypy issue. 2020-10-27 18:12:13 -07:00
Walter
fa01303460 #257 CasIngressBuilder changes
* Calculate radius from frontline length global
* Determine CAS waypoint by waypoint type rather than name
* Change logging from error to exception
2020-10-27 09:48:36 -05:00
walterroach
1e5bd916d9 CasIngressBuilder proper inheritence 2020-10-26 23:33:56 -05:00
walterroach
62d89239fc merge 2020-10-26 23:20:05 -05:00
walterroach
91bee4e6c2 Changed CAS engage task to zone 2020-10-26 23:10:53 -05:00
Walter
a465dde32f Changed task to add AAA and infantry properly 2020-10-26 16:42:40 -07:00
Walter
254dd5f70f Add Air Defenses to engage task 2020-10-26 16:42:40 -07:00
Walter
1a70ed5121 CAS Weapons Hold until Ingress 2020-10-26 16:42:40 -07:00
Walter
16d5a550ce Move Engage targets to ingress point for CAS 2020-10-26 16:42:40 -07:00
Khopa
794cc43a41 Fix pmc mb339 modded faction id 2020-10-26 23:56:52 +01:00
Walter
8583bbf893 Changed task to add AAA and infantry properly 2020-10-26 17:19:52 -05:00
Walter
63d510f2ea Add Air Defenses to engage task 2020-10-26 15:48:22 -05:00
David Pierron
2c6b26003b Merge pull request #237 from VEAF/skynet-iads-plugin
First version of the Skynet IADS plugin
2020-10-26 21:39:58 +01:00
Walter
fdaf3bc30f CAS Weapons Hold until Ingress 2020-10-26 15:26:51 -05:00
Walter
816d9696b5 Move Engage targets to ingress point for CAS 2020-10-26 15:26:47 -05:00
David Pierron
bdaa6a294a Merge branch 'skynet-iads-plugin' of https://github.com/VEAF/dcs_liberation into skynet-iads-plugin 2020-10-26 21:17:26 +01:00
David Pierron
a6b15b9529 Merge remote-tracking branch 'upstream/develop' into skynet-iads-plugin 2020-10-26 21:17:16 +01:00
David Pierron
878529e8f8 Merge branch 'develop' into skynet-iads-plugin 2020-10-26 21:14:24 +01:00
David Pierron
2cb37b5bd8 added typing 2020-10-26 21:06:25 +01:00
David Pierron
de95cfc981 base plugin needs to be loaded first,
before skynetiads
2020-10-26 21:06:08 +01:00
David Pierron
0477247cf2 correction :
skipping plugin work orders did not work
2020-10-26 21:05:34 +01:00
David Pierron
e0319a4047 Merge remote-tracking branch 'upstream/develop'
into skynet-iads-plugin
2020-10-26 18:05:07 +01:00
Khopa
839f163ac5 Fixed issue with base defense that could not be repaired 2020-10-26 00:32:05 +01:00
Khopa
ba9ad4c371 Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop 2020-10-26 00:24:30 +01:00
Khopa
bdcf7c1828 Fixed mig 21 wrong identifier in some files. 2020-10-26 00:24:03 +01:00
Dan Albert
821e9fc114 Merge branch 'fix-mypy-actions' into develop 2020-10-25 16:19:57 -07:00
Dan Albert
334d21897b Fix mypy actions.
GitHub actions fail based on the exit code of the *last* command in
the step, so we were failing mypy but passing the action.
2020-10-25 16:19:46 -07:00
Dan Albert
b405c3ab32 Fix mypy issues in faction code. 2020-10-25 16:19:46 -07:00
Khopa
3cabb1e02d Inverted usa 1990 and 2005 factions names 2020-10-26 00:16:37 +01:00
Khopa
ec7f8f5710 Completed basic faction parameters loading test. 2020-10-25 14:38:10 +01:00
Khopa
93f0627c5e Fixed errors in faction code 2020-10-25 14:21:25 +01:00
Khopa
3d8c2d689e Added unit tests for faction. (To be completed) 2020-10-25 14:20:43 +01:00
Khopa
e1572c09ff Improved faction & faction loader typing. Fixed error with netherlands faction. 2020-10-25 14:08:11 +01:00
Khopa
910af12fb9 Default faction for bluefor is USA 2005. 2020-10-25 12:19:51 +01:00
C. Perreau
acbd45341f Merge pull request #249 from Khopa/faction_refactor
Moddable factions
2020-10-25 02:19:22 +02:00
Khopa
901c89371c Fixed syntax errors in json files 2020-10-24 23:40:05 +02:00
Khopa
743534bdda Migrated latest factions to new faction json format. 2020-10-24 23:35:39 +02:00
Walter
98bb1a9f65 Explicit message for confirm_no_client_launch 2020-10-24 13:28:02 -07:00
Dan Albert
bfc602f22f Add cheat option to capture bases.
Capturing bases is sometimes really annoying because of the DCS unit
AI and our non-optimal ground victory heuristics. Add a cheat option
to allow the player to move on without the tedium.
2020-10-24 13:06:37 -07:00
Dan Albert
dd2b61edf3 Use FrontLine in ConflictTheater.conflicts. 2020-10-24 12:24:29 -07:00
Khopa
0fd58135fd Migrated more factions to new faction json format. 2020-10-24 18:02:41 +02:00
Khopa
4672252242 Added description field to both campaign and faction json file. 2020-10-24 17:40:04 +02:00
Khopa
a0c61bf73a Authors informations are loaded in Campaign object (to be displayed in UI later) 2020-10-24 17:23:09 +02:00
Khopa
d6c19a8aff Added author field to campaign json file 2020-10-24 17:18:55 +02:00
Khopa
b6a933e264 Readded Full Map - Caucasus, remade by Discord user "george" 2020-10-24 17:16:56 +02:00
Khopa
0191eca9dc Keeping new factions up to date with develop branch. 2020-10-24 17:03:39 +02:00
Khopa
f962fd55bc Merge branch 'develop' into faction_refactor
# Conflicts:
#	game/factions/bluefor_coldwar.py
#	game/factions/bluefor_coldwar_a4.py
#	game/factions/bluefor_coldwar_mods.py
#	game/factions/bluefor_modern.py
2020-10-24 16:57:28 +02:00
David Pierron
33fa719e8d Merge branch 'skynet-iads-plugin' of https://github.com/VEAF/dcs_liberation into skynet-iads-plugin 2020-10-24 11:03:03 +02:00
David Pierron
adb9352905 removed mist.lua from skynet plugin
It's already included in the base plugin, which is mandatory
2020-10-24 11:02:46 +02:00
David Pierron
58574c67df Merge branch 'develop' into skynet-iads-plugin 2020-10-24 11:01:18 +02:00
Dan Albert
04d3ba4c47 Add mypy to github actions so we stop regressing. 2020-10-24 01:25:47 -07:00
Dan Albert
0f1d2b8685 Remove a plugin we don't include. 2020-10-24 01:10:15 -07:00
Dan Albert
c06a855113 Fix mypy regressions.
Mostly in the plugin system, which needed a handful of asserts that
shouldn't be necessary, but fixing them requires a refactor.
2020-10-24 01:10:11 -07:00
Dan Albert
e9bfd58ee1 Shorten strike waypoint descriptions.
We don't really need this much detail in these, and it was overflowing
the kneeboard.
2020-10-23 22:52:32 -07:00
Dan Albert
5f02febb6c Fix kneeboard crash for small strike missions.
When we don't coalesce target points on the kneeboard we call
add_waypoint_row multiple times, so calls after the first were using
the travel time from one strike target to the next. Since they were so
close that rounded down to zero and caused a divide by zero error.

Update the last waypoint in add_waypoint instead.
2020-10-23 22:41:28 -07:00
Dan Albert
15db12fb21 Fix TOT/start for BARCAPs in other packages.
We were only getting BARCAP results right in BARCAP packages. This
fixes calculations of TOTs and start times for BARCAPs in strike
packages.

The probably needs some refactoring. BARCAP is just the symptomatic
example at the moment, but the real problem is that different mission
profiles exist and we currently only handle one. Making profiles
explicit in mission planning will clean this up, will be needed for
other future mission types, and makes it easier for us to alter
behavior for waypoint and timing decisions based on the aircraft or
mission type.
2020-10-23 22:28:51 -07:00
Dan Albert
c3fca6696d Fix waypoint altitudes for helicopters.
Fixes https://github.com/Khopa/dcs_liberation/issues/234
2020-10-23 14:52:14 -07:00
Dan Albert
dd4c37cde3 Pick runways and ascent/descent based on headwind. 2020-10-23 14:41:42 -07:00
Dan Albert
aa7ffdabb0 Fix planned flights view, stop hiding bugs. 2020-10-23 14:19:45 -07:00
Dan Albert
85f6616185 Add bombers to coalitions.
* B-1
* B-52
* F-117
* Tu-160
* Tu-22
* Tu-95

Also alters the default loadouts for the F-15E.
2020-10-23 13:50:03 -07:00
David Pierron
c8955bdca7 activate the SAM at mission start,
to haste their response
2020-10-23 07:49:26 +02:00
Khopa
669a8c9e64 Merge branch 'develop' into faction_refactor 2020-10-23 01:39:48 +02:00
Khopa
b4d0eb0b99 Started cleaning old factions, added back most factions as json. Fixed LHA not spawning. 2020-10-23 01:38:08 +02:00
C. Perreau
035bebaab8 Merge pull request #238 from walterroach/ter_fix
Fixes issue Khopa#203 TER weapons not selectable in custom payload
2020-10-23 01:07:37 +02:00
Walter
b20200318b ter fix 2020-10-22 16:29:46 -05:00
Walter
9caf83cda9 Fixes issue #203
Conditional in comprehension appears to be intended to exclude
dunders, but was also excluding many TER weapons from Weapon class args
beginning with '_'
2020-10-22 16:28:38 -05:00
David Pierron
493f9df4e2 Merge remote-tracking branch 'upstream/develop' into skynet-iads-plugin 2020-10-22 21:36:49 +02:00
Dan Albert
cc61893bca Fix more fallout from double/right click change. 2020-10-22 12:12:28 -07:00
David Pierron
3ba2fb76aa removed VEAF from the default plugins 2020-10-22 18:40:40 +02:00
David Pierron
e024da277b first working version of the skynetiads plugin 2020-10-22 18:39:54 +02:00
Khopa
bc1e793ce6 Fixed faction loader so it works with known modded units 2020-10-22 13:55:37 +02:00
Khopa
aa7eacc043 Added list of modded units in pydcs_extensions module 2020-10-22 13:51:24 +02:00
Khopa
dd7b9f1790 Fix faction shorad units loader 2020-10-22 13:50:24 +02:00
Khopa
d6b94345d9 Merge branch 'develop' into faction_refactor 2020-10-22 13:40:14 +02:00
Khopa
aa1ac56ec3 Faction rework, working :) 2020-10-22 13:33:18 +02:00
Dan Albert
fd969020af Add escort tasks for the AI. 2020-10-22 01:29:19 -07:00
Dan Albert
95f486870d Correct AI startup time.
It doesn't look like the AI is subject to much startup time. I see
B-1, F-15, F-16, and M-2000 all start up in about 2 minutes.
2020-10-22 00:46:53 -07:00
C. Perreau
f6d049da3c Merge pull request #232 from VEAF/develop
changes to the export of state.json :
2020-10-22 09:46:10 +02:00
Dan Albert
69f15824ca Fix breakage in package list UI. 2020-10-22 00:45:10 -07:00
Dan Albert
8c70d1ab79 Fix foot to meter conversion.
Somehow this constant was wrong so all of our foot-to-meter
conversions were coming out ~7% too large. We're still introducing
some error because we're rounding early rather than only when we need
an integer, but it's much more accurate now.
2020-10-21 23:58:23 -07:00
Dan Albert
58fd651a0b Confirm exit to avoid losing progress.
Fixes https://github.com/Khopa/dcs_liberation/issues/218
2020-10-21 18:35:14 -07:00
Dan Albert
177b505cb7 Update client slots UI on end turn.
Fixes https://github.com/Khopa/dcs_liberation/issues/222
2020-10-21 17:54:58 -07:00
Dan Albert
8ac5dbe22a Add double- and right-click actions for ATO lists.
Fixes https://github.com/Khopa/dcs_liberation/issues/230
2020-10-21 17:52:21 -07:00
Khopa
b744238fb8 Factions are properly loaded, now need to refactor whole code. 2020-10-22 01:32:33 +02:00
David Pierron
57b7402753 changes to the export of state.json :
- dcsLiberation.installPath does not include "state.json" anymore
- corrected behavior :
  - try LIBERATION_EXPORT_DIR
  - then try dcsLiberation.installPath
  - then try TEMP
  - then try working directory
- corrected multiple bugs in dcs_liberation.lua
- corrected bad string.format causing DCS crashes in
  jtacautolase-config.lua
2020-10-21 23:44:36 +02:00
Khopa
dcaa390d24 Added support for Su-57 mod by Cubanace 2020-10-21 22:21:36 +02:00
Khopa
59010f6949 Added support for Su-57 mod by Cubanace 2020-10-21 21:40:31 +02:00
Khopa
6a91fad10a Merge branch 'develop' into faction_refactor 2020-10-21 19:51:25 +02:00
C. Perreau
f03029417d Merge pull request #229 from VEAF/develop
Removed VEAF submodule
2020-10-21 11:39:18 +02:00
David Pierron
f5aa342602 Removed VEAF submodule 2020-10-21 11:32:39 +02:00
C. Perreau
53582ba539 Merge pull request #228 from VEAF/new-plugin-system
New plugin system
2020-10-20 23:16:19 +02:00
David Pierron
44c976948d bug correction in the JTACautolase LUA config 2020-10-20 23:02:46 +02:00
David Pierron
41d5020467 bug correction - when running a new campaing
without accessing the plugin setup screen
2020-10-20 22:25:39 +02:00
David Pierron
1bd26005f2 Merge remote-tracking branch 'upstream/develop' into new-plugin-system 2020-10-20 22:10:19 +02:00
David Pierron
b1840ce2ca Merge 'upstream/develop' into new-plugin-system 2020-10-20 22:10:08 +02:00
C. Perreau
769246c55d Merge pull request #224 from justin-lovell/feature/remote-hosting-flexibility
Flexible Dedicated Hosting Options for Mission Files
2020-10-20 22:04:37 +02:00
Khopa
24394d4d00 Faction rework wip. 2020-10-20 20:58:39 +02:00
Justin Lovell
cab5825b72 Flexible Dedicated Hosting Options
* Fixed minor errors on the original LUA scripting
* Refactored code to be self-contained to a function
* Changed the search logic to use an environment variable first, then
  fallback into other search options
2020-10-20 21:49:32 +11:00
Dan Albert
84beb2dfe5 Remove dead code. 2020-10-20 00:15:22 -07:00
Dan Albert
f8ac39fb82 Fix min/max inversion in wind setting. 2020-10-20 00:10:37 -07:00
Dan Albert
eb69d01067 Add distance and ground speed to the kneeboard. 2020-10-19 23:25:46 -07:00
Dan Albert
1c4f255c7f Add waypoint departure time to the kneeboard. 2020-10-19 22:44:54 -07:00
Dan Albert
4125f6ec06 Don't scale waypoint info text size when zooming. 2020-10-19 22:26:31 -07:00
Dan Albert
5023e0d30f Fix disabled delete button in package UI.
The selection changed handler isn't called for the initial selection.
2020-10-19 21:34:17 -07:00
Dan Albert
f65595c626 Automatically select newly created packages. 2020-10-19 21:17:24 -07:00
Dan Albert
916d1eec96 Limit flight size to available aircraft. 2020-10-19 20:46:18 -07:00
Dan Albert
c2d615315e Add client slot selection to new flight window. 2020-10-19 20:29:55 -07:00
Dan Albert
aa96ce7134 Fix cancel of new package. 2020-10-19 01:21:49 -07:00
Dan Albert
01f83e8451 Place CAP racetracks more defensively.
Ensure that we're never putting a CAP race track within 20 nmi of an
enemy airfield (aside from the target airfield, if the target is
hostile).
2020-10-19 00:12:36 -07:00
David Pierron
ed92e9afb9 changed the system to make use of JSON files 2020-10-18 18:23:31 +02:00
Dan Albert
064890c0a2 Mark the F-16 as SEAD capable.
Latest OB update has HARMs for the F-16.
2020-10-17 16:47:41 -07:00
Dan Albert
8617f48fc2 Sort air unit purchases properly, and by name.
Previously we were sorting by task first and price second. Much easier
to find things if they're sorted by name (although longer term we
should make the sort option selectable).
2020-10-17 16:46:54 -07:00
Dan Albert
2269cf0f08 Show objective value per turn in the tooltip. 2020-10-17 16:11:28 -07:00
Dan Albert
3d41eb1ab4 Clean up CAP types.
Stop using "CAP". Use BARCAP or TARCAP instead.

TARCAP no longer allowed anywhere but front lines, since that's all we
have mission planning for right now. Later will add TARCAP and BARCAP
for all objective types with different timing profiles.

Part two of the fix for
https://github.com/Khopa/dcs_liberation/issues/210.
2020-10-17 14:32:09 -07:00
Dan Albert
cace523aa8 Avoid crash for custom/empty flight plans.
Fixes https://github.com/Khopa/dcs_liberation/issues/210
2020-10-17 14:10:54 -07:00
Dan Albert
002f55dc04 Update the map from the main window.
This guarantees that we update the map *after* updating the model that
the map uses to draw flight plans. Without this, after creating a new
game we'd redraw the previous game's flight plans because the model
hadn't been updated by the time the map was.

Fixes https://github.com/Khopa/dcs_liberation/issues/212
2020-10-17 13:33:28 -07:00
Dan Albert
49b6951ac3 Generate weather conditions at turn start.
Weather and exact time of day information is helpful during mission
planning, so generate it at the start of the turn rather than at
takeoff time.

Another advantage aside from planning is that we can now use the wind
information to set carrier headings and takeoff runways appropriately.
2020-10-16 18:25:25 -07:00
Dan Albert
7aa17e5ad6 Fix package/flight selection signals.
Qt helpfully converts None to 0 for us, so use -1 instead of None.
2020-10-16 14:12:27 -07:00
Dan Albert
9db41270f3 Add red ATO cheat option, show red flight plans. 2020-10-16 13:43:01 -07:00
Dan Albert
e4852c74ab Remove completed TODOs/dead code. 2020-10-16 13:00:59 -07:00
Dan Albert
613f84aa3c Add aircraft-specific speed estimates. 2020-10-16 03:10:14 -07:00
Dan Albert
2814876976 Fix A-10C II radio data.
These are two different planes.
2020-10-16 03:09:21 -07:00
Dan Albert
69bf3999aa Fix new package double flight delete.
This button was connected twice.
2020-10-16 03:08:48 -07:00
Dan Albert
2fa3b26119 Improve speed estimations.
Reasonable ground speed depends a lot on altitude, so plumb that
information through to the speed estimator.

Also adds calculations for ground speed based on desired mach. I don't
know if DCS is using the same formulas, but we should at least be
pretty close.
2020-10-16 02:11:50 -07:00
Dan Albert
5a027c552e Point players to the kneeboard from the briefing. 2020-10-15 21:43:41 -07:00
Dan Albert
9efecf9514 Fix inventory handling for new packages. 2020-10-15 21:33:04 -07:00
Dan Albert
8b87c43869 Warn the player about misconfigured TOTs. 2020-10-15 21:04:22 -07:00
Dan Albert
f7fec834e6 Add a button to automatically set the package TOT. 2020-10-15 20:23:57 -07:00
Dan Albert
de43a1215c Update departure time when TOT is changed.
Fixes https://github.com/Khopa/dcs_liberation/issues/207
2020-10-15 18:03:03 -07:00
Dan Albert
8dc531bb7f Make the departure time non-editable.
Fixes part 2 of https://github.com/Khopa/dcs_liberation/issues/207.
2020-10-15 17:57:25 -07:00
Khopa
da2584d7ee Updated pydcs version 2020-10-15 20:58:06 +02:00
David Pierron
373924a959 small changes to veaf plugin 2020-10-15 16:21:39 +02:00
Khopa
f75032bd79 Put 2.2.0-preview as version string 2020-10-15 12:17:33 +02:00
Khopa
4203dc5d41 Prepared changelog first draft for version 2.2.0 2020-10-14 23:34:09 +02:00
Khopa
9fe1f8ff90 Now use the new sea map to place boats and offshore buildings. 2020-10-14 23:30:33 +02:00
Khopa
bc825f760d Added sea zones for each map, and display it in polygon map mode. 2020-10-14 23:21:16 +02:00
David Pierron
191199d9de Merge remote-tracking branch 'upstream/develop' into new-plugin-system 2020-10-14 09:01:37 +02:00
Khopa
883a66a792 Fix : Added missing channel.json file 2020-10-13 00:35:27 +02:00
Khopa
411e71b9a2 Added setting to display culling distance 2020-10-12 20:57:32 +02:00
David Pierron
5463505787 added documentation for plugins system 2020-10-12 20:01:27 +02:00
David Pierron
ec6fc076de multiple changes
- load plugins when loading a game
- moved plugins scripts to resources/plugins (for pyinstaller)
- removed vanilla JTAC and JTAC_smoke options and settings GUI
- call JtacAutolasePlugin in armor.py
- made a dictionary of INSTALLED_PLUGINS
- removed NIOD from the VEAF plugin
2020-10-12 19:49:39 +02:00
Khopa
5a245bf362 Fixed crash in polygon display mode for Nevada map. 2020-10-12 18:53:15 +02:00
Khopa
d95912322c Added Polygon drawing mode for map background 2020-10-12 18:48:50 +02:00
Khopa
9c58e73b39 Added more display options for SAMS 2020-10-12 18:12:45 +02:00
David Pierron
3c4ccd7d57 Merge remote-tracking branch 'upstream/develop' into new-plugin-system 2020-10-12 17:29:54 +02:00
David Pierron
d22943d755 added a customizable plugin system
- the base LUA functionality has been implemented as a mandatory plugin
- the jtacautolase functionality has been implemented as a plugin
- added a VEAF framework plugin

The plugins have GUI elements in the Settings window.
2020-10-12 17:27:13 +02:00
Dan Albert
974b6590d8 Estimate TOTs for packages.
We estimate the longest possible time from mission start to TOT for
all flights in a package and use that to set the TOT (plus any delay
used to stagger flights). This both cuts down on loiter time for
shorter flights and ensures that long flights will make it to the
target in time.

This is also used to compute the start time for the AI, so the
explicit delay option is no longer needed.
2020-10-12 01:00:47 -07:00
Dan Albert
d414c00b74 Make waypoint info more readable. 2020-10-10 16:52:59 -07:00
Khopa
7b79d183eb Changelog 2.1.5 did not make it on this branch somehow 2020-10-11 00:48:13 +02:00
Dan Albert
edd56cb407 Fix bad import of THEMES. 2020-10-10 14:11:44 -07:00
Dan Albert
c777204f50 Draw waypoint information on the map. 2020-10-10 14:09:10 -07:00
Dan Albert
55f12f20c1 Fix missing default parameter. 2020-10-10 14:05:25 -07:00
Dan Albert
1ac062653d Make the starting budget text editable. 2020-10-10 13:00:59 -07:00
Dan Albert
b0a176a22c Make "all" the default flight path display option. 2020-10-10 12:43:51 -07:00
Khopa
f4b07cb518 New game wizard : added to slider to choose starting money 2020-10-10 16:21:41 +02:00
Khopa
a0ff78a810 Merge branch 'develop' of https://github.com/khopa/dcs_liberation into develop
 Conflicts:
	qt_ui/widgets/map/QLiberationMap.py
2020-10-10 15:46:23 +02:00
Khopa
f22391855b Fixed SAM threat radius scale, added detection range, changed UI colors for SAMS range. 2020-10-10 15:43:58 +02:00
Dan Albert
1fa18447e1 Show player slots in the overview. 2020-10-09 20:07:54 -07:00
Dan Albert
2d8c8c63c9 Improve flight path display options.
Adds an option show only selected flight, and also changes the show
all option to highlight the selected flight.
2020-10-09 16:49:32 -07:00
Dan Albert
31d5e3151b Refactor display rules. 2020-10-09 14:54:58 -07:00
David Pierron
c77bfe9da2 plugin base : inject mission configuration data 2020-10-09 21:25:21 +02:00
David Pierron
9a7dfc55e3 Merge remote-tracking branch 'upstream/develop' into develop 2020-10-09 21:03:12 +02:00
C. Perreau
63bc3bd46e Merge pull request #193 from DanAlbert/tot
Plan waypoint TOTs.
2020-10-09 13:25:08 +02:00
Dan Albert
b5e5a3b2da Plan waypoint TOTs.
Also fixes the CAP racetracks so the AI actually stays on station.

Waypoint TOT assignment happens at mission generation time for the
sake of the UI. It's a bit messy since we have the late-initialized
field in FlightWaypoint, but on the other hand we don't have to reset
every extant waypoint whenever the player adjusts the mission's TOT.

If we want to clean this up a bit more, we could have two distinct
types for waypoints: one for the planning stage and one with the
resolved TOTs. We already do some thing like this with Flight vs
FlightData.

Future improvements:

* Estimate the group's ground speed so we don't need such wide margins
  of error.
* Delay takeoff to cut loiter fuel cost.
* Plan mission TOT based on the aircraft in the package and their
  travel times to the objective.
* Tune target area time prediction. Flights often don't need to travel
  all the way to the target point, and probably won't be doing it
  slowly, so the current planning causes a lot of extra time spent in
  enemy territory.
* Per-flight TOT offsets from the package to allow a sweep to arrive
  before the rest, etc.
2020-10-09 01:08:34 -07:00
Dan Albert
7abe32be5c Fix name of decent waypoint. 2020-10-09 00:26:57 -07:00
Dan Albert
f0279a6866 Ignore the entire logs directory.
We use a rotating log handler, so we generate more than just the one
file.
2020-10-08 22:49:04 -07:00
Dan Albert
5ce942c9a0 Confirm mission start when no client slots exist.
Especially considering the button in this position used to be how
players added client slots, confirm that they in fact want to launch
an AI-only mission before launching, and guide them toward the new UI.
2020-10-07 17:09:57 -07:00
David Pierron
59d6cc7625 Merge remote-tracking branch 'upstream/develop' into develop 2020-10-07 09:21:41 +02:00
Dan Albert
4abf806837 Fix initial frequencies for support aircraft.
Vaicom (a mod that adds voice control for the communications menus)
isn't able to follow the waypoint frequency change that normally sets
the radio channel for the AWACS/tanker flights. Set the group's
frequency correctly to start so it works.
2020-10-06 23:44:11 -07:00
Dan Albert
6bfb8cf2fd Fix double logging.
Calling logging.basicConfig creates a stream handler for the root
logger, and then we were adding our own with a different formatter.
Pass the format string to basicConfig so we don't need to add our own
duplicate stream handler.
2020-10-06 23:31:40 -07:00
Dan Albert
1d7f1082ea Fix logging by deferring campaign data loading.
Logging before we've made it to the logging setup was causing the root
logger to be permanently configured to the default (warning) log
level, so we weren't getting any info or debug logs any more.

Defer the campaign data load until it is needed rather than doing it
at import time. I've also cleaned up a bit so we only load each
campaign once, rather than re-loading the campaign to create the
theater again after the wizard is finished.
2020-10-06 23:26:18 -07:00
Dan Albert
1c4aec83cb Clean up start-up logging.
Most of this wasn't helpful. What was is now logging instead of print
so it can be configured.
2020-10-06 22:24:47 -07:00
Dan Albert
e537396fec Stagger package start times.
Avoids crowding the taxiways, and adds some life to the end of the
mission.

Later on, this will happen more naturally because we can delay
takeoffs to align with the package's DTOT.
2020-10-06 21:51:23 -07:00
Dan Albert
944748a0ac Don't throw away exception info on save/load.
Makes it much easier to determine what we did that broke save game
compatibility.
2020-10-06 21:50:44 -07:00
Dan Albert
023925d741 Set preferred mission types for aircraft. 2020-10-06 17:30:20 -07:00
Dan Albert
93db1254ec Improve insufficient aircraft message.
Specify only the tasks which were unfulfilled so the player knows what
to buy.
2020-10-06 17:29:42 -07:00
Dan Albert
e0725ff139 Add escort mission planning to the UI. 2020-10-06 17:29:19 -07:00
Dan Albert
9e96aee89f Add default escort loadouts. 2020-10-06 17:29:11 -07:00
Dan Albert
db6b660270 Fix mypy issues in all modules except qt_ui. 2020-10-06 17:24:08 -07:00
Dan Albert
1808e5bccf Add initial mypy.ini. 2020-10-06 17:01:37 -07:00
Dan Albert
5f1601a2da Remove the userdata package. 2020-10-06 17:01:37 -07:00
Khopa
60ce6658ad Campaigns are sorted by terrain in new game wizard 2020-10-07 01:12:16 +02:00
Khopa
e664652cc5 Now properly merged operation.py 2020-10-07 01:07:56 +02:00
Khopa
ca48a42701 Revert "Reverted automerge errors."
This reverts commit 00ea8ac4
2020-10-07 00:59:18 +02:00
Khopa
00ea8ac4e1 Reverted automerge errors. 2020-10-07 00:56:42 +02:00
Khopa
de5238e89a Fixed issue in Normandy small campaign 2020-10-07 00:27:43 +02:00
Khopa
3bb1327a65 Manuall reintroduced inverted campaign config in json campaign files 2020-10-07 00:25:02 +02:00
Khopa
71f77dd8fb Added moddable campaigns through json files. 2020-10-07 00:09:11 +02:00
C. Perreau
9101dae38a Merge pull request #184 from DanAlbert/waypoint-planning
Improve automated mission planning.
2020-10-06 22:04:58 +02:00
C. Perreau
1f18bf2bd8 Merge branch 'develop' into waypoint-planning 2020-10-06 22:01:44 +02: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
Dan Albert
f040804d02 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 02:17:14 -07:00
Khopa
e27625556c Exported existing campaigns to json objects. 2020-10-04 22:09:57 +02:00
Dan Albert
b13711ddef Update the waypoint builder UI.
Changing targets doesn't make sense now that flights belong to a
package. Change all the "generate" dialogs to simply confirm dialogs
to make sure the user is okay with us clobbering the flight plan.
2020-10-04 12:24:31 -07:00
Dan Albert
6ce82be46b Set up split/join points. 2020-10-04 12:24:31 -07:00
Dan Albert
56a5864600 Generate common ingress/egress points.
This still isn't very good because it doesn't work well for anything
but the automatically planned package.

Instead, should be a part of the Package itself, generated the first
time it is needed, and resettable by the user.
2020-10-04 12:24:31 -07:00
Dan Albert
07cbaa3e70 Plan escort flights.
TODO: UI
2020-10-04 12:24:31 -07:00
Dan Albert
2aecea88b0 Orient CAP tracks toward the enemy.
Pointing the race track 90 degrees away from where the enemy is
expected means the radar can't see much. CAP flights normally fly
*toward* the expected direction of contact and alternate approaching
and retreating legs with their wingman.
2020-10-04 12:24:31 -07:00
Dan Albert
582c43fb6c Generate CAP missions in useful locations.
CAP missions should be between the protected location and the nearest
threat. Find the closest enemy airfield and ensure that the CAP race
track is between it and the protected location.
2020-10-04 12:24:26 -07:00
Dan Albert
cc7c2cc707 Refactor flight plan generation. 2020-10-04 12:24:26 -07:00
Dan Albert
8b717c4f4c Replace doctrine dict with a real type. 2020-10-04 12:24:26 -07:00
Dan Albert
aa309af015 Redraw flight plans when they change. 2020-10-04 12:24:26 -07:00
Dan Albert
1e041b6249 Perform coalition-wide mission planning.
Mission planning on a per-control point basis lacked the context it
needed to make good decisions, and the ability to make larger missions
that pulled aircraft from multiple airfields.

The per-CP planners have been replaced in favor of a global planner
per coalition. The planner generates a list of potential missions in
order of priority and then allocates aircraft to the proposed flights
until no missions remain.

Mission planning behavior has changed:

* CAP flights will now only be generated for airfields within a
  predefined threat range of an enemy airfield.
* CAS, SEAD, and strike missions get escorts. Strike missions get a
  SEAD flight.
* CAS, SEAD, and strike missions will not be planned unless
  they have an escort available.
* Missions may originate from multiple airfields.

There's more to do:

* The range limitations imposed on the mission planner should take
  aircraft range limitations into account.
* Air superiority aircraft like the F-15 should be preferred for CAP
  over multi-role aircraft like the F/A-18 since otherwise we run the
  risk of running out of ground attack capable aircraft even though
  there are still unused aircraft.
* Mission priorities may need tuning.
* Target areas could be analyzed for potential threats, allowing
  escort flights to be optional or omitted if there is no threat to
  defend against. For example, late game a SEAD flight for a strike
  mission probably is not necessary.
* SAM threat should be judged by how close the extent of the SAM's
  range is to friendly locations, not the distance to the site itself.
  An SA-10 30 nm away is more threatening than an SA-6 25 nm away.
* Much of the planning behavior should be factored out into the
  coalition's doctrine.

But as-is this is an improvement over the existing behavior, so those
things can be follow ups.

The potential regression in behavior here is that we're no longer
planning multiple cycles of missions. Each objective will get one CAP.
I think this fits better with the turn cycle of the game, as a CAP
flight should be able to remain on station for the duration of the
turn (especially with refueling).

Note that this does break save compatibility as the old planner was a
part of the game object, and since that class is now gone it can't be
unpickled.
2020-10-04 12:24:26 -07:00
Khopa
1f240b02f4 Fix aircrafts landing point 2020-10-04 14:27:13 +02:00
Khopa
6317f376b7 EPLRS typo fixed 2020-10-04 14:11:28 +02:00
Khopa
5ecf9aeed8 EPLRS implemented for base defense unit. 2020-10-03 18:32:11 +02:00
Khopa
db36a76c2c Fixed eplrs for frontline ground units 2020-10-03 17:35:06 +02:00
Khopa
3df8fb5fe9 Merge branch 'develop' into develop_mission_planner 2020-10-03 17:06:07 +02:00
Khopa
7dd3367203 Version number for release 2.1.4 2020-10-03 16:50:56 +02:00
Khopa
e72c82521a Forgot the changelog for 2.1.4 2020-10-03 16:45:21 +02:00
C. Perreau
aca415db23 Merge pull request #179 from Khopa/develop
Release 2.1.4
2020-10-03 16:33:26 +02:00
Khopa
5ba2e8a7a1 Fixed error after merge 2020-10-03 16:18:32 +02:00
Khopa
07d4b126f5 Enable EPLRS for ground units that can use it. 2020-10-03 16:18:12 +02:00
David Pierron
98b2d8b3b9 typo in the generate_initial_units function name 2020-10-02 13:15:40 -07:00
David Pierron
f8ef5db5a3 bug when continuing an old campaign save 2020-10-02 13:15:40 -07:00
C. Perreau
6e14ec3227 Merge pull request #147 from DanAlbert/ato
Replace mission planning UI.
2020-10-02 13:31:45 +02:00
C. Perreau
028292a023 Merge branch 'develop_mission_planner' into ato 2020-10-02 13:26:21 +02:00
David Pierron
a38f2e36a2 Merge remote-tracking branch 'upstream/develop' into develop 2020-10-02 09:25:18 +02:00
Dan Albert
a44cbe5972 Tone down failure message for missing plugin file.
This looked an awful lot like an error, but it's the common case.
2020-10-01 23:57:53 -07:00
C. Perreau
2066a2e9bc Merge pull request #167 from Khopa/develop
Release 2.1.3
2020-10-01 23:23:06 +02:00
Khopa
f381bf85a4 Fixed JTAC script not working after changes made to lua files 2020-10-01 23:17:32 +02:00
Khopa
44dcdcc8bb Changelog update 2020-10-01 23:17:08 +02:00
Khopa
01220800f3 Fixed Viggen icon 2020-10-01 22:47:58 +02:00
Khopa
8ecb4cdcf4 Added more ground vehicles icons. 2020-10-01 21:09:05 +02:00
Khopa
8402d108c0 Pydcs location 2020-10-01 20:22:11 +02:00
Khopa
75af2d468e Fix A-10C_II icon 2020-10-01 19:38:43 +02:00
Khopa
72ce37f008 Added custom payloads for A-10C II 2020-10-01 19:33:46 +02:00
Khopa
e48e884286 Added A-10C_2. Changed bluefor factions' country to "Combined Joint Task Forces Blue" instead of "USA" (support more units) 2020-10-01 19:20:25 +02:00
Khopa
f9d5c1f8de Update pydcs location 2020-10-01 19:07:06 +02:00
Khopa
0873dcab0a Gitmodule points to pydcs 2020-10-01 19:06:38 +02:00
C. Perreau
a1343c2849 Merge pull request #166 from DanAlbert/build-canaries
Build and archive binaries on push/PR.
2020-10-01 12:53:35 +02:00
Dan Albert
f732cc54d0 Build and archive binaries on push/PR.
Not building a full release, but this makes it easier to test someone
else's PR, or for players to get at pre-release builds.
2020-09-30 21:03:12 -07:00
David Pierron
473a7d5fa4 added a 'mkrelease' config for VS.Code 2020-09-30 19:47:02 -07:00
David Pierron
8054a0b62f removed useless link.cmd.sample file 2020-09-30 19:47:02 -07:00
Dan Albert
7f9cba5d37 Fix more None ATC bugs.
Fixes https://github.com/Khopa/dcs_liberation/issues/164
2020-09-30 19:20:50 -07:00
David Pierron
5807fbf896 added a 'mkrelease' config for VS.Code 2020-09-30 13:35:09 +02:00
David Pierron
2c1dc6a18d removed useless link.cmd.sample file 2020-09-30 13:34:41 +02:00
C. Perreau
f68e6387e6 Merge pull request #161 from VEAF/make-mission-portable
Make mission portable
2020-09-29 23:50:34 +02:00
Khopa
f032001bee Limit number of aircraft that can be bought at a Control Point. 2020-09-29 23:47:57 +02:00
David Pierron
3bae591c04 corrected and enhanced mission portability 2020-09-29 20:46:22 +02:00
Khopa
18a1f0af94 Added credits to new contributor to about dialog. 2020-09-29 20:03:07 +02:00
David Pierron
afbd4a4716 Make mission portable
use inline json.lua and write to %LIBERATION_EXPORT_DIR%, %TEMP%
or the DCS working directory
2020-09-29 17:27:35 +02:00
David Pierron
a98da14c6f Merge pull request #1 from Khopa/master
Merge from base
2020-09-29 15:14:55 +02:00
Khopa
a2a70213a7 Update pydcs location 2020-09-28 00:55:02 +02:00
Khopa
8b0f877041 Verrsion string updated to 2.1.2 2020-09-28 00:42:27 +02:00
Khopa
bf7ad4cad2 Merge remote-tracking branch 'khopa/master' into develop 2020-09-28 00:27:25 +02:00
C. Perreau
66b659c0af Merge pull request #152 from VEAF/introduced-scripts-plugins
Introduced LUA scripts plugins
2020-09-28 00:26:26 +02:00
Khopa
8709ea948f Merge remote-tracking branch 'khopa/master' into develop 2020-09-28 00:21:33 +02:00
Khopa
7236c10403 Changelog update 2020-09-28 00:21:12 +02:00
C. Perreau
dde703ec41 Merge pull request #154 from VEAF/add-tanker-type-to-tanker-name
add tanker type to tanker name
2020-09-28 00:06:49 +02:00
Khopa
aa2e9b123c Fix : AI is not planning flights for Tornado. 2020-09-28 00:03:01 +02:00
Dan Albert
a3c06ce6e0 Limit task type combo box to valid mission types. 2020-09-27 13:44:58 -07:00
Dan Albert
0e1dfb8ccb Implement CAP and CAS for front lines. 2020-09-27 13:44:58 -07:00
Dan Albert
8a4a81a008 Remove unused frontline code. 2020-09-27 13:44:58 -07:00
Dan Albert
ff083942e8 Replace mission planning UI.
Mission planning has been completely redone. Missions are now planned
by right clicking the target area and choosing "New package".

A package can include multiple flights for the same objective. Right
now the automatic flight planner is only fragging single-flight
packages in the same manner that it used to, but that can be improved
now.

The air tasking order (ATO) is now the left bar of the main UI. This
shows every fragged package, and the flights in the selected package.
The info bar that was previously on the left is now a smaller bar at
the bottom of the screen. The old "Mission Planning" button is now
just the "Take Off" button.

The flight plan display no longer shows enemy flight plans. That could
be re-added if needed, probably with a difficulty/cheat option.

Aircraft inventories have been disassociated from the Planner class.
Aircraft inventories are now stored globally in the Game object.

Save games made prior to this update will not be compatible do to the
changes in how aircraft inventories and planned flights are stored.
2020-09-27 13:44:58 -07:00
Khopa
737e04d09e Merge branch 'master' into develop 2020-09-27 19:16:34 +02:00
C. Perreau
0fe59efd72 Merge pull request #157 from DanAlbert/fix-none
Fix None dereference.
2020-09-27 19:10:31 +02:00
C. Perreau
ddb50e6254 Merge pull request #151 from VEAF/UHF-intraflight-frequency-for-Player-and-Clients-A-10C
UHF Intraflight Frequency for Player and Clients A-10C
2020-09-27 19:09:33 +02:00
Dan Albert
72e6ae4186 Fix None dereference. 2020-09-26 16:32:02 -07:00
David Pierron
4d510f643a add tanker type to tanker name 2020-09-25 17:11:17 +02:00
David Pierron
66f607b5e6 added a comment that links to my forum post 2020-09-25 11:33:07 +02:00
David Pierron
1a125c62e7 added sample __plugins.lst file 2020-09-25 11:21:23 +02:00
David Pierron
84da44a27b Introduced LUA scripts plugins
In order to be able to customize the scripts that can be injected in the
mission, a __plugin.lst file is read and the scripts mentionned in this
file are injected (through DoScriptFile and not DoScript).

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

* AJS37
* AV-8B
* JF-17

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

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

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

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

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

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

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

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

    PP1
    PP2
    PP3
    PP4

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

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

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

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

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

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

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

There's more that could be done:

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

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

Also missing is any checking that the contents do not go beyond the
bounds of the page. We could add this if needed. For now the page has
enough room for about a dozen waypoints, which is quite a bit more
than most missions need.
2020-08-31 13:01:05 -07:00
Dan Albert
66af6be063 Update pydcs. 2020-08-31 13:01:05 -07:00
C. Perreau
69e1d2779d Merge pull request #112 from DanAlbert/gitignore
Update gitignore.
2020-08-31 12:04:37 +02:00
Dan Albert
001752a81e Update gitignore. 2020-08-29 19:58:03 -07:00
C. Perreau
1000041bce Merge pull request #109 from DanAlbert/non-modal-mission-planning
Make the mission planning window non-modal.
2020-08-29 18:06:57 +02:00
Dan Albert
d50e791c30 Make the mission planning window non-modal.
Doesn't appear to be any need for this to be modal. Making it
non-modal allows interacting with the map during planning.
2020-08-28 13:42:38 -07:00
Khopa
7817d59989 Fixed campaign sometimes not starting when the user does not explicitly re-select a campaign and just kee p the default one.. 2020-08-27 23:47:00 +02:00
Khopa
139c4c1dd8 Fixed crash on mission generation when clearing slots. 2020-08-27 23:46:10 +02:00
Khopa
75bb6941d3 Added version string in the window title 2020-08-24 22:49:55 +02:00
Khopa
e92fb38271 Fixed Sweden 1990 faction not working 2020-08-24 22:32:37 +02:00
C. Perreau
e4eeef8f99 Merge pull request #102 from parithon/menukbsupport
Add keyboard support to the menu system
2020-08-24 21:42:27 +02:00
Khopa
21c355bc9f Fix small issue with ground object menu when the ground object is empty. 2020-08-24 21:34:24 +02:00
Anthony Conrad
04c878f57c Added keyboard support to the menu system 2020-08-23 22:25:16 -07:00
Anthony Conrad
ef23ce58d1 Added keyboard support for the menu system 2020-08-23 21:54:12 -07:00
C. Perreau
d213fa1b91 Merge pull request #98 from Khopa/develop
2.1.1-alpha-2
2020-08-23 18:26:14 +02:00
Khopa
4b2804427e Possible to replace SAM, possible to see building recon images on ground site. 2020-08-23 18:25:23 +02:00
Khopa
d707a59a71 Added thumbnail for strike targets / buildings. 2020-08-23 18:02:36 +02:00
Khopa
923358b364 Cheat menu : added buttons to remove money 2020-08-23 15:29:11 +02:00
C. Perreau
1d0c0ac19c Merge pull request #96 from parithon/develop
Add a Github Action and Installer
2020-08-23 14:14:30 +02:00
C. Perreau
7c97ecddac Merge branch 'develop' into develop 2020-08-23 14:08:09 +02:00
Khopa
bcae51cc92 Added possibility to repair SAM sites (WIP) 2020-08-23 13:43:33 +02:00
Anthony Conrad
18896a69cf Added PyDCS as a submodule 2020-08-22 20:00:07 -07:00
Anthony Conrad
4c310d268d Added an installer script using Inno Setup and Github Action to create releases when a tag is pushed. 2020-08-22 18:44:11 -07:00
Khopa
70babd9c32 Added Incirlik airbase in the Inherent Resolve campaign 2020-08-22 03:53:51 +02:00
Khopa
15d0edce2e Added MQ_9 Reaper as a recruitable CAS unit to usa 2005 2020-08-22 03:52:54 +02:00
Khopa
d7ab1774ac AI was not using AH_1W 2020-08-22 03:51:53 +02:00
Khopa
42d9607b0d Syria 2011 update 2020-08-22 03:51:18 +02:00
Khopa
bf82474fd7 Fixed Viper TGP bug. 2020-08-22 02:59:24 +02:00
Khopa
64211275cc Changelog update 2020-08-21 13:14:36 +02:00
Khopa
cf8060f7ff AH-64 better payloads management. 2020-08-21 13:14:03 +02:00
Khopa
cee1d01d9a Payloads fix for AH-64A. And AH-64D has payloads for all missions type. 2020-08-21 12:50:37 +02:00
Khopa
536d763a8e usa 1990 tweaks 2020-08-21 12:09:59 +02:00
Khopa
6d27b6ce41 Fixed position of Tarawa on Syrian Civil War campaign.(A new save is required though) 2020-08-21 12:03:50 +02:00
Khopa
0d05655e94 Merge branches 'develop' and 'master' of https://github.com/khopa/dcs_liberation into develop 2020-08-21 11:49:39 +02:00
C. Perreau
9cf697d72c Merge pull request #88 from Steveveepee/Steveveepee-patch-1
Update QWaitingForMissionResultWindow.py
2020-08-21 11:34:49 +02:00
C. Perreau
f0f818c524 Merge pull request #89 from Steveveepee/Steveveepee-patch-2
Update aircraft.py
2020-08-21 11:34:30 +02:00
Steve Pole
d2a3448819 Update aircraft.py
corrected spelling of aircrafts to aircraft
2020-08-21 17:07:10 +10:00
Steve Pole
1a25b15bce Update QWaitingForMissionResultWindow.py
corrected spelling of aircrafts to aircraft
2020-08-21 17:05:50 +10:00
Khopa
83808a63ed Fix inverted Syria map starting point 2020-08-21 03:06:19 +02:00
C. Perreau
515d1d0dee Merge pull request #83 from Khopa/develop
Version 2.1.0
2020-08-21 02:12:07 +02:00
Khopa
2d47df96b6 Better performances for Golan heights startup with only one frontline. 2020-08-21 01:13:20 +02:00
Khopa
ad4d71316f Israel 2000 update 2020-08-21 01:13:00 +02:00
Khopa
40932c9e84 More Performance for Golan heights startup with only one frontline. 2020-08-21 01:07:25 +02:00
Khopa
59bee5f23e Changelog update 2020-08-21 01:06:38 +02:00
Khopa
e6f00febea Credits 2020-08-21 01:06:00 +02:00
Khopa
6ea694bc7e Added more Syria campaigns 2020-08-21 00:41:34 +02:00
Khopa
8f7a8edde4 Added custom payloads for AH-1W 2020-08-21 00:39:31 +02:00
Khopa
11c7107152 Fixed assymetric default F16 payload for CAS 2020-08-21 00:37:04 +02:00
Khopa
d18a9f376f Added Syria map with GolanHeights setup.
Added a setting to forbid navy units on some control points.
2020-08-20 22:15:54 +02:00
Khopa
6ba95d8c70 Syria terrain update 2020-08-20 22:14:59 +02:00
Khopa
1c00418d64 Syria polygon, terrain and background image added. 2020-08-20 21:04:20 +02:00
Khopa
602e7fb530 Added syria map icon 2020-08-20 19:12:30 +02:00
Khopa
e6a0a1d4a4 Added payloads for drones. Possibility to setup a different JTAC unit for some factions. China using Wingloong instead of Reaper as JTAC unit. 2020-08-20 19:11:51 +02:00
Khopa
136cf143bd Added payloads for drones. Possibility to setup a different JTAC unit for some factions. China using Wingloong instead of Reaper as JTAC unit. 2020-08-20 16:08:52 +02:00
Khopa
5afa2a23f6 Version String managed in a single place. And shown in about Window 2020-08-20 16:02:16 +02:00
Khopa
513c81b508 Changelog update 2020-08-20 14:30:52 +02:00
Khopa
45af66e000 Fixed T+ not updating in the list of flight shown in the mission planner. 2020-08-20 14:22:10 +02:00
Khopa
b38f6c39e4 Changelog update with TODO 2020-08-20 14:21:06 +02:00
Khopa
036874fbf0 Preparing version 2.1.0 2020-08-20 13:12:26 +02:00
Khopa
9f3b49a7f5 Changelog update 2020-08-20 13:11:35 +02:00
Khopa
9c35156db9 Fix : First unit of base defenses group was not controllable with Combined Arms. 2020-08-20 12:54:03 +02:00
Khopa
ab2edc6e59 Added Syrian faction 1982. + Mig-25PD better support. 2020-08-20 12:53:34 +02:00
Khopa
9af278217c More start date. Some small factions changes, F-16A support. 2020-08-20 12:39:18 +02:00
Khopa
13dbc9c0fe Added error message in mission when state file can not be written. 2020-08-20 01:09:10 +02:00
Khopa
14f7fbe794 Added new exclusions zones in caucasus map 2020-08-20 00:18:07 +02:00
Khopa
a60e6aa860 Reworked campaign selection wizard. Added two small scale camapaigns on PG map 2020-08-20 00:17:10 +02:00
Khopa
220e72322c Added new factions in anticipation for new syria map. 2020-08-18 21:08:26 +02:00
Khopa
1a100dc54b Merge branches 'develop' and 'master' of https://github.com/khopa/dcs_liberation into develop 2020-08-18 17:53:27 +02:00
C. Perreau
04b95c986d Update README.md 2020-08-17 15:22:39 +02:00
C. Perreau
8f6bb90945 Update README.md 2020-08-17 15:21:57 +02:00
C. Perreau
50e78bd069 Update README.md 2020-08-17 15:20:14 +02:00
C. Perreau
ce60e1a8f8 Update README.md 2020-08-17 15:19:48 +02:00
C. Perreau
fb7ed19f7a Merge pull request #72 from root0fall/add_budget_update
add budget preview to purchase dialogue
2020-08-17 11:46:23 +02:00
root0fall
970888b5ca add budget preview to purchase dialogue 2020-08-17 12:37:02 +08:00
C. Perreau
8c934654ba Update README.md 2020-08-16 16:44:38 +02:00
C. Perreau
ad3dc70cfd Merge pull request #62 from Khopa/develop
Develop
2020-08-16 16:42:21 +02:00
Khopa
ae5949bc7f Added Ka-50 to Russia 1990 2020-08-16 16:32:59 +02:00
Khopa
c7c563e68c Requirements update to new pydcs version 2020-08-16 16:31:06 +02:00
C. Perreau
3dac0af6c4 Merge pull request #61 from Khopa/develop
2.0.11
2020-08-16 15:27:25 +02:00
Khopa
f9ce6966bb Changelog update 2020-08-16 15:26:20 +02:00
C. Perreau
f61e23153b Merge pull request #60 from Khopa/develop
Develop
2020-08-16 15:22:10 +02:00
Khopa
ff4f008a63 Merge branches 'develop' and 'master' of https://github.com/khopa/dcs_liberation into develop 2020-08-16 15:21:22 +02:00
C. Perreau
cbcf8a0a90 Merge pull request #59 from bwRavencl/fix_uk_typo
Fixed typo "United Kingdown" -> "United Kingdom"
2020-08-16 15:20:59 +02:00
Khopa
647e62059f Changelog update 2020-08-16 15:10:55 +02:00
Khopa
6e3ef24e3a Changelog update 2020-08-16 15:09:44 +02:00
Khopa
2559b27a6f Restrict afterburner for AI units. 2020-08-16 15:08:27 +02:00
Khopa
421e2508d4 JTAC invisble and immortal commands update. 2020-08-16 15:01:34 +02:00
Matteo Hausner
c9f8a93813 Fixed typo "United Kingdown" -> "United Kingdom" 2020-08-16 14:07:37 +02:00
Khopa
126849cf9a JTAC message will be displayed for 25 seconds instead of just 10. 2020-08-16 13:52:42 +02:00
Khopa
c08768f648 Cleaned up dead code 2020-08-16 13:31:12 +02:00
Khopa
2c07257bf6 Small update to nevada exclusion polygons around nellis AFB. 2020-08-16 13:30:36 +02:00
Khopa
f797bbb97f Fixed JTAC using code above than 1688 that cannot be used in game. 2020-08-16 13:11:28 +02:00
Khopa
a7f3b6e0dc F-15E added for USA 2005 and USA 1990 factions. 2020-08-16 13:09:31 +02:00
Khopa
60732c33c0 Libya mispell issue 2020-08-16 12:53:40 +02:00
Khopa
65b77e241f Changelog update 2020-08-16 02:41:56 +02:00
Khopa
a167b95cec Destroyed units will not remain on airfields. 2020-08-16 02:25:42 +02:00
Khopa
283cfd1ce9 Removed some dead code. 2020-08-16 02:19:52 +02:00
Khopa
e16db60d0f Fix error with JTAC compatibility with old saves 2020-08-16 02:18:30 +02:00
Khopa
6a3b5bbe1d Empty neutral airports from supply 2020-08-16 02:17:43 +02:00
Khopa
339c3f506c changelog update 2020-08-16 01:02:12 +02:00
Khopa
9fade70092 Fixed Tankers TACAN being the same and being different from the one in briefing. 2020-08-16 01:01:01 +02:00
Khopa
e18d84ae5e Fixed issues with libya 2011 faction. 2020-08-15 01:10:30 +02:00
Khopa
fa76e31640 JTAC smoke markers can be disabled 2020-08-14 22:59:45 +02:00
Khopa
2fd4fa25f7 Added JTAC smoke parameter. 2020-08-14 22:39:45 +02:00
Khopa
c27d8e3b16 Updated readme.md file. 2020-08-14 19:45:02 +02:00
Khopa
0841c52a75 Readme update link 2020-08-14 19:28:46 +02:00
Khopa
669bff13c7 Updated Github readme. 2020-08-14 19:24:35 +02:00
Khopa
01ea4fa7a6 Fixed big performance issues in release executable. 2020-08-13 16:53:56 +02:00
Khopa
ef024b5118 Removed unused part of release script 2020-08-13 16:10:37 +02:00
Khopa
a96a107ef9 Added Mi24, Mi28, Mig31, Su30 to Russia. 2020-08-13 14:51:30 +02:00
Khopa
8d3ab2be5d Payloads for Mi-24V && Mi-28 2020-08-13 14:50:36 +02:00
Khopa
6ed407f656 Fixed OH-58D not being used by AI 2020-08-13 14:47:59 +02:00
Khopa
6b3625f0ea Forgot to put jtac in changelog. 2020-08-13 00:36:28 +02:00
C. Perreau
db8e7f0474 Merge pull request #46 from Khopa/develop
Merge develop 2.0.10
2020-08-13 00:02:21 +02:00
Khopa
d6398630df Version number 2020-08-12 23:58:23 +02:00
Khopa
464bfccfb6 C-101 picture fix.
Removed UH60 from PMCs.
2020-08-12 23:57:25 +02:00
Khopa
aa16da837d Fixed error 2020-08-12 22:59:00 +02:00
Khopa
ce4478803a Added new factions, and OH-58D payloads. 2020-08-12 22:57:50 +02:00
Khopa
114239fe8e Default size for info panel & map changed 2020-08-12 18:27:13 +02:00
Khopa
67d96061da Fixed JTAC infos in briefing 2020-08-12 18:26:49 +02:00
Khopa
2d5ad16399 More WW2 factions 2020-08-12 18:26:26 +02:00
Khopa
4c7f79c6f8 Fix bug in mission result window. 2020-08-12 03:31:56 +02:00
Khopa
2a50768db1 JTAC support 2020-08-12 03:13:51 +02:00
Khopa
bff33e6992 Added LHA to Caucasus campaigns. 2020-08-10 18:41:41 +02:00
Khopa
448057a0b9 Disabled interceptors for upcoming release 2020-08-10 18:17:00 +02:00
Khopa
0a47669b14 Possible fix for debriefing updates. 2020-08-10 18:10:10 +02:00
Khopa
b5893ce521 Fix potential error in mission waiting for results window. 2020-08-10 17:56:30 +02:00
Khopa
23537211b0 Fallback point is further away 2020-08-10 17:55:32 +02:00
Khopa
39f25db439 Replaced Sa342L by SA342M in uk 90 factions. (L version doesn't want to attack ennemy.) 2020-08-10 17:49:22 +02:00
Khopa
a551067c72 Restrict Jettison for CAS aircrafts 2020-08-10 17:04:46 +02:00
Khopa
23f4df766c Now possible to open and save game under different names. 2020-08-09 17:31:53 +02:00
Khopa
7354a34f1a Misc UI changes to new game wizard and mission result window. 2020-08-09 16:37:10 +02:00
Khopa
2de48b3918 Fix no lha for some factions 2020-08-09 16:36:21 +02:00
Khopa
e823a7a7b0 Replaced new game wizard picture by my own screenshoots (not sure of the source for the previous ones) 2020-08-09 16:36:00 +02:00
Khopa
ffc1f9d48e New harrier payload, tarawa compatible. 2020-08-09 14:43:53 +02:00
Khopa
bb19bfb925 Improved special case for Su-33 carrier takeoff payload 2020-08-07 23:06:54 +02:00
Khopa
b3a2464249 Fixed issue with IL-78M tanker 2020-08-07 23:03:15 +02:00
Khopa
09718e73a3 Changed campaign names in new game window 2020-08-06 00:37:39 +02:00
Khopa
d5f20377ea Remove weapons fired from debriefing window (not computed anymore) 2020-08-06 00:36:49 +02:00
Khopa
f703d620c2 Changed the Nevada campaign setup 2020-08-06 00:35:55 +02:00
Khopa
5ac680b37d Slight change to base defense position calculations 2020-08-06 00:35:24 +02:00
Khopa
9bf57e99fb Removed Mig-29 from Strike mission 2020-08-06 00:34:13 +02:00
Khopa
b787f7cb11 RTB rules for CAS aircraft changed. Added PP M points for AJS 37 2020-08-06 00:33:37 +02:00
Khopa
7b35965dbf Fix : Awacs frequency in briefing 2020-08-06 00:32:47 +02:00
Khopa
1618c1e677 Awacs use 233Mhz instead of 133Mhz 2020-08-06 00:32:17 +02:00
Khopa
0a5ce108b7 Added preplanned point to briefing 2020-08-06 00:31:50 +02:00
Khopa
99c180441d Updated terrain data for all maps. 2020-08-05 22:01:32 +02:00
Khopa
79a7b0557c Improved Caucasus landmap collision data, added exclusions zones for airports. 2020-08-05 20:13:42 +02:00
Khopa
91cf192d2e Fixed AI aircraft do not start datalink. 2020-08-05 01:04:44 +02:00
Khopa
66d7435ed6 Minor balances changes 2020-08-05 00:58:14 +02:00
Khopa
19ea75b281 Fixed all aircraft spawning in the air after a few missions have been played without restarting DCS Liberation. 2020-08-05 00:58:02 +02:00
Khopa
f0350b7045 New Caucasus campaign in Russia 2020-08-04 01:17:26 +02:00
Khopa
9d4d3d0523 Avoid duplicate groups id. 2020-08-04 01:08:27 +02:00
Khopa
a76962e206 Added new campaign generation settings. Added Helicopter Carrier to China faction. 2020-08-04 00:14:42 +02:00
Khopa
4060039440 Changed tanker frequency range to avoid overlap with Persian Gulf TACAN frequencies. 2020-08-03 21:47:22 +02:00
Khopa
c3a3a428a4 Fix tanker speed, tacan and increased race track length. 2020-08-03 20:34:28 +02:00
Khopa
171e23bd09 Fixed S-300 (SA-10) sites missing a tracking radar. 2020-08-03 19:47:08 +02:00
Khopa
6f4b7e0f1a WIP 2020-08-03 19:43:12 +02:00
Khopa
ccf2cd3425 Factions small changes 2020-08-03 19:42:09 +02:00
Khopa
6ee444efd7 Aircraft should RTB when Bingo.
CAP should RTB when winchester.
2020-08-02 13:59:55 +02:00
Khopa
3e5be909a2 Lua script injected in mission will not listen to weapon fired event anymore. 2020-08-02 02:46:00 +02:00
Khopa
dac78f8f09 changelog update 2020-08-02 02:45:19 +02:00
Khopa
6b91e1b03c Fixed FW-190A8 spawning with bomb rack even when configured for CAP 2020-08-02 02:31:40 +02:00
Khopa
772295fc04 Avoid having Su-33 crashing when taking off from their carrier with a too big payload by removing some fuel. 2020-08-01 18:07:49 +02:00
Khopa
a4e93276b8 Possible to mix factions side. Player will always be blue. 2020-08-01 14:21:34 +02:00
Khopa
456a82acaa Multiple factions fixes 2020-08-01 14:20:37 +02:00
Khopa
f2fb2cb363 Fixed label in strike mission generator 2020-08-01 14:19:46 +02:00
C. Perreau
2a7e5eecd7 Merge pull request #30 from DeusEx010101/add-usa-aggressors-faction
Added USA aggressors aircraft.
2020-07-31 20:35:18 +02:00
Khopa
79502f56a0 Added Australia / Canada 2020-07-31 20:34:27 +02:00
Khopa
12dacfe2d2 Fixed A-20G payloads. 2020-07-30 19:31:38 +02:00
Donnie
d46337d694 Added USA aggressors. TODO. Decide on final aggressors unit list. 2020-07-26 11:27:18 -04:00
Khopa
f897cf745f Multiple WIP changes on UI / Submit manually debriefing. 2020-07-26 12:59:16 +02:00
Khopa
cfa4f7da2e Take off alt waypoint was editable in flight waypoint table. 2020-07-26 00:57:45 +02:00
Khopa
b21272cfab Made waypoint altitude column not editable 2020-07-25 23:07:23 +02:00
Khopa
8ce0520101 QSettings Windows can edit new performance setting to disable destroyed units 2020-07-25 23:05:13 +02:00
Khopa
4c17e1fd33 Possible to disable destroyed units. 2020-07-25 23:04:00 +02:00
Khopa
d5fb1f62f5 Previously destroyed units are added to the mission. 2020-07-25 18:46:10 +02:00
Khopa
b34ede3795 LHA carrier group ships are more spread out. 2020-07-25 16:21:03 +02:00
Khopa
4989d84693 Carrier group ships are more spread out. 2020-07-25 16:18:13 +02:00
Khopa
2f210ab59f Fixed error in carrier group generator if faction has no destroyers 2020-07-25 16:17:40 +02:00
Khopa
3ffea901f1 Added japan_2005 faction. 2020-07-25 16:15:23 +02:00
Khopa
5e2e6520ce Fixed issues with coldwar factions. 2020-07-25 16:14:53 +02:00
Khopa
a8679c8eef Fixed strike mission generator window was named "sead mission generator" 2020-07-25 15:14:48 +02:00
Khopa
469e842e96 Fixed wrong radio frequency for german WW2 warbirds. 2020-07-24 01:37:33 +02:00
Khopa
cd945c625f Base defense units can be controlled with CA. 2020-07-24 00:23:11 +02:00
Khopa
1346192b75 Fix : Carrier on Persian Gulf full map were sharing the same id. 2020-07-24 00:11:55 +02:00
Khopa
45bebdd94e Updated pydcs version in requirements.txt 2020-07-24 00:02:13 +02:00
Khopa
d0bbb025d3 Added carrier frequency to briefing. 2020-07-23 21:30:50 +02:00
Khopa
8d0c53ef69 Remove Oliver Hazard Perry from Cold War factions. 2020-07-23 21:12:46 +02:00
Khopa
a59c2dfb10 Changed F-15C default payload. Replaced AIM-9P by AIM-9M. Replaced AIM-120B by AIM-120C. 2020-07-22 23:59:25 +02:00
Khopa
9c7689f9b5 Increased offset for attack points. 2020-07-20 21:32:49 +02:00
Khopa
c580979fee Open base menu by single mouse click. 2020-07-20 20:36:57 +02:00
Khopa
e8e7bc95ea Culling is a bit less aggresive. 2020-07-20 00:29:37 +02:00
Khopa
839e3e0833 Removed JF-17 from usa 2005 faction. 2020-07-19 17:11:34 +02:00
Khopa
4f451fab2f Show required mods in new gam wizard 2020-07-19 17:11:05 +02:00
Khopa
7d0413f41d Remove Type 93 from radar db and cc fleet gen 2020-07-17 01:45:46 +02:00
Khopa
bbac78195d Credits 2020-07-17 01:08:47 +02:00
Khopa
20fef86b84 Fixed aircraft carrier cold start 2020-07-17 01:02:21 +02:00
Khopa
9581a8f1f4 Support for frenchpack mod & Rafale Mod 2020-07-09 00:39:33 +02:00
Khopa
58b4c36b6c MB-339PAN payload fix. 2020-07-05 18:10:54 +02:00
Khopa
15aaa5d9a1 MB-339PAN support. 2020-07-05 17:15:43 +02:00
Khopa
b38332d061 Requirements update, new pydcs version 2020-07-04 16:43:01 +02:00
Khopa
4cfbbb6756 A-4E-C is now considered carrier capable; 2020-07-04 15:59:50 +02:00
Khopa
dec7db9e69 Cleaning up dead code 2020-07-04 15:59:23 +02:00
Khopa
274be3bcfc Fix : Carrier sail into the wind. Not in the same direction. 2020-07-04 15:59:00 +02:00
Khopa
d8a668ce60 Fix : Channel map, removed town names 2020-07-04 02:04:53 +02:00
Khopa
a845ed1998 Fix : Code that should be commented for custom ref point. 2020-07-04 01:37:04 +02:00
Khopa
53bd147de2 Community A-4E-C support 2020-07-04 01:35:48 +02:00
Khopa
4248b518a2 Merge branch 'develop' of https://github.com/khopa/dcs_liberation 2020-07-03 02:55:50 +02:00
Khopa
8f65a88d8b New satelitte background for all maps. 2020-07-03 02:54:58 +02:00
C. Perreau
4d4a640f34 Merge pull request #11 from DeusEx010101/UI-fixes-new-themes
UI Update!
2020-07-01 15:39:31 +02:00
Donnie
af9ead5937 lots of UI enhancements for better feedback and state 2020-06-30 22:19:45 -04:00
Donnie
ae3518f450 Map cp lines and flight paths are now the color of your coalition (red or blue) 2020-06-30 16:44:04 -04:00
Donnie
04e77a97f2 Fix: map icons now match player side color (red or blue) 2020-06-29 17:32:29 -04:00
Donnie
0ff3ce98e0 new map graphic test 2020-06-29 17:31:47 -04:00
Donnie
df2659787c adjust map colors 2020-06-29 17:31:35 -04:00
Donnie
d994673e91 blurred map background to soften edges 2020-06-29 10:00:12 -04:00
Donnie
dce781ef0e New splash screen 2020-06-29 09:59:20 -04:00
Donnie
b0e2c73024 Made SAM range circles easier to see 2020-06-28 22:56:48 -04:00
Donnie
bda28f81cc Added disable button style 2020-06-28 22:45:30 -04:00
Donnie
d8dc3d48b1 made dcs theme default 2020-06-28 01:12:47 -04:00
Donnie
5fdfa6339d front line thinner 2020-06-28 01:10:38 -04:00
Donnie
9a799190ef Map ui design updated 2020-06-28 01:06:02 -04:00
Donnie
8d32ba5b8e Map red icons darker 2020-06-28 01:05:37 -04:00
Donnie
85a7f89ba9 Css updates to dcs theme 2020-06-27 23:55:56 -04:00
Donnie
17f5378326 Modified recruit button layout 2020-06-27 23:54:27 -04:00
Donnie
9d67741310 made font references constants 2020-06-27 23:53:56 -04:00
Donnie
ac6a106c6a Merge branch 'master' into DCS-UI-Theme 2020-06-26 16:18:01 -04:00
Donnie
d775b8baa0 Some clean up and refactoring 2020-06-26 11:39:28 -04:00
Donnie
2dc1b3ec43 Icons change based on theme. wip 2020-06-26 00:57:00 -04:00
Donnie
d3d5160861 UI Theme switcher working 2020-06-26 00:39:04 -04:00
Donnie
cdf8c3b6e5 menu bar and info panel spacing 2020-06-24 22:09:31 -04:00
Donnie
29b1bbab9d combobox styles. what a pain. WIP 2020-06-24 18:13:20 -04:00
Khopa
c5d055c19b Prepared version RC9 2020-06-24 23:54:23 +02:00
Khopa
8f7b51a3df Fixed an issue with MolniyaGroup generator causing an error on campaign start. 2020-06-24 23:51:23 +02:00
Khopa
44e8cc810f Fixed an issue with MolniyaGroup generator causing an error on campaign start. 2020-06-24 23:49:27 +02:00
Donnie
b994878465 checkboxes 2020-06-24 17:33:36 -04:00
Khopa
8d5d703cbe Merge branches 'develop' and 'master' of https://github.com/khopa/dcs_liberation into develop 2020-06-24 23:22:34 +02:00
Khopa
1c1936d8f8 Fix : Carrier TACAN was wrongfully set up as an A/A TACAN 2020-06-24 23:22:07 +02:00
Donnie
7890fd5cc7 Base theme applied. WIP. 2020-06-24 16:40:19 -04:00
C. Perreau
4bdf11eb79 Merge pull request #9 from DeusEx010101/New-Icons
changed AA icon
2020-06-24 22:29:56 +02:00
Donnie
81f4c7303f changed AA icon 2020-06-24 15:39:08 -04:00
C. Perreau
0641907ea0 Merge pull request #8 from DeusEx010101/New-Icons
Icons updated to be more consistent
2020-06-24 21:31:32 +02:00
Donnie
ff0f32fcf5 Icons updated to be more consistent 2020-06-24 14:52:06 -04:00
Khopa
768049ca36 Changelog update 2020-06-23 12:57:46 +02:00
Khopa
88c8fe7cca Fixed zoom on map. 2020-06-23 12:56:01 +02:00
Khopa
5e7facfeb6 Fixed issue with WW2 LST group (boats superposed) 2020-06-23 12:06:19 +02:00
Khopa
6e43467ef6 Fix : Reduced maximum number of uboat 2020-06-23 12:05:15 +02:00
Khopa
cd4ef8ae32 Fix frequency for P-47 2020-06-23 11:46:57 +02:00
Khopa
93a4463f22 Fixed logging issues and SEAD flights departing without waiting. 2020-06-22 23:20:14 +02:00
Khopa
56cf6bdaa4 Mission planning polishing. 2020-06-22 21:31:25 +02:00
Khopa
fe02df27a2 Do not allow selection of non existing carrier in mission planner 2020-06-22 12:44:49 +02:00
Khopa
a60ab68287 Aircraft Carrier will try to sail in the wind when possible. 2020-06-21 18:15:58 +02:00
Khopa
83e46ddc97 SEAD AI will only target the designated target group. 2020-06-21 17:50:06 +02:00
Khopa
9c89ad7c72 Reduced maximum wind in generated missions. 2020-06-21 15:05:57 +02:00
Khopa
0518abdedc Balance for WW2 aircraft. WW2 B_17 bombing mode is now much more effective. 2020-06-20 15:28:42 +02:00
Khopa
93bcd07c7f Added aircraft icons + fixed ww2 empty aircraft loadout not being loaded. 2020-06-20 13:44:11 +02:00
Khopa
5f1f4f8d81 Culling optimizations + fixed some aircraft icons 2020-06-19 14:31:25 +02:00
Khopa
e78120d9c6 Fix : Egress waypoint for SEAD mission is wrongfully named INGRESS 2020-06-17 23:15:44 +02:00
Khopa
cc5986d435 Updated readme 2020-06-17 13:48:33 +02:00
Khopa
5192306b06 Generate fleet and missiles sites 2020-06-17 13:04:55 +02:00
Khopa
826935eb7d Ship icons for ship groups 2020-06-14 17:31:45 +02:00
Khopa
cd41bcf45c Generate WW2 Ship groups, added B17 to allies. Implemented modifiable doctrine to setup flight generator. 2020-06-13 18:53:43 +02:00
Khopa
601375d06f Channel map support. 2020-06-12 19:10:58 +02:00
Khopa
c708abafc8 Started refactoring on ground objects. WW2 factions will have WW2 style buildings. Added ground objects templates for : "village", "ww2bunker", "allycamp" 2020-06-11 21:50:09 +02:00
Khopa
90d588353c Icon and background image for channel map. Updated Normandy map background. 2020-06-11 21:48:16 +02:00
Khopa
2d4287df2a Polished mission generator, fixed sam bug on map view. 2020-06-10 22:34:31 +02:00
Khopa
fde3a988b7 Added missions generators in flight planner. + refactoring 2020-06-09 02:13:46 +02:00
Khopa
397164e667 Added multiple settings (external views, map views) and added marker generators. 2020-06-07 19:55:38 +02:00
Khopa
a7824039e3 Added revenue for derrick building 2020-06-07 15:54:15 +02:00
Khopa
bb9247e821 Fix info panel messages. 2020-06-06 16:57:20 +02:00
Khopa
4bc04ec031 Auto redeploy frontline units. 2020-06-06 16:50:19 +02:00
Khopa
8fd91e6c5c Added possible ground target : Oil derrick 2020-06-06 15:45:15 +02:00
Khopa
ee137d086a Change power station template. (Some buildings could superpose in older version) 2020-06-06 15:10:07 +02:00
Khopa
fcd81850cb Debriefing and info on AA site and buildings destroyed. KC130 replace S3B 2020-06-06 04:10:22 +02:00
Khopa
aa4b07d024 Add more waypoints to generated flights 2020-06-05 21:24:23 +02:00
Khopa
16a096d288 Possible to setup whether AI should starts from parking or not. 2020-06-05 14:23:38 +02:00
Khopa
b219b2a71b Possible to setup custom saved game and installation directory. 2020-06-05 14:21:42 +02:00
Khopa
ce70242c35 Added 947 to allies and default payload for P47 2020-06-04 13:30:38 +02:00
Khopa
dec01e2611 Added P-47 to db 2020-06-04 13:25:14 +02:00
Khopa
4373d89661 WIP on CAP, and AI units can starts from ground uncontrolled instead of using late activation. 2020-06-03 00:45:44 +02:00
Khopa
c73290eebb Nav Target point for JF-17 and F-14.
Fix activation trigger using wrong coalition when playing REDFOR side.
2020-06-02 13:00:10 +02:00
Khopa
a022f9c2e1 Changelog update 2020-06-01 15:27:45 +02:00
Khopa
5adb25a695 Using late activation trigger. Planned flight will not spawn if their home base has been captured or is contested. 2020-06-01 15:11:38 +02:00
Khopa
08c2972cf9 Added map theater : North Caucasus 2020-06-01 14:20:43 +02:00
Khopa
8132c7e676 Fix : CAP aircraft will jettison fuel tanks. 2020-06-01 13:03:35 +02:00
Khopa
8c68c9f703 Fix for payload for release versions. 2020-06-01 12:51:59 +02:00
Khopa
000b6142fd Added Ka-50 to bluefor modern. 2020-06-01 12:42:02 +02:00
Khopa
c203ded1cd Improved Mission Planning flight selection behaviour. 2020-06-01 12:36:10 +02:00
Khopa
64c5c39b2a BARCAP renamed CAP for regular airbases. 2020-06-01 11:14:30 +02:00
Khopa
a8d2a1e371 Removed Ju-88 from CAS (only has torpedo in DCS) 2020-06-01 01:49:20 +02:00
Khopa
9e5846b24a Improved WW2 support. 2020-06-01 01:27:16 +02:00
Khopa
836ff9122c Fix base defense units not being generated when other ground objects were too close. 2020-06-01 00:35:26 +02:00
Khopa
75d836358b Revert an error on sam site generation. (Disabled the wrong code.) 2020-06-01 00:05:32 +02:00
Khopa
bb11e7f90c Removed FARP AA that does not respect the factions config. 2020-05-31 23:58:43 +02:00
Khopa
cf6a71ab86 Fix issue when "no night mission" is enabled. 2020-05-31 23:50:13 +02:00
Khopa
7f7288937d Handle error when there is no AWACS for a faction. 2020-05-31 23:26:03 +02:00
Khopa
a38f9c2183 Added performance settings. 2020-05-31 23:07:54 +02:00
Khopa
7ee880cadc Generate ICLS command for carriers 2020-05-31 20:00:07 +02:00
Khopa
9ae34d474b Added Bluefor modern faction 2020-05-31 19:44:53 +02:00
Khopa
2817e2f2c8 Update for new pyDCS version. Supercarrier support added. 2020-05-31 19:19:03 +02:00
Khopa
02886a09d3 AI Strike flight with user generated STRIKE, will now perform their bombing task correctly. 2020-05-31 18:02:00 +02:00
Khopa
0b9d827ad6 AI Strike flight will bomb all their targets correctly 2020-05-31 15:43:56 +02:00
Khopa
ab3ea84d70 AI flight planner now auto generate STRIKE flights.
Fix CAS point position in predefined wpt selector.
When an airbase is captured, base defenses are re-generated for the new base owner.
2020-05-30 02:32:45 +02:00
Khopa
03a1c44659 README update for QT UI branch 2020-05-30 00:03:42 +02:00
Khopa
53364444fd Possible fix for crash when loading old save files. 2020-05-30 00:03:11 +02:00
Khopa
94040e8551 Fix new campaign wizard crashing and made the campaign generator slightly faster. 2020-05-29 23:47:13 +02:00
Khopa
34d46ee28e Fix objective name error when no more objective name are availables. 2020-05-29 23:43:26 +02:00
Khopa
59986d74f4 Generate random TACAN frequency for ships at start.
TACAN infos are displayed in briefing.
2020-05-29 03:28:09 +02:00
Khopa
8afdf5ef65 Polishing, Bug Fixes, Slight improvements to flight generator for BARCAP. 2020-05-29 03:05:27 +02:00
Khopa
fc64e57495 Ground objects have 'Objective' name for easier search. Predefined Waypoint generator entirely reworked.
Added finances menu, and info panel.
2020-05-29 00:40:06 +02:00
Khopa
6dec5ea8f8 Reworked payloads, factions, map display, carrier. Re-added Tarawa support, many minor bug fixes. UI changes. 2020-05-27 21:45:58 +02:00
Khopa
3f2aafcd28 Now possible to create/delete flights. 2020-05-25 14:54:02 +02:00
Khopa
7a3ee6b1a9 Frontline progression is now based on combat results. 2020-05-25 04:09:51 +02:00
Khopa
f7d9c62afb Updated mission launch instructions. 2020-05-25 01:22:20 +02:00
Khopa
1a26a7f346 Fix an issue on Nevada map, cp not well connected. 2020-05-25 00:52:46 +02:00
Khopa
f6ff4405f8 Merge branch 'qt_ui' of https://github.com/shdwp/dcs_liberation into qt_ui 2020-05-24 23:53:51 +02:00
Khopa
14ceb0abc6 Fix : Interceptor groups can have custom waypoints. 2020-05-24 23:50:30 +02:00
Khopa
c9a3af6065 Flight waypoint editor to select targets 2020-05-24 23:44:06 +02:00
Khopa
4e4a7fe84e Budget amount per turn, now depends on factory owned and such 2020-05-24 21:30:05 +02:00
Khopa
989d25e2e2 Updated UI ground objects icons 2020-05-24 20:53:41 +02:00
Khopa
f57e453d8d Ground war rework 2020-05-24 20:32:45 +02:00
Wrycu
fcfa785ae9 fix(save): correct openbeta detection 2020-01-18 11:29:16 -08:00
Wrycu
3de688da29 fix(payloads): fix for payloads when compiled into an exe 2020-01-18 11:28:26 -08:00
Khopa
5884d9d120 Made it possible to toggle 'flight paths' visibility. 2019-12-14 20:32:39 +01:00
Khopa
074ea5c719 JF-17 Support (Require pydcs own data export) 2019-12-09 02:09:50 +01:00
Khopa
e794446a54 Fix briefing text being wrong 2019-12-04 19:42:46 +01:00
Khopa
1320a67e1f Fix briefing generation, missing line break 2019-12-04 19:39:04 +01:00
Khopa
7c45177650 Fix issue with bullseye missing causing a crash when launching the mission in MP on a server. 2019-12-04 19:18:20 +01:00
Khopa
6b2d6ab57f Updated pydcs version in requirements.txt 2019-11-21 22:22:39 +01:00
Khopa
04add8ebb5 Fix weird import issue in operation.py. 2019-11-21 22:08:05 +01:00
Khopa
ad8138fb04 Fix an issue with player UH-1H radio 2019-11-21 19:58:28 +01:00
Khopa
6598e034c1 Briefing generation changed to match new flight. Include info for all flight containing client slots; 2019-11-21 19:56:26 +01:00
Khopa
bba51c6a23 Changed position of the carrier on Caucasus small map 2019-11-21 19:55:29 +01:00
Khopa
7d524300e5 Start window maximized 2019-11-21 19:55:03 +01:00
Khopa
a25a0031ff Improved CAS flight waypoints generated by ai flight planner. 2019-11-11 02:41:17 +01:00
Khopa
98b899c9c7 Fix for SEAD flights not being correctly configured. 2019-11-10 23:41:47 +01:00
Khopa
4424425dd4 Changed default map when creating new game. (Added 'recommended' label to the maps i really tested the most) 2019-11-03 17:32:34 +01:00
Khopa
85de3a09ea Remove triggers from generated mission for now. Do not generate the 'quick' mission
Fix range for ai flight planner so they are more likely to plan CAS flights
(It's up to the player to setup flight on the runway or in flight)
2019-11-03 17:27:30 +01:00
Khopa
e82db1fecd Only allow recruiting carrier units in aircraft carrier groups. Generate one group on carrier when possible. 2019-10-26 23:54:33 +02:00
Khopa
76638b549f Generate ship groups for carriers. Carriers can be destroyed. Faction need to have carrier available. 2019-10-26 22:59:59 +02:00
Khopa
2936df6a02 Fix error (unable to start DCS liberation if no save file exists) 2019-10-26 20:58:28 +02:00
Khopa
27e3cf8ac5 Added possibility to setup parking slot type for players' flight. 2019-10-23 17:01:41 +02:00
Khopa
adf1f8db8c Print weapons with their pretty name in loadout editor 2019-10-20 01:54:33 +02:00
Khopa
9f319ab99a Added loadout editor to mission preparation screen. 2019-10-19 16:14:40 +02:00
Khopa
48a40f2511 Stats view now has a tab for vehicles forces. Display waypoint coordinates in waypoint tab. 2019-10-19 13:12:21 +02:00
Khopa
7fbc75b375 New mission briefing menu, work in progress. 2019-10-19 00:07:37 +02:00
Khopa
65a54acd4f Generate ground forces on all active frontlines. 2019-10-17 01:45:46 +02:00
Khopa
de96552f78 Actually generate the planned flights from the flight planner. 2019-10-16 23:20:11 +02:00
Khopa
fee959940a Base menu layout improved. 2019-10-16 22:22:16 +02:00
Khopa
48748bbc39 Improved base menu to show planned AI flights and base defence. (WIP) 2019-10-16 00:41:02 +02:00
Khopa
827138fc6a AI Flight planner generate CAS & SEAD 2019-10-15 23:52:43 +02:00
Khopa
b7ee98dcd6 Flight Planner first version (just print planned flight in log but does not generate them yet) 2019-10-15 23:06:21 +02:00
Khopa
fa99df3ce7 Normandy map image added to repo (was missing in last commit). 2019-10-15 23:05:43 +02:00
Khopa
d2ea6ed2fd Don't show airbase armor & air defense. 2019-10-15 19:19:44 +02:00
Khopa
d8d17e5c18 F_14B client slots are now by default using pre-alignement. 2019-10-14 18:53:43 +02:00
Khopa
0519438292 Improved german WW2 flak group generator. 2019-10-14 01:49:30 +02:00
Khopa
09b8ff6b93 Generate AA & armor to defend airbase. Destruction status correctly tracked. 2019-10-14 01:25:48 +02:00
Khopa
bd66dcb39e Added support for Normandy WW2 map 2019-10-13 23:00:26 +02:00
Khopa
93504eaf7a Added & generated Normandy theater geometry data 2019-10-13 21:24:13 +02:00
Khopa
9e116f481e Fixed issue with P51D liveries that caused errors. 2019-10-13 17:57:38 +02:00
Khopa
bd95258176 Fixed issue with lost helicopters causing errors in debriefing.
Mission generated now configure bluefor and redfor coalition properly, so it is possible to use country that are not in the default coalitions.
Added insurgent faction.
2019-10-13 17:17:33 +02:00
Khopa
a7e202bbc8 Made it possible to capture airbase ingame; 2019-10-13 05:16:34 +02:00
Khopa
3f5f4f4bb1 New automatic de-briefing system implemented 2019-10-13 03:40:22 +02:00
Khopa
4365db0fae Fix : Create default blank liveries for the F_16C_50 to avoid errors at mission generation; 2019-10-12 11:17:00 +02:00
Khopa
82bb608fd3 Mission units destroyed are stored in a json file being written mission runtime. (First step that will remove the need to save the debriefing manually after mission)
Using Mist framework to do this in the mission script env.
2019-10-12 03:21:33 +02:00
Khopa
ba0b3adf71 Added some factions, refactor bluefor/redfor distinction code. 2019-10-12 00:02:07 +02:00
Khopa
707e1f8b67 Refactored factions in separates files to declutter db.py. 2019-10-11 23:08:00 +02:00
Khopa
7f97e894a3 Fix issue with Zu23 Ural aa site generator 2019-10-11 02:07:08 +02:00
Khopa
19118f7b84 Update factions. 2019-10-11 02:05:18 +02:00
Khopa
63c46d1b21 Removed placeholder text. Polished UI when no saved game is found. 2019-10-11 01:27:29 +02:00
Khopa
4c3ff906ff Improved layout for airbase window layout.
Fixed missing icons for some windows.
2019-10-11 01:14:43 +02:00
Khopa
6b77e1cce5 Added possibility to not display SAM ranges in display options. 2019-10-11 00:27:10 +02:00
Khopa
fdd8f102e6 Generate battle environment : SEAD + CAS flight.
Added more factions.
2019-10-11 00:18:25 +02:00
Khopa
2d0c195e46 Added SAM HQ-7, tweaks in db 2019-10-08 22:35:08 +02:00
Khopa
f5a5fb765b Show SAM range on map. 2019-10-08 21:42:48 +02:00
Khopa
f698cb66b8 Ally SAM are shown with blue icon on map. 2019-10-08 21:07:06 +02:00
Khopa
fc51b16e4a Added playable F16C_Bl50 support (For now, you need to update pydcs to the version on my own fork at https://github.com/Khopa/dcs for it to work)
+ Payload overrides for F/A-18C and M-2000-5
2019-10-08 20:45:45 +02:00
Khopa
0365d7a900 Fix issue with payloads loading. 2019-10-08 00:44:57 +02:00
Khopa
7a14376765 Generate CAP on whole map, for 2 hours. Added China. 2019-10-07 01:57:36 +02:00
Khopa
2167953b87 Ground objects are always generated and destroyable, even when it's not the current mission objective.
Fix : SAM site destruction status is saved correctly.
Added most SAM site to generator.
2019-10-06 16:14:13 +02:00
Khopa
17352bfcf7 Fix : Not possible anymore to start a mission by clicking the PendingDelivery event icon. 2019-10-06 01:02:49 +02:00
Khopa
d4577aa7a6 Briefing Window : Only show "clients slot" spinbox if the aircraft is flyable. 2019-10-06 00:54:02 +02:00
Khopa
e955c10170 Added smaller Caucasus map for dev test and smaller campaign 2019-10-05 13:13:43 +02:00
Khopa
9d4b6183b0 Added Iranian faction, SAM SA-2 & SAM HAWK site generation added 2019-10-05 04:36:40 +02:00
Khopa
3c9bffb557 Persian Gulf : Added Liwa, Ras Al Khaimah & Al Ain airbases 2019-10-05 03:47:39 +02:00
Khopa
d4d7b546e1 Fixed debriefing success validation for others events. 2019-10-05 02:03:51 +02:00
Khopa
9f0c17115e Fixed debriefing for Naval Intercept 2019-10-05 02:01:12 +02:00
Khopa
f6fdd3d12a Fixed issue with FrontlineAttack debriefing validation 2019-10-05 01:52:18 +02:00
Khopa
2401da2b24 Persian Gulf Theather : Disconnect Lar from Qeshm 2019-10-04 23:34:01 +02:00
Khopa
50ef8fef24 Fixed Persian Gulf Theater control points in Iran being blue when midgame is selected. 2019-10-04 23:12:27 +02:00
Khopa
079248bfeb Fixed a few issue with the briefing view. 2019-10-04 22:18:20 +02:00
Khopa
650ee9666d Fix turn counter, so that turn 0 is turn 1 2019-07-19 19:52:26 +02:00
Khopa
18bcc1bce7 Implemented stats view. 2019-07-13 14:08:44 +02:00
Khopa
73b4ec47b9 Apply CSS to whole app, but disabled for now 2019-07-13 00:53:11 +02:00
Khopa
70331d913d UI : Daytime effect on map background 2019-07-13 00:35:35 +02:00
Khopa
9c72c9a063 Put correct cursor style when mouse hover a map item. 2019-07-13 00:05:59 +02:00
Khopa
af8ae09434 Draw control point as Arc with life gauge representing power 2019-07-12 23:37:44 +02:00
Khopa
e8a8364ac2 Added back the settings menu in the new UI 2019-07-12 23:13:03 +02:00
Khopa
0e7d49488c Put CSS in resources folder, so that it is properly embedded in the release. Updated requirement and release build for new UI. 2019-07-12 19:35:30 +02:00
Khopa
405c6659b9 Cleaned up some 'print'. 2019-07-12 19:14:44 +02:00
Khopa
7ca435337f Merge 2019-07-12 00:35:54 +02:00
Khopa
024b665dd9 Commit before merge 2019-07-12 00:32:47 +02:00
Khopa
a397296624 Fix : Only possible to open event menu if event is actually displayed. 2019-07-11 21:38:59 +02:00
Khopa
adb9e38ff4 Waiting and Debriefing Window, QT UI is now functionnal. 2019-07-11 21:31:16 +02:00
Khopa
c4dc432be1 Fix issue when the faction lack units in a category of task 2019-07-11 12:16:47 +02:00
Wrycu
49795993f1 refactor(release): clean up release build process
* handle case where a dist has never been created
* ignore requirements.txt
* include map resources instead of pulling from submodule
2019-07-10 23:17:52 -07:00
Wrycu
ea6b2ab2dc chore(ignore): removed unused/personal files 2019-07-10 23:14:46 -07:00
Wrycu
44d04d3ba6 chore(requirements): add release requirement 2019-07-10 23:12:49 -07:00
Wrycu
577c171d48 chore(ignore): removed unused/personal files 2019-07-10 23:11:45 -07:00
Vasyl Horbachenko
c5edf7a581 Update README.md 2019-07-09 18:04:50 +03:00
Vasyl Horbachenko
ac67ec86b1 Delete a.py 2019-07-09 18:04:19 +03:00
Wrycu
291f538645 chore(requirements): add requirements file 2019-07-07 18:03:51 -07:00
Khopa
817a3a5e15 Briefing has start function and departure airbase selection 2019-07-07 13:57:04 +02:00
Khopa
0b87e192ce Briefing windows layout done 2019-07-07 13:36:40 +02:00
Khopa
fc83ca0de6 Added credits to pydcs in about window 2019-07-07 12:16:04 +02:00
Khopa
c621e822dc Fix artifact on map when drawing events & improved base menu 2019-07-07 12:11:12 +02:00
Khopa
1776452964 Show events on map 2019-07-06 13:54:45 +02:00
Khopa
4e5e0a4a7a ix : Possible to buy units in player base, not ennemy bases 2019-07-06 13:22:09 +02:00
Khopa
bb32f47b8c Implemented basic base menu to buy units 2019-07-06 13:21:03 +02:00
Khopa
6b5f77c415 Added classes for sub windows. (WIP) 2019-07-05 22:01:16 +02:00
Khopa
9a73c78705 Using a singleton QObject to propagate game model update across whole app 2019-07-05 21:43:14 +02:00
Khopa
8246c8e94b Top bar layout, added some icons and possible to pass turn 2019-07-05 21:02:18 +02:00
Khopa
89e8ef65ea Added about dialog & credits + icon in pass turn button 2019-07-05 20:44:01 +02:00
Khopa
2428d39308 Implemented top bar with turn counter and budget info. Added CSS stylesheet. 2019-07-05 20:05:27 +02:00
Khopa
dc91f5004e New Game Wizard with Qt UI 2019-07-05 01:58:23 +02:00
Khopa
b66bf4cc36 Qt Map drawn with line and frontline 2019-07-04 19:04:31 +02:00
Khopa
67910bce61 Map implementation 2019-07-04 09:33:19 +02:00
Khopa
2adacdba02 icon as png for qt 2019-07-03 23:35:22 +02:00
Khopa
4652e7d188 Basics for QT UI 2019-07-03 23:33:47 +02:00
Khopa
a30d4a4514 Fix in 'unit_task' method 2019-07-03 21:06:03 +02:00
Khopa
a17a9399ef Merge remote-tracking branch 'wrycu/develop' into develop_khopa
# Conflicts:
#	gen/aaa.py
2019-07-03 19:45:03 +02:00
Khopa
ba6d10cd9e Small fix and database changes. 2019-07-03 18:50:16 +02:00
Wrycu
b1576e8f15 fix(SAMs): correctly track when a SAM site is destroyed 2019-07-01 21:49:25 -07:00
Wrycu
55de28105e fix(farp): remove unsupported units from spawning in farps 2019-07-01 21:45:58 -07:00
Wrycu
40e3d59432 fix(cap): cap now properly spawns at a randomized interval after take-off 2019-06-30 12:59:01 -07:00
Khopa
665f022111 Fix Ship Gen 2019-06-30 19:15:46 +02:00
Khopa
309c10c4cb Selectable factions wip 2019-06-30 14:05:26 +02:00
Khopa
658120b8d9 Tweaked ground object generator 2019-06-30 05:48:19 +02:00
Khopa
fbd01fbfdb WIP for possibility to select player and ennemy faction 2019-06-30 05:31:22 +02:00
Khopa
09135adadc Allow selection of a time period, for campaign start 2019-06-30 02:58:04 +02:00
Khopa
0c7a36cef6 Start time generation now more correct. 2019-06-30 01:56:40 +02:00
Khopa
abf6fe69bd Generate start time of the mission according to settings. 2019-06-30 01:38:12 +02:00
Khopa
b3318583ed Added a filter so that map displayed change color according to the time of the day. 2019-06-30 01:32:47 +02:00
Khopa
288fe20def Doubled size of map image for Caucasus 2019-06-30 01:12:15 +02:00
Khopa
96ee14bf08 Icon order in topbar 2019-06-30 01:11:46 +02:00
Khopa
00b9ba0f32 Display current day time icon in top bar and improved overall look and feel. 2019-06-30 01:02:50 +02:00
Khopa
2a63f7b187 Drew and added misc icons 2019-06-30 00:42:07 +02:00
Khopa
c655d97109 Drew a few placeholder icons for daytime 2019-06-30 00:25:18 +02:00
Khopa
fd26700867 Show current turn, date and time of the day in overview. 2019-06-30 00:14:33 +02:00
Khopa
df5d9782e7 Added turn counter, top bar refresh correctly. 2019-06-29 21:06:05 +02:00
Khopa
61af07bd79 Fixed pydoc 2019-06-27 22:47:01 +02:00
Khopa
0e008ac87b Added method to get DCS saved games directory 2019-06-27 22:41:58 +02:00
Khopa
28eab870d4 Added methods to retrieve the DCS installation directory (with Steam/Standalone version abstraction) 2019-06-27 22:38:05 +02:00
Khopa
86aedaf631 Added a module to check DCS environment, to be completed. 2019-06-27 21:58:12 +02:00
Khopa
e242851260 Optimized imports in debriefing.py 2019-06-27 21:43:09 +02:00
Khopa
c884fb7262 Merge remote-tracking branch 'wrycu/develop' into develop_khopa
# Conflicts:
#	ui/overviewcanvas.py
2019-06-27 21:24:01 +02:00
Khopa
b6104fbdaf Removed idea files from version control 2019-06-27 21:03:14 +02:00
Khopa
9305f1d483 Added xml idea files in gitignore 2019-06-27 21:00:02 +02:00
Khopa
048cd22d85 Removed idea files from version control 2019-06-27 20:56:23 +02:00
Khopa
e1ea9664dc Added idea files to gitignore 2019-06-27 20:54:36 +02:00
Wrycu
72d3863f8a feat(intercept): add random delay to enemy intercept 2019-06-25 22:21:12 -07:00
Wrycu
49de53f1c9 feat(strike): add chance of AAA/tanks to strike targets 2019-06-24 20:44:28 -07:00
Wrycu
b17e34351c fix(strike): spawn air defense SAM sites on strike 2019-06-24 19:01:56 -07:00
Wrycu
84b1df75c2 feat(attack): air defense spawns further from the thing they are defending 2019-06-23 14:27:07 -07:00
Wrycu
89f122c325 feat(attack): add multi-unit SAM site support 2019-06-23 13:43:16 -07:00
Wrycu
e9103acb07 feat(attack): base attack - now spawn all CAP aircraft when a base is attacked 2019-06-22 14:30:27 -07:00
Wrycu
c7eae7e97a fix(base): correct AA spawn 2019-06-22 14:17:28 -07:00
Wrycu
f5851d09f5 fix(overview): correct negative radius 2019-06-22 14:15:24 -07:00
Vasyl Horbachenko
ba8112280d Merge pull request #66 from shdwp/develop
Develop
2019-05-31 16:49:03 +03:00
Vasyl Horbachenko
4cc373595c Update README.md 2019-05-31 16:39:18 +03:00
Wrycu
36aa4edb05 refactor(units): add SAMs to air defense 2019-05-30 21:32:39 -07:00
Wrycu
26840373ed refactor(turns): modify turn settings
* raise air defense limits
* enemy gains more units even if no action is taken on a turn
* player gains money even if no action was taken on a turn
2019-05-30 21:31:50 -07:00
Vasyl Horbachenko
4c5a2650d1 Merge pull request #58 from JohanAberg/patch-1
ValueError: negative radius
2019-05-21 04:50:30 +03:00
Johan Aberg
b99cdfb5c3 ValueError: negative radius
When cp.base.strength is zero 0, radius_m becomes negative and an ValueError is raised.

INFO:root:Commision Al Maktoum: {<class 'dcs.vehicles.Armor.APC_BTR_80'>: 3}
ERROR:root:<class 'ValueError'>
Traceback (most recent call last):
  File "tkinter\__init__.py", line 1702, in __call__
  File "ui\window.py", line 156, in dismiss
  File "ui\mainmenu.py", line 29, in display
  File "ui\overviewcanvas.py", line 590, in update
  File "ui\overviewcanvas.py", line 252, in draw
  File "ui\overviewcanvas.py", line 312, in draw_map
  File "ui\overviewcanvas.py", line 331, in draw_bases
ValueError: negative radius

Proposed fix:
radius_m = max(radius * cp.base.strength - 2, 0)
2019-04-04 19:10:59 +13:00
Vasyl Horbachenko
6ec14e744e fixed starting bat; updated escort options for Strikes 2019-03-31 19:55:27 +03:00
Vasyl Horbachenko
310db66c22 Merge branch 'develop' of https://github.com/shdwp/dcs_liberation into develop 2019-03-27 11:53:14 +02:00
Vasyl Horbachenko
c7c2b9a248 special flight for AI to SEAD; AI bomb task 2019-03-27 11:53:00 +02:00
Vasyl Horbachenko
b697a8b40a cleanups & minor fixesh 2019-03-27 10:41:40 +02:00
Vasyl Horbachenko
c152b49b88 minor fixes; F-14B 2019-03-27 08:47:29 +02:00
Vasyl Horbachenko
2356fc2bbf Merge pull request #54 from Khopa/develop_khopa
Re-added missing png icons
2019-01-23 23:27:34 +02:00
Vasyl Horbachenko
f7e2c8921c integration tests for operation generation; adjusted waypoint altitude for AI to not fly too low; removed C101 from list of generated AI aircrafts 2019-01-16 01:07:24 +02:00
Khopa
604352f8df Re-added missing icons (they were somehow removed from the repository, and the software couldn't be launched) 2018-12-03 21:06:47 +01:00
Vasyl Horbachenko
fbbe56f954 fixes to convoy strikes; UI updates; don't generate helis for enemy 2018-11-06 04:06:59 +02:00
Vasyl Horbachenko
7842c69ebb fixed incompatible missions being available from carriers 2018-11-06 02:59:24 +02:00
Vasyl Horbachenko
e1d50f1f27 added missing assets; convoy strike event 2018-11-06 02:33:38 +02:00
Vasyl Horbachenko
9d0997624b fixes for selectable departure; generate statics for used units; raised RTB alt 2018-11-05 03:17:06 +02:00
Vasyl Horbachenko
355cd3e0e4 display events on map 2018-11-04 04:50:51 +02:00
Vasyl Horbachenko
af5cd57094 WIP: display events on the map 2018-11-04 04:06:44 +02:00
Vasyl Horbachenko
8f85101cec WIP: display events on the map; start events from adjacent CPs 2018-11-04 02:38:14 +02:00
Vasyl Horbachenko
97be483624 Merge pull request #43 from Khopa/sdl_map
Scrollable & Zoomable Map view
2018-11-04 00:58:14 +02:00
Khopa
1ff5721912 Possible to toggle map size 2018-11-03 23:56:10 +01:00
Vasyl Horbachenko
80cbc663bf Merge pull request #45 from calvinmorrow/labels_and_coalition_view
Labels and coalition view
2018-11-04 00:54:21 +02:00
Vasyl Horbachenko
b3e729af0d Merge pull request #44 from calvinmorrow/bugfix_37
Add initiatorMissionID key to multiplayer debriefing table
2018-11-03 20:45:56 +02:00
Calvin Morrow
3ed864021d Move log button and minimize padding to fit existing window size 2018-10-31 15:24:34 -07:00
Calvin Morrow
8bc269fa99 Add labels game setting and map visibility setting to mission generation forced options 2018-10-31 15:11:12 -07:00
Khopa
c656c0f7e4 Confirm dialog to start a new game 2018-10-31 17:18:08 +01:00
Khopa
858e5d2d04 Do redraw the map when declutter options are toggled 2018-10-31 14:35:17 +01:00
Khopa
903a8db46f Commented debug mouse cursos position rect 2018-10-31 14:26:23 +01:00
Khopa
5aa731853d The zoom is a bit less annoying to use 2018-10-31 14:24:37 +01:00
Khopa
540096178c Display zoom level on pygame overlay in bottom right corner 2018-10-31 14:22:49 +01:00
Khopa
ed2a611197 Removed unnecessary print 2018-10-31 14:14:17 +01:00
Khopa
1c61b0b5a2 Refactored the way icons are loaded + reformat 2018-10-31 13:10:07 +01:00
Calvin Morrow
82f7e5d0c4 Add initiatorMissionID key to multiplayer debriefing table for dead unit detection 2018-10-30 19:40:59 -07:00
Khopa
ecb2c86dc4 Change order of map view options 2018-10-31 01:29:47 +01:00
Khopa
5cbbc3b1ab Improved base intel view 2018-10-31 01:22:08 +01:00
Khopa
e0b2e178f9 Display detailled info about base when mouse hover 2018-10-31 00:59:02 +01:00
Khopa
390974ba0f Icons for all kind of targets 2018-10-31 00:20:29 +01:00
Khopa
91379ff7d9 Instructions for map + tweaks (enable AA for text) 2018-10-30 23:32:27 +01:00
Khopa
1fe9e56997 Added missing resources. Bigger maps for nevada & caucasus 2018-10-30 20:30:10 +01:00
Khopa
e10b853712 Much better performance for the pygame viewport. 2018-10-30 20:00:14 +01:00
Khopa
7d70862e72 Work in progress : features restored. 2018-10-30 17:12:35 +01:00
Khopa
90bdcec7ff Render map on a pygame SDL layer. WIP 2018-10-30 14:47:31 +01:00
Vasyl Horbachenko
63da350223 armor balance tweaks 2018-10-23 04:04:30 +03:00
Vasyl Horbachenko
854f31cb7a armor balance improvements; fixed trigger for farps 2018-10-23 03:40:12 +03:00
Vasyl Horbachenko
eba6daf6c8 Merge remote-tracking branch 'origin/master' 2018-10-23 03:08:13 +03:00
Vasyl Horbachenko
911d57b415 fixed FOB generation 2018-10-23 03:08:01 +03:00
Vasyl Horbachenko
d91e0344a7 Update README.md 2018-10-23 00:54:55 +03:00
Vasyl Horbachenko
e9d7ee51f3 readme upd 2018-10-22 02:29:42 +03:00
Vasyl Horbachenko
9053408e13 Merge branch 'develop' 2018-10-22 02:18:26 +03:00
Vasyl Horbachenko
8f4094ee98 number of fixes 2018-10-22 02:13:38 +03:00
Vasyl Horbachenko
933e064079 fixed debriefing crash 2018-10-14 04:30:46 +03:00
Vasyl Horbachenko
274e08dd8b quick mission debriefing fixed; adjusted constants 2018-10-14 04:09:05 +03:00
Vasyl Horbachenko
05c968edc2 added Gazelle as CAS aircraft + minor fixes 2018-10-13 23:38:26 +03:00
Vasyl Horbachenko
270820de0b added Gazelle as CAS aircraft 2018-10-13 22:50:26 +03:00
Vasyl Horbachenko
e049a97bec minor text update 2018-10-13 22:30:43 +03:00
Vasyl Horbachenko
e2306ba0f3 new weather, strike objectives placement fixes & tarawa for av8b 2018-10-13 22:28:30 +03:00
Vasyl Horbachenko
6d0f488672 updated version compatibility check 2018-10-13 04:44:59 +03:00
Vasyl Horbachenko
397f9a58cb fixed naval intercept crash; fixed wrong targets order; fixed initial waypoint being WP #1; m2k a2g ccip; fixed time being time zone offset ahead; lowered rain weather chance 2018-10-13 04:41:18 +03:00
Vasyl Horbachenko
4fc766a524 trigger fixes; strike waypoint fixes; m2k strike payload update 2018-10-13 02:36:25 +03:00
Vasyl Horbachenko
3f8b5c6c00 minor UI fix 2018-10-12 23:33:10 +03:00
Vasyl Horbachenko
ce43be0d67 display strike objectives on map; minor fixes 2018-10-12 23:31:00 +03:00
Vasyl Horbachenko
ff08888385 minor adjustment in strike objectives generation 2018-10-12 22:38:26 +03:00
Vasyl Horbachenko
6b96410ea4 updated config menu 2018-10-12 22:29:37 +03:00
Vasyl Horbachenko
f21bd10f09 number of minor bugfixes and UI improvements 2018-10-12 21:32:43 +03:00
Vasyl Horbachenko
07b35f8ee1 specify tanker callsign in briefing 2018-10-12 05:45:28 +03:00
Vasyl Horbachenko
251435ae0b debriefing results based only on IDs; fixes in strike ops 2018-10-12 05:41:52 +03:00
Vasyl Horbachenko
b81bf90319 few minor fixes 2018-10-12 03:46:53 +03:00
Vasyl Horbachenko
d6b1b8665d minor fixes 2018-10-12 03:37:32 +03:00
Vasyl Horbachenko
64bd3e6a52 removed debugging code 2018-10-12 03:17:41 +03:00
Vasyl Horbachenko
520a0f91fd UI update; enemy vehicle difficulty settings; minor adjustments 2018-10-12 03:13:33 +03:00
Vasyl Horbachenko
0015667829 new frontline position finding method; AA for strikes; other minor fixes and adjustments 2018-10-12 00:12:25 +03:00
Vasyl Horbachenko
35a7da2816 oil strike objectives correct placement; updated starting point for gulf 2018-10-11 04:42:38 +03:00
Vasyl Horbachenko
5bbf3fc49f fixed land map for gulf; ability to get logs from settings; minor trigger updates 2018-10-11 04:12:02 +03:00
Vasyl Horbachenko
7a8dfeb819 Merge remote-tracking branch 'origin/develop' into develop 2018-10-11 03:45:31 +03:00
Vasyl Horbachenko
e28a24c875 randomized strike objects with templates; forbid ground objects and vehicles placement on mountains and in forests; updated push trigger so it include player group; adjacent CP missions could be initiated from carriers 2018-10-11 03:45:20 +03:00
Vasyl Horbachenko
823c6a6137 Merge pull request #27 from Khopa/combined_arms_slots
Combined arms slots
2018-10-03 02:17:21 +03:00
Khopa
2cbe63f162 Fixed potential exception in start if user enter invalid value inside the combined arms slots entry. 2018-09-29 11:55:42 +02:00
Khopa
93d0746d3e Replaced with player faction check by coalition check 2018-09-29 11:46:15 +02:00
Khopa
8cb7c7378f Type for ca_slot_entry 2018-09-29 11:34:22 +02:00
Khopa
3500c85e8d UI for CA slot selection (align with unit count rows) 2018-09-22 13:42:51 +02:00
Khopa
c699567c73 Only generate Combined Arms "Tactical Commander" slots for player side. 2018-09-22 13:33:31 +02:00
Khopa
056c397e68 [Cherry Picked] Added possibility to add 'DCS: Combined Arms' slots to the generated mission. 2018-09-22 11:46:40 +02:00
Vasyl Horbachenko
8431c7745d fixed base atttack op 2018-09-15 00:30:40 +03:00
Vasyl Horbachenko
8df4607e50 Update README.md 2018-09-14 23:49:50 +03:00
Vasyl Horbachenko
edf9efddf9 update to ground objects parser; waypoints in briefings & general briefings update; minor fixes 2018-09-13 05:09:57 +03:00
Vasyl Horbachenko
03fc17fae6 new ground objects format & parser; place dead objects instead of removing them completely 2018-09-12 05:19:21 +03:00
Vasyl Horbachenko
6fb342a42c updated location argument; updated ground units placement during attack operation 2018-09-12 00:20:35 +03:00
Vasyl Horbachenko
262347f8c8 tweaked caucasus start times 2018-09-11 22:46:26 +03:00
Vasyl Horbachenko
1176b92073 capture armor placement tweaks 2018-09-11 17:39:23 +03:00
Vasyl Horbachenko
afb084ebf8 minor bugfixes 2018-09-11 05:55:05 +03:00
Vasyl Horbachenko
12853feec3 updated version check 2018-09-11 03:35:52 +03:00
Vasyl Horbachenko
d31876b65e FARP spawn for helis on frontline cas; updated ground object placements 2018-09-11 03:12:33 +03:00
Vasyl Horbachenko
ca521e7e51 FARPs for heli flights WIP 2018-09-10 23:12:04 +03:00
Vasyl Horbachenko
f21c515d5c convert resource files to gif 2018-09-10 21:46:34 +03:00
Vasyl Horbachenko
fa5259d1f2 minor fixes before rc 2018-09-10 01:48:38 +03:00
Vasyl Horbachenko
7a5361c057 minor updates and strike mission objects expand 2018-09-10 01:38:50 +03:00
Vasyl Horbachenko
40bfb6fa88 strike operations fixes 2018-09-09 23:56:30 +03:00
Vasyl Horbachenko
61a237d1ae proper settings for barcap 2018-09-09 20:57:32 +03:00
Vasyl Horbachenko
cf7276b528 updated AA units 2018-09-09 20:39:49 +03:00
Vasyl Horbachenko
67f69d0a7f updated AA units 2018-09-09 20:27:53 +03:00
Vasyl Horbachenko
a918914431 dont limit aircraft to predefined role; better scrambling screen 2018-09-09 20:21:07 +03:00
Vasyl Horbachenko
49f2c00d76 Update README.md 2018-09-09 04:39:53 +03:00
Vasyl Horbachenko
d284305323 Update README.md 2018-09-09 04:39:03 +03:00
Vasyl Horbachenko
c32ac8577c Update README.md 2018-09-09 04:38:47 +03:00
Vasyl Horbachenko
4ba1dd87e8 updates to strike missions; frontline operations invalid units placement fixed; minor UI updates 2018-09-09 04:15:44 +03:00
Vasyl Horbachenko
e0d82da6cb save game version check; mission description in briefings; minor fixes and improvements; predefined ground objects (statics) support; strike mission WIP 2018-09-09 01:21:32 +03:00
Vasyl Horbachenko
2179e4af47 Merge pull request #12 from Khopa/ui_dev
Modified UI Style
2018-09-08 19:15:52 +03:00
Vasyl Horbachenko
0d5530f5ea Update README.md 2018-08-15 15:14:19 +03:00
Vasyl Horbachenko
a528249062 Update README.md 2018-08-15 15:13:49 +03:00
Khopa
75d734d6e4 Typo in UI 2018-08-09 13:08:07 +02:00
Khopa
1d73affa08 New style for event result menu 2018-08-08 23:40:10 +02:00
Khopa
834ee30c86 Style for event result menu 2018-08-08 23:18:49 +02:00
Khopa
d236b8a94d UI alignement in base menu 2018-08-08 22:38:09 +02:00
Khopa
a132cba7ef Changed the color of text on map (easier to read this way for now imo) 2018-08-08 22:30:33 +02:00
Khopa
86706231e0 New style for event/mission menu 2018-08-08 22:30:12 +02:00
Khopa
5eb921948a Display the units x/x/x value of the base in the base menu. 2018-08-08 21:52:09 +02:00
Khopa
fd54a5f86c Display the name of the base in base menu 2018-08-08 21:41:16 +02:00
Khopa
1b2ad5b419 Improved style of main menu, configuration menu, and base menu 2018-08-08 21:22:00 +02:00
Khopa
14cd54668e Improved style of new game menu 2018-08-08 18:45:25 +02:00
Khopa
2047089b64 Added icon to ui windows. 2018-08-08 16:27:42 +02:00
Khopa
ee924ef36e Added title name to UI window 2018-08-08 16:24:20 +02:00
Vasyl Horbachenko
f9c1dd980b spamram for fa18; non-randomized groups vertical separation on spawn; argument to affect DCS folder selection 2018-08-04 04:35:27 +03:00
Vasyl Horbachenko
74c1861240 build archieve tool 2018-07-31 23:40:35 +03:00
Vasyl Horbachenko
e8226782c1 Merge pull request #8 from shdwp/develop
minor balance improvements
2018-07-31 04:17:48 +03:00
Vasyl Horbachenko
a592cf3a05 minor balance improvements 2018-07-31 04:16:17 +03:00
Vasyl Horbachenko
0b1cb0d770 Merge pull request #7 from shdwp/develop
Develop
2018-07-31 04:00:18 +03:00
Vasyl Horbachenko
a4aa1cff3a widened spawn windows for aircraft 2018-07-31 03:59:55 +03:00
Vasyl Horbachenko
6a02d2ffb6 few minor fixes; added F5 2018-07-31 03:47:50 +03:00
Vasyl Horbachenko
7458181e90 success rate calculation division by zero fix 2018-07-29 04:41:32 +03:00
Vasyl Horbachenko
ec28cdc936 version bump 2018-07-29 04:20:30 +03:00
Vasyl Horbachenko
9dbc9a8a56 prompt window with logs on raised exception; minor UI updates; minor fixes 2018-07-29 04:16:39 +03:00
Vasyl Horbachenko
73d4a2d414 moved viggen to CAS; tanker position fix on enemy attacks; more logs 2018-07-28 04:58:28 +03:00
Vasyl Horbachenko
e93ad8b800 adjusted budget amounts 2018-07-20 04:08:04 +03:00
Vasyl Horbachenko
d48985ca6d new airports for PG; TACANs and ICLS; list frequencies in mission briefing; carrier ops improvements; cold start option 2018-07-20 04:06:13 +03:00
Vasyl Horbachenko
5f7d717b63 fixed sir abu having a frontline 2018-07-19 01:07:24 +03:00
Vasyl Horbachenko
e266698e68 fixed start.bat for usernames w/ spaces; fixed triggers for player defending; minor fixes 2018-07-19 00:57:15 +03:00
Vasyl Horbachenko
e8098e795c added symlink to pydcs to resources/tools 2018-07-18 03:20:18 +03:00
Vasyl Horbachenko
8d69724272 env gen minor update 2018-07-18 03:14:25 +03:00
Vasyl Horbachenko
683114f916 updates to CAP op 2018-07-18 00:45:55 +03:00
Vasyl Horbachenko
3b454470f9 Merge branch 'improved_cas' of https://github.com/shdwp/dcs_pmcliberation into improved_cas 2018-07-17 23:31:58 +03:00
Vasyl Horbachenko
f40f83bb09 fixes and improvements for fronline CAP 2018-07-17 14:22:45 +03:00
Vasyl Horbachenko
932bec2f84 fixes to frontline attack; frontline CAP WIP 2018-07-17 14:22:45 +03:00
Vasyl Horbachenko
820820eb92 frontline attack ops 2018-07-17 14:22:45 +03:00
Vasyl Horbachenko
6f5835a2b8 Improved Frontline CAS 2018-07-17 14:21:50 +03:00
Vasiliy Horbachenko
07128bb5e6 fixed capture issue when two or more points were attacking the base 2018-07-17 05:53:55 +03:00
Vasyl Horbachenko
b302372d4c fixes and improvements for fronline CAP 2018-07-17 05:22:41 +03:00
Vasyl Horbachenko
cad7d2c735 fixes to frontline attack; frontline CAP WIP 2018-07-17 02:14:46 +03:00
Vasyl Horbachenko
e4c3f8bce2 frontline attack ops 2018-07-16 23:58:01 +03:00
Vasyl Horbachenko
1fbf4e292a Merge branch 'improved_cas' of https://github.com/shdwp/dcs_pmcliberation into improved_cas 2018-07-16 23:03:47 +03:00
Vasyl Horbachenko
62f5b2d06d Improved Frontline CAS 2018-07-16 23:02:08 +03:00
Vasyl Horbachenko
3831658162 more base recovery after attack 2018-07-16 22:55:29 +03:00
Vasyl Horbachenko
b2545e4de0 Improved Frontline CAS 2018-07-16 22:19:19 +03:00
Vasyl Horbachenko
26e43f5e54 Merge branch 'master' of https://github.com/shdwp/dcs_liberation 2018-07-16 22:12:31 +03:00
Vasyl Horbachenko
44ba5c32c6 use correct join char for paths 2018-07-16 22:11:47 +03:00
Vasyl Horbachenko
ce7d3a89c0 fixed user path for regular DCS 2018-07-16 22:11:03 +03:00
Vasyl Horbachenko
c74b1205df fixed user path for regular DCS 2018-07-16 22:05:42 +03:00
Vasyl Horbachenko
58dd16219b removed observer slots 2018-07-15 08:55:20 +03:00
Vasyl Horbachenko
e0e1d0238f nevada debugging code removal 2018-07-15 05:55:38 +03:00
Vasyl Horbachenko
71e22bdb21 Merge remote-tracking branch 'origin/master' 2018-07-15 04:21:07 +03:00
Vasyl Horbachenko
3c2025ab44 minor hotfixes in mp debriefing (WIP) 2018-07-15 04:20:55 +03:00
Vasyl Horbachenko
d314e22970 Update start.bat 2018-07-13 04:59:59 +03:00
Vasyl Horbachenko
ff7181b648 updated .gitignore to ignore payload resource files 2018-07-13 04:34:56 +03:00
Vasyl Horbachenko
4cbd30fdbc debriefing based on events, not world state; tweaked visualgen; vehicles will not group in single location during capture op; fixed triggers for carrier ops; fixed naval ops; correct speed for inflight heli spawns; awacs will not change it's freq 2018-07-13 04:33:07 +03:00
Vasiliy Horbachenko
4ce7480df8 debriefing based on events, not world state WIP 2018-07-12 05:12:01 +03:00
Vasyl Horbachenko
d2aede593b added files missing from git 2018-07-05 03:13:30 +03:00
Vasyl Horbachenko
32fb5ad0e2 fixed a number of issues: user directory on different drive, carrier ops, multiplayer debriefing parser, multiplayer mission generation; added su25 2018-07-05 02:42:46 +03:00
Vasyl Horbachenko
fa55ae1fcc fixed multiplayer mission start 2018-07-04 05:20:35 +03:00
Vasyl Horbachenko
90fbe77682 fixed unplayable F-5 2018-07-04 04:51:31 +03:00
Vasyl Horbachenko
26a7609875 fixed crash on mission debriefing 2018-07-04 04:44:58 +03:00
1013 changed files with 70851 additions and 36579 deletions

58
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: Build
on: [push, pull_request]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install environment
run: |
python -m venv ./venv
- name: Install dependencies
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: mypy game
run: |
./venv/scripts/activate
mypy game
- name: mypy gen
run: |
./venv/scripts/activate
mypy gen
- name: mypy theater
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: |
./venv/scripts/activate
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec
- uses: actions/upload-artifact@v2
with:
name: dcs_liberation
path: dist/

126
.github/workflows/release.yml vendored Normal file
View File

@@ -0,0 +1,126 @@
name: Release Pipeline
on:
push:
tags: [ '*' ]
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Install environment
run: |
python -m venv ./venv
- name: Install dependencies
run: |
./venv/scripts/activate
python -m pip install -r requirements.txt
# For some reason the shiboken2.abi3.dll is not found properly, so I copy it instead
Copy-Item .\venv\Lib\site-packages\shiboken2\shiboken2.abi3.dll .\venv\Lib\site-packages\PySide2\ -Force
- name: Finalize version
run: |
New-Item -ItemType file resources\final
- name: mypy game
run: |
./venv/scripts/activate
mypy game
- name: mypy gen
run: |
./venv/scripts/activate
mypy gen
- name: mypy theater
run: |
./venv/scripts/activate
mypy theater
- name: Build binaries
run: |
./venv/scripts/activate
$env:PYTHONPATH=".;./pydcs"
pyinstaller pyinstaller.spec
- name: Create Installer
env:
TAG_NAME: ${{ github.ref }}
run: |
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
(Get-Content .\installer\dcs_liberation.iss) -replace "{{version}}",$version | Out-File .\build\installer.iss
cd .\installer
iscc.exe ..\build\installer.iss
cd ..
Copy-Item .\changelog.md .\dist
- uses: actions/upload-artifact@v2
with:
name: dcs_liberation
path: dist/
release:
needs: [ build ]
runs-on: windows-latest
steps:
- uses: actions/download-artifact@v2
with:
name: dcs_liberation
- name: "Get Version"
id: version
env:
TAG_NAME: ${{ github.ref }}
run: |
Get-ChildItem -Recurse -Depth 1
$version = ($env:TAG_NAME -split "/") | Select-Object -Last 1
$prerelease = ("2.1.1-alpha3" -match '[^\.\d]').ToString().ToLower()
Write-Host $version
Write-Host $prerelease
Write-Output "::set-output name=number::$version"
Write-Output "::set-output name=prerelease::$prerelease"
$changelog = Get-Content .\changelog.md
$last_change = ($changelog | Select-String -Pattern "^#\s" | Select-Object -Skip 1 -First 1).LineNumber - 2
($changelog | Select-Object -First $last_change) -join "`n" | Out-File .\releasenotes.md
Compress-Archive -Path .\dcs_liberation -DestinationPath "dcs_liberation.$version.zip" -Compression Optimal
- uses: actions/create-release@v1
id: create_release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: ${{ github.ref }}
body_path: releasenotes.md
draft: false
prerelease: ${{ steps.version.outputs.prerelease }}
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dcs_liberation.exe
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.exe
asset_content_type: application/exe
- uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dcs_liberation.${{ steps.version.outputs.number }}.zip
asset_name: dcs_liberation.${{ steps.version.outputs.number }}.zip
asset_content_type: application/zip

24
.gitignore vendored
View File

@@ -1,12 +1,22 @@
*.pyc
__pycache__
build/*
build/**
resources/payloads/*.lua
venv
logs.txt
.DS_Store
dist/**
a.py
resources/tools/a.miz
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/dictionaries
.idea/**/shelf
.idea/misc.xml
.idea/*.iml
.idea/
/kneeboards
/liberation_preferences.json
/state.json
logs/
qt_ui/logs/liberation.log
*.psd

5
.gitmodules vendored
View File

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

View File

@@ -1,10 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
<option name="processCode" value="true" />
<option name="processLiterals" value="true" />
<option name="processComments" value="true" />
</inspection_tool>
</profile>
</component>

8
.idea/modules.xml generated
View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/dcs_pmcliberation.iml" filepath="$PROJECT_DIR$/.idea/dcs_pmcliberation.iml" />
</modules>
</component>
</project>

11
.idea/vcs.xml generated
View File

@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitSharedSettings">
<option name="FORCE_PUSH_PROHIBITED_PATTERNS">
<list />
</option>
</component>
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

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

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

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

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

View File

@@ -1,8 +1,40 @@
[DCS World](https://www.digitalcombatsimulator.com/en/products/world/) single-player liberation dynamic campaign.
![Logo](https://i.imgur.com/c2k18E1.png)
[Installation instructions/Manual](https://github.com/shdwp/dcs_liberation/wiki)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?logo=paypal)](https://www.paypal.com/paypalme/KhopaDCSL)
[![Patreon](https://img.shields.io/badge/patreon-become%20a%20patron-orange?logo=patreon)](https://patreon.com/khopa)
Inspired by *ARMA Liberation* mission.
[![Download](https://img.shields.io/github/downloads/khopa/dcs_liberation/total?label=Download)](https://github.com/Khopa/dcs_liberation/releases)
Uses [pydcs](http://github.com/pydcs/dcs) for mission generation.
[![Discord](https://img.shields.io/discord/595702951800995872?label=Discord&logo=discord)](https://discord.gg/bKrtrkJ)
[![GitHub pull requests](https://img.shields.io/github/issues-pr/khopa/dcs_liberation)](https://github.com/Khopa/dcs_liberation)
[![GitHub issues](https://img.shields.io/github/issues/khopa/dcs_liberation)](https://github.com/Khopa/dcs_liberation/issues)
![GitHub stars](https://img.shields.io/github/stars/khopa/dcs_liberation?style=social)
## About DCS Liberation
DCS Liberation is a [DCS World](https://www.digitalcombatsimulator.com/en/products/world/) turn based single-player or co-op dynamic campaign.
It is an external program that generates full and complex DCS missions and manage a persistent combat environment.
![Logo](https://i.imgur.com/4hq0rLq.png)
## Downloads
Latest release is available here : https://github.com/Khopa/dcs_liberation/releases
## Resources
Tutorials, contributors and developer's guides are available in the project's [Wiki](https://github.com/Khopa/dcs_liberation/wiki/)
## Special Thanks
First, a big thanks to shdwp, for starting the original DCS Liberation project.
Then, DCS Liberation uses [pydcs](http://github.com/pydcs/dcs) for mission generation, and nothing would be possible without this.
It also uses the popular [Mist](https://github.com/mrSkortch/MissionScriptingTools) lua framework for mission scripting.
Excellent lua scripts DCS Liberation uses as plugins:
* For the JTAC feature, DCS Liberation embeds Ciribob's JTAC Autolase [script](https://github.com/ciribob/DCS-JTACAutoLaze).
* Walder's [Skynet-IADS](https://github.com/walder/Skynet-IADS) is used for Integrated Air Defense System.
Please also show some support to these projects !

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env python3
import os
import dcs
import theater.caucasus
import theater.persiangulf
import theater.nevada
import ui.window
import ui.mainmenu
import ui.newgamemenu
import ui.corruptedsavemenu
from game.game import Game
from theater import start_generator
from userdata import persistency
dcs.planes.FlyingType.payload_dirs.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "resources\\payloads"))
def proceed_to_main_menu(game: Game):
m = ui.mainmenu.MainMenu(w, None, game)
m.display()
w = ui.window.Window()
try:
game = persistency.restore_game()
if not game:
new_game_menu = None # type: NewGameMenu
def start_new_game(player_name: str, enemy_name: str, terrain: str, sams: bool, midgame: bool, multiplier: float):
if terrain == "persiangulf":
conflicttheater = theater.persiangulf.PersianGulfTheater()
elif terrain == "nevada":
conflicttheater = theater.nevada.NevadaTheater()
else:
conflicttheater = theater.caucasus.CaucasusTheater()
if midgame:
for i in range(0, int(len(conflicttheater.controlpoints) / 2)):
conflicttheater.controlpoints[i].captured = True
start_generator.generate_initial(conflicttheater, enemy_name, sams, multiplier)
game = Game(player_name=player_name,
enemy_name=enemy_name,
theater=conflicttheater)
game.budget = int(game.budget * multiplier)
game.settings.multiplier = multiplier
game.settings.sams = sams
if midgame:
game.budget = game.budget * 4 * len(list(conflicttheater.conflicts()))
proceed_to_main_menu(game)
new_game_menu = ui.newgamemenu.NewGameMenu(w, start_new_game)
new_game_menu.display()
else:
proceed_to_main_menu(game)
except Exception as e:
ui.corruptedsavemenu.CorruptedSaveMenu(w).display()
w.run()

338
changelog.md Normal file
View File

@@ -0,0 +1,338 @@
# 2.2.1
# Features/Improvements
* **[Factions]** Added factions : Georgia 2008, USN 1985, France 2005 Frenchpack by HerrTom
* **[Factions]** Added map Persian Gulf full by Plob
* **[Flight Planner]** Player flights with start delays under ten minutes will spawn immediately.
* **[UI]** Mission start screen now informs players about delayed flights.
* **[Units]** Added support for F-14A-135-GR
* **[Modding]** Possible to setup liveries overrides in factions definition files
## Fixes :
* **[Flight Planner]** Hold, join, and split points are planned cautiously near enemy airfields. Ascend/descend points are no longer planned.
* **[Flight Planner]** Custom waypoints are usable again. Not that in most cases custom flight plans will revert to the 2.1 flight planning behavior.
* **[Flight Planner]** Fixed UI bug that made it possible to create empty flights which would throw an error.
* **[Flight Planner]** Player flights from carriers will now be delayed correctly according to the player's settings.
* **[Misc]** Spitfire variant with clipped wings was not seen as flyable by DCS Liberation (hence could not be setup as client/player slot)
* **[Misc]** Updated Syria terrain parking slots database, the out-of-date database could end up generating aircraft in wrong slots (We are still experiencing issues with somes airbases, such as Khalkhalah though)
# 2.2.0
## 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
## Features/Improvements :
* **[Units/Factions]** Enabled EPLRS for ground units that supports it (so they appear on A-10C II TAD and Helmet)
## Fixes :
* **[UI]** Fixed an issue that prevent saving after aborting a mission
* **[Mission Generator]** Fixed aircraft landing point type being wrong
# 2.1.4
## Fixes :
* **[UI]** Fixed an issue that prevented generating the mission (take off button no working) on old savegames.
## Features/Improvements :
* **[Units/Factions]** Added A-10C_2 to USA 2005 and Bluefor modern factions
* **[UI]** Limit number of aircraft that can be bought to the number of available parking slots.
* **[Mission Generator]** Use inline loading of the JSON.lua library, and save to either %LIBERATION_EXPORT_DIR%, or to DCS working directory
## Changes :
* **[Units/Factions]** Bluefor generic factions will now use the new "Combined Joint Task Forces Blue" country in the generated mission instead of "USA"
## Fixes :
* **[UI]** Fixed icon for Viggen
* **[UI]** Added icons for some ground units
* **[Misc]** Fixed issue with Chinese characters in pydcs preventing generating the mission. (Take Off button not working) (thanks to spark135246)
* **[Misc]** Fixed an error causing with ATC frequency preventing generating the mission. (Take Off button not working) (thanks to danalbert)
# 2.1.2
## Fixes :
* **[Mission Generator]** Fix mission generation issues with radio frequencies (Thanks to contributors davidp57 and danalbert)
* **[Mission Generator]** AI should now properly plan flights for Tornados
# 2.1.1
## Features/Improvements :
* **[Other]** Added an installer option (thanks to contributor parithon)
* **[Kneeboards]** Generate mission kneeboards for player flights. Kneeboards include
airfield/carrier information (ATC frequencies, ILS, TACAN, and runway
assignments), assigned radio channels, waypoint lists, and AWACS/JTAC/tanker
information. (Thanks to contributor danalbert)
* **[Radios]** Allocate separate intra-flight channels for most aircraft to reduce global
chatter. (Thanks to contributor danalbert)
* **[Radios]** Configure radio channel presets for most aircraft. Currently supported are:
* AJS37
* AV-8B
* F-14B
* F-16C
* F/A-18C
* JF-17
* M-2000C (Thanks to contributor danalbert)
* **[Base Menu]** Added possibility to repair destroyed SAM and base defenses units for the player (Click on a SAM site to fix it)
* **[Base Menu]** Added possibility to buy/sell/replace SAM units
* **[Map]** Added recon images for buildings on strike targets, click on a Strike target to get detailled informations
* **[Units/Factions]** Added F-16C to USA 1990
* **[Units/Factions]** Added MQ-9 Reaper as CAS unit for USA 2005
* **[Units/Factions]** Added Mig-21, Mig-23, SA-342L to Syria 2011
* **[Cheat Menu]** Added buttons to remove money
## Fixed issues :
* **[UI/UX]** Spelling issues (Thanks to contributor steveveepee)
* **[Campaign Generator]** LHA was placed on land in Syrian Civil War campaign
* **[Campaign Generator]** Fixed inverted configuration for Syria full map
* **[Campaign Generator]** Syria "Inherent Resolve" campaign, added Incirlik Air Base
* **[Mission Generator]** AH-1W was not used by AI to generate CAS mission by default
* **[Mission Generator]** Fixed F-16C targeting pod not being added to payload
* **[Mission Generator]** AH-64A and AH-64D payloads fix.
* **[Units/Factions]** China will use KJ-2000 as awacs instead of A-50
# 2.1.0
## Features/Improvements :
* **[Campaign Generator]** Added Syria map
* **[Campaign Generator]** Added 5 campaigns for the Syria map
* **[Campaign Generator]** Added 2 small scale campaign for Persian Gulf map
* **[Units/Factions]** Added factions for Syria map : Syria 2011, Arab Armies 1982, 1973, 1968, 1948, Israel 1982, 1973, 1948
* **[Base Menu]** Budget is visible in recruitment menu. (Thanks to Github contributor root0fall)
* **[Misc]** Added error message in mission when the state file can not be written
* **[Units/Factions]** China, Pakistan, UAE will now use the new WingLoong drone as JTAC instead of the MQ-9 Reaper
* **[Units/Factions]** Minor changes to Syria 2011 and Turkey 2005 factions
* **[UI]** Version number is shown in about dialog
## Fixed issues :
* **[Mission Generator]** Caucasus terrain improvement on exclusions zone (added forests between Vaziani and Beslan to exclusion zones)
* **[Mission Generator]** The first unit of every base defenses group could not be controlled with Combined Arms.
* **[Mission Generator]** Reduced generated helicopter altitude for CAS missions
* **[Mission Generator]** F-16C default CAS payload was asymmetric, fixed.
* **[Mission Generator]** AH-1W couldn't be bought, and added default payloads.
* **[UI/UX]** Fixed Mi-28N missing thumbnail
* **[UI/UX]** Fixed list of flights not refreshing when changing the mission departure (T+).
# 2.0.11
## Features/Improvements :
* **[Units/Factions]** Added Mig-31, Su-30, Mi-24V, Mi-28N to Russia 2010 faction.
* **[Units/Factions]** Added F-15E to USA 2005 and USA 1990 factions.
* **[Mission Generator]** Added a parameter to choose whether the JTACs should use smoke markers or not
## Fixed issues :
* **[Units/Factions]** Fixed big performance issue in new release UI that occurred only when running the .exe
* **[Units/Factions]** Fixed mission generation not working with Libya faction
* **[Units/Factions]** Fixed OH-58D not being used by AI
* **[Units/Factions]** Typo in UK 1990 name (fixed by bwRavencl)
* **[Units/Factions]** Fixed Tanker Tacan channel not being the same as the briefing one. (Sorry)
* **[Mission Generator]** Neutral airbases services will now be disabled. (Not possible to refuel or re-arm there)
* **[Mission Generator]** AI will be configured to limit afterburner usage
* **[Mission Generator]** JTAC will not use laser codes above 1688 anymore
* **[Mission Generator]** JTAC units were misconfigured and would not be invisible/immortal.
* **[Mission Generator]** Increased JTAC status message duration to 25s, so you have more time to enter coordinates;
* **[Mission Generator]** Destroyed units carcass will not appear on airfields to avoid having a destroyed vehicle blocking a runway or taxiway.
# 2.0.10
## Features/Improvements :
* **[Misc]** Now possible to save game in a different file, and to open DCS Liberation savegame files. (You are not restricted to a single save file anymore)
* **[UI/UX]** New dark UI Theme and default theme improvement by Deus
* **[UI/UX]** New "satellite" map backgrounds
* **[UX]** Base menu is opened with a single mouse click
* **[Units/Factions/Mods]** Added Community A-4E-C support for faction Bluefor Cold War
* **[Units/Factions/Mods]** Added MB-339PAN support for faction Bluefor Cold War
* **[Units/Factions/Mods]** Added Rafale AI mod support
* **[Units/Factions/Mods]** Added faction "France Modded" with units from frenchpack v3.5 mod
* **[Units/Factions/Mods]** Added faction "Insurgent modded" with Insurgent units from frenchpack v3.5 mod (Toyota truck)
* **[Units/Factions/Mods]** Added factions Canada 2005, Australia 2005, Japan 2005, USA Aggressors, PMC
* **[New Game Wizard]** Added the list of required mods for modded factions.
* **[New Game Wizard]** No more RED vs BLUE opposing faction restrictions.
* **[New Game Wizard]** New campaign generation settings added : No aircraft carrier, no lha, no navy, invert map starting positions.
* **[Mission Generator]** Artillery units will start firing mission after a random delay. It should reduces lag spikes induced by artillery strikes by spreading them out.
* **[Mission Generator]** Ground units will retreat after taking too much casualties. Artillery units will retreat if engaged.
* **[Mission Generator]** The briefing will now contain the carrier ATC frequency
* **[Mission Generator]** The briefing contains a small situation update.
* **[Mission Generator]** Previously destroyed units are visible in the mission. (And added a performance settings to disable this behaviour)
* **[Mission Generator]*c* Basic JTAC on Frontlines
* **[Campaign Generator]** Added Tarawa in caucasus campaigns
* **[Campaign Generator]** Tuned the various existing campaign parameters
* **[Campaign Generator]** Added small campaign : "Russia" on Caucasus Theater
## Fixed issues :
* **[Mission Generator]** Carrier will sail into the wind, not in the same direction
* **[Mission Generator]** Carrier cold start was not working (flight was starting warm even when cold was selected)
* **[Mission Generator]** Carrier group ships are more spread out
* **[Mission Generator]** Fixed wrong radio frequency for german WW2 warbirds
* **[Mission Generator]** Fixed FW-190A8 spawning with bomb rack for CAP missions
* **[Mission Generator]** Fixed A-20G spawning with no payload
* **[Mission Generator]** Fixed Su-33 spawning too heavy to take off from carrier
* **[Mission Generator]** Fixed Harrier AV-8B spawning too heavy to take off from tarawa
* **[Mission Generator]** Base defense units were not controllable with Combined Arms
* **[Mission Generator]** Tanker speed was too low
* **[Mission Generator]** Tanker TACAN settings were wrong
* **[Mission Generator]** AI aircraft should start datalink ON (EPLRS)
* **[Mission Generator]** Base defense units should not spawn on runway and or taxyway. (The chance for this to happen should now be really really low)
* **[Mission Generator]** Fixed all flights starting "In flight" after playing a few missions (parking slot reset issue)
* **[Mission Script/Performance]** Mission lua script will not listen to weapons fired event anymore and register every fired weapons. This should improve performance especially in WW2 scenarios or when rocket artillery is firing.
* **[Campaign Generator]** Carrier name will now not appear for faction who do not have carriers
* **[Campaign Generator]** SA-10 sites will now have a tracking radar.
* **[Units/Factions]** Remove JF-17 from USA 2005 faction
* **[Units/Factions]** Remove AJS-37 from Russia 2010
* **[Units/Factions]** Removed Oliver Hazard Perry from cold war factions (too powerful sam system for the era)
* **[Bug]** On the persian gulf full map campaign, the two carriers were sharing the same id, this was causing a lot of bugs
* **[Performance]** Tuned the culling setting so that you cannot run into situation where no friendly or enemy AI flights are generated
* **[Other]** Application doesn't gracefully exit.
* **[Other]** Other minor fixes, and multiples factions small changes
# 2.0 RC 9
## Features/Improvements :
* **[UI/UX]** New icons from contributor Deus
## Fixed issues :
* **[Mission Generator]** Carrier TACAN was wrongfully set up as an A/A TACAN
* **[Campaign Generator]** Fixed issue with Russian navy group generator causing a random crash on campaign creation.
# 2.0 RC 8
## Fixed issues :
* **[Mission Generator]** Frequency for P-47D-30 changed to 124Mhz (Generated mission with 251Mhz would not work)
* **[Mission Generator]** Reduced the maximum number of uboat per generated group
* **[Mission Generator]** Fixed an issue with the WW2 LST groups (superposed units).
* **[UI]** Fixed issue with the zoom
# 2.0 RC 7
## Features/Improvements :
* **[Units/Factions]** Added P-47D-30 for factions allies_1944
* **[Units/Factions]** Added factions : Bluefor Coldwar, Germany 1944 Easy
* **[Campaign/Map]** Added a campaign in the Channel map
* **[Campaign/Map]** Changed the Normandy campaign map
* **[Campaign/Map]** Added new campaign Normandy Small
* **[Mission Generator]** AI Flight generator has been reworked
* **[Mission Generator]** Add PP points for JF-17 on STRIKE missions
* **[Mission Generator]** Add ST point for F-14B on STRIKE missions
* **[Mission Generator]** Flights with client slots will never be delayed
* **[Mission Generator]** AI units can start from parking (With a new setting in Settings Window to disable it)
* **[Mission Generator]** Tacan for carrier will only be in Mode X from now
* **[Mission Generator]** RTB waypoints for autogenerated flights
* **[Flight Planner]** Added CAS mission generator
* **[Flight Planner]** Added CAP mission generator
* **[Flight Planner]** Added SEAD mission generator
* **[Flight Planner]** Added STRIKE mission generator
* **[Flight Planner]** Added buttons to add autogenerated waypoints (ASCEND, DESCEND, RTB)
* **[Flight Planner]** Improved waypoint list
* **[Flight Planner]** WW2 factions uses different parameters for flight planning.
* **[Settings]** Added settings to disallow external views
* **[Settings]** Added settings to choose F10 Map mode (All, Allies only, Player only, Fog of War, Map Only)
* **[Settings]** Added settings to choose whether to auto-generate objective marks on the F10 map
* **[Info Panel]** Added information about destroyed buildings in info panel
* **[Info Panel]** Added information about destroyed units at SAM site in info panel
* **[Debriefing]** Added information about units destroyed outside the frontline in the debriefing window
* **[Debriefing]** Added destroyed buildings in the debriefing window
* **[Map]** Tooltip now contains the list of building for Strike targets on the map
* **[Map]** Added "Oil derrick" building
* **[Map]** Added "ww2 bunker" building (WW2)
* **[Map]** Added "ally camp" building (WW2)
* **[Map]** Added "V1 Site" (WW2)
* **[Misc]** Made it possible to setup DCS Saved Games directory and DCS installation directory manually at first start
* **[Misc]** Added culling performance settings
## Fixed issues :
* **[Units/Factions]** Replaced S3-B Tanker by KC130 for most factions (More fuel)
* **[Units/Factions]** WW2 factions will not have offshore oil station and other modern buildings generated. No more third-reich operated offshore stations will spawn on normandy's coast.
* **[Units/Factions]** Aircraft carrier will try to move in the wind direction
* **[Units/Factions]** Missing icons added for some aircraft
* **[Mission Generator]** When playing as RED the activation trigger would not be properly generated
* **[Mission Generator]** FW-190A8 is now properly considered as a flyable aircraft
* **[Mission Generator]** Changed "strike" payload for Su-24M that was ineffective
* **[Mission Generator]** Changed "strike" payload for JF-17 to use LS-6 bombs instead of GBU
* **[Mission Generator]** Change power station template. (Buildings could end up superposed).
* **[Maps/Campaign]** Now using Vasiani airbase instead of Soganlung airport in Caucasus campaigns (more parking slot)
* **[Info Panel]** Message displayed on base capture event stated that the enemy captured an airbase, while it was the player who captured it.
* **[Map View]** Graphical glitch on map when one building of an objective was destroyed, but not the others
* **[Mission Planner]** The list of flights was not updated on departure time change.
# 2.0 RC 6
Saves file from RC5 are not compatible with the new version.
Sorry :(
## Features/Improvements :
* **[Units/Factions]** Supercarrier support (You have to go to settings to enable it, if you have the supercarrier module)
* **[Units/Factions]** Added 'Modern Bluefor' factions, containing all most popular DCS flyable units
* **[Units/Factions]** Factions US 2005 / 1990 will now sometimes have Arleigh Burke class ships instead of Perry as carrier escorts
* **[Units/Factions]** Added support for newest WW2 Units
* **[Campaign logic]** When a base is captured, refill the "base defenses" group with units for the new owner.
* **[Mission Generator]** Carrier ICLS channel will now be configured (check your briefing)
* **[Mission Generator]** SAM units will spawn on RED Alarm state
* **[Mission Generator]** AI Flight planner now creates its own STRIKE flights
* **[Mission Generator]** AI units assigned to Strike flight will now actually engage the buildings they have been assigned.
* **[Mission Generator]** Added performance settings to allow disabling : smoke, artillery strike, moving units, infantry, SAM Red alert mode.
* **[Mission Generator]** Using Late Activation & Trigger in attempt to improve performance & reduce stutter (Previously they were spawned through 'ETA' feature)
* **[UX]** : Improved flight selection behaviour in the Mission Planning Window
## Fixed issues :
* **[Mission Generator]** Payloads were not correctly assigned in the release version.
* **[Mission Generator]** Game generation does not work when "no night mission" settings was selected and the current time was "day"
* **[Mission Generator]** Game generation does not work when the player selected faction has no AWACS
* **[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)

1
dcs
View File

@@ -1 +0,0 @@
submodules/dcs/dcs

View File

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

21
game/data/aaa_db.py Normal file
View File

@@ -0,0 +1,21 @@
from dcs.vehicles import AirDefence
AAA_UNITS = [
AirDefence.SPAAA_Gepard,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.AAA_Vulcan_M163,
AirDefence.AAA_ZU_23_Closed,
AirDefence.AAA_ZU_23_Emplacement,
AirDefence.AAA_ZU_23_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent_Closed,
AirDefence.AAA_ZU_23_Insurgent_on_Ural_375,
AirDefence.AAA_ZU_23_Insurgent,
AirDefence.AAA_8_8cm_Flak_18,
AirDefence.AAA_Flak_38,
AirDefence.AAA_8_8cm_Flak_36,
AirDefence.AAA_8_8cm_Flak_37,
AirDefence.AAA_Flak_Vierling_38,
AirDefence.AAA_Kdo_G_40,
AirDefence.AAA_8_8cm_Flak_41,
AirDefence.AAA_Bofors_40mm
]

View File

@@ -0,0 +1,17 @@
import inspect
import dcs
DEFAULT_AVAILABLE_BUILDINGS = ['fuel', 'ammo', 'comms', 'oil', 'ware', 'farp', 'fob', 'power', 'factory', 'derrick']
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',
'Haystack 1', 'Haystack 2', 'Haystack 3', 'Haystack 4', 'Hemmkurvenvenhindernis',
'Log posts 1', 'Log posts 2', 'Log posts 3', 'Log ramps 1', 'Log ramps 2', 'Log ramps 3',
'Belgian Gate', 'Container white']
FORTIFICATION_UNITS = [c for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]
FORTIFICATION_UNITS_ID = [c.id for c in vars(dcs.vehicles.Fortification).values() if inspect.isclass(c)]

View File

@@ -0,0 +1,55 @@
from dcs.planes import (
Bf_109K_4,
C_101CC,
FW_190A8,
FW_190D9,
F_5E_3,
F_86F_Sabre,
I_16,
L_39ZA,
MiG_15bis,
MiG_19P,
MiG_21Bis,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
SpitfireLFMkIX,
SpitfireLFMkIXCW
)
from pydcs_extensions.a4ec.a4ec import A_4E_C
"""
This list contains the aircraft that do not use the guns as the last resort weapons, but as a main weapon
They'll RTB when they don't have gun ammo left
"""
GUNFIGHTERS = [
# Cold War
MiG_15bis,
MiG_19P,
MiG_21Bis,
F_86F_Sabre,
A_4E_C,
F_5E_3,
# Trainers
C_101CC,
L_39ZA,
# WW2
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
I_16,
]

119
game/data/doctrine.py Normal file
View File

@@ -0,0 +1,119 @@
from dataclasses import dataclass
from datetime import timedelta
from game.utils import nm_to_meter, feet_to_meter
@dataclass(frozen=True)
class Doctrine:
cas: bool
cap: bool
sead: bool
strike: bool
antiship: bool
strike_max_range: int
sead_max_range: int
rendezvous_altitude: int
hold_distance: int
push_distance: int
join_distance: int
split_distance: int
ingress_egress_distance: int
ingress_altitude: int
egress_altitude: int
min_patrol_altitude: int
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,
cas=True,
sead=True,
strike=True,
antiship=True,
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),
ingress_altitude=feet_to_meter(20000),
egress_altitude=feet_to_meter(20000),
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(
cap=True,
cas=True,
sead=True,
strike=True,
antiship=True,
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),
ingress_altitude=feet_to_meter(18000),
egress_altitude=feet_to_meter(18000),
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(
cap=True,
cas=True,
sead=False,
strike=True,
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),
ingress_egress_distance=nm_to_meter(7),
ingress_altitude=feet_to_meter(8000),
egress_altitude=feet_to_meter(8000),
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),
)

74
game/data/radar_db.py Normal file
View File

@@ -0,0 +1,74 @@
from dcs.ships import (
CGN_1144_2_Pyotr_Velikiy,
CG_1164_Moskva,
CVN_70_Carl_Vinson,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
CVN_74_John_C__Stennis,
CV_1143_5_Admiral_Kuznetsov,
CV_1143_5_Admiral_Kuznetsov_2017,
FFG_11540_Neustrashimy,
FFL_1124_4_Grisha,
FF_1135M_Rezky,
FSG_1241_1MP_Molniya,
LHA_1_Tarawa,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
Type_052B_Destroyer,
Type_052C_Destroyer,
Type_054A_Frigate,
USS_Arleigh_Burke_IIa,
)
from dcs.vehicles import AirDefence
UNITS_WITH_RADAR = [
# Radars
AirDefence.SAM_SA_15_Tor_9A331,
AirDefence.SAM_SA_11_Buk_CC_9S470M1,
AirDefence.SAM_Patriot_AMG_AN_MRC_137,
AirDefence.SAM_Patriot_ECS_AN_MSQ_104,
AirDefence.SPAAA_Gepard,
AirDefence.AAA_Vulcan_M163,
AirDefence.SPAAA_ZSU_23_4_Shilka,
AirDefence.EWR_1L13,
AirDefence.SAM_SA_6_Kub_STR_9S91,
AirDefence.SAM_SA_10_S_300PS_TR_30N6,
AirDefence.SAM_SA_10_S_300PS_SR_5N66M,
AirDefence.EWR_55G6,
AirDefence.SAM_SA_10_S_300PS_SR_64H6E,
AirDefence.SAM_SA_11_Buk_SR_9S18M1,
AirDefence.CP_9S80M1_Sborka,
AirDefence.SAM_Hawk_TR_AN_MPQ_46,
AirDefence.SAM_Hawk_SR_AN_MPQ_50,
AirDefence.SAM_Patriot_STR_AN_MPQ_53,
AirDefence.SAM_Hawk_CWAR_AN_MPQ_55,
AirDefence.SAM_SR_P_19,
AirDefence.SAM_Roland_EWR,
AirDefence.SAM_SA_3_S_125_TR_SNR,
AirDefence.SAM_SA_2_TR_SNR_75_Fan_Song,
AirDefence.HQ_7_Self_Propelled_STR,
# Ships
CVN_70_Carl_Vinson,
Oliver_Hazzard_Perry_class,
Ticonderoga_class,
FFL_1124_4_Grisha,
CV_1143_5_Admiral_Kuznetsov,
FSG_1241_1MP_Molniya,
CG_1164_Moskva,
FFG_11540_Neustrashimy,
CGN_1144_2_Pyotr_Velikiy,
FF_1135M_Rezky,
CV_1143_5_Admiral_Kuznetsov_2017,
CVN_74_John_C__Stennis,
CVN_71_Theodore_Roosevelt,
CVN_72_Abraham_Lincoln,
CVN_73_George_Washington,
USS_Arleigh_Burke_IIa,
LHA_1_Tarawa,
Type_052B_Destroyer,
Type_054A_Frigate,
Type_052C_Destroyer
]

1385
game/db.py

File diff suppressed because it is too large Load Diff

213
game/debriefing.py Normal file
View File

@@ -0,0 +1,213 @@
import json
import logging
import os
import threading
import time
import typing
from game import db
DEBRIEFING_LOG_EXTENSION = "log"
class DebriefingDeadUnitInfo:
country_id = -1
player_unit = False
type = None
def __init__(self, country_id, player_unit , type):
self.country_id = country_id
self.player_unit = player_unit
self.type = type
def __repr__(self):
return str(self.country_id) + " " + str(self.player_unit) + " " + str(self.type)
class Debriefing:
def __init__(self, state_data, game):
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"]
self.mission_ended = state_data["mission_ended"]
self.destroyed_units = state_data["destroyed_objects_positions"]
self.__destroyed_units = []
logging.info("--------------------------------")
logging.info("Starting Debriefing preprocessing")
logging.info("--------------------------------")
logging.info(self.base_capture_events)
logging.info(self.killed_aircrafts)
logging.info(self.killed_ground_units)
logging.info(self.weapons_fired)
logging.info(self.mission_ended)
logging.info(self.destroyed_units)
logging.info("--------------------------------")
self.player_country_id = db.country_id_from_name(game.player_country)
self.enemy_country_id = db.country_id_from_name(game.enemy_country)
self.dead_aircraft = []
self.dead_units = []
self.dead_aaa_groups = []
self.dead_buildings = []
for aircraft in self.killed_aircrafts:
try:
country = int(aircraft.split("|")[1])
type = db.unit_type_from_name(aircraft.split("|")[4])
player_unit = (country == self.player_country_id)
aircraft = DebriefingDeadUnitInfo(country, player_unit, type)
if type is not None:
self.dead_aircraft.append(aircraft)
except Exception as e:
logging.error(e)
for unit in self.killed_ground_units:
try:
country = int(unit.split("|")[1])
type = db.unit_type_from_name(unit.split("|")[4])
player_unit = (country == self.player_country_id)
unit = DebriefingDeadUnitInfo(country, player_unit, type)
if type is not None:
self.dead_units.append(unit)
except Exception as e:
logging.error(e)
for unit in self.killed_ground_units:
for cp in game.theater.controlpoints:
logging.info(cp.name)
logging.info(cp.captured)
if cp.captured:
country = self.player_country_id
else:
country = self.enemy_country_id
player_unit = (country == self.player_country_id)
for i, ground_object in enumerate(cp.ground_objects):
logging.info(unit)
logging.info(ground_object.group_name)
if ground_object.is_same_group(unit):
unit = DebriefingDeadUnitInfo(country, player_unit, ground_object.dcs_identifier)
self.dead_buildings.append(unit)
elif ground_object.dcs_identifier in ["AA", "CARRIER", "LHA"]:
for g in ground_object.groups:
for u in g.units:
if u.name == unit:
unit = DebriefingDeadUnitInfo(country, player_unit, db.unit_type_from_name(u.type))
self.dead_units.append(unit)
self.player_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.player_country_id]
self.enemy_dead_aircraft = [a for a in self.dead_aircraft if a.country_id == self.enemy_country_id]
self.player_dead_units = [a for a in self.dead_units if a.country_id == self.player_country_id]
self.enemy_dead_units = [a for a in self.dead_units if a.country_id == self.enemy_country_id]
self.player_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.player_country_id]
self.enemy_dead_buildings = [a for a in self.dead_buildings if a.country_id == self.enemy_country_id]
logging.info(self.player_dead_aircraft)
logging.info(self.enemy_dead_aircraft)
logging.info(self.player_dead_units)
logging.info(self.enemy_dead_units)
self.player_dead_aircraft_dict = {}
for a in self.player_dead_aircraft:
if a.type in self.player_dead_aircraft_dict.keys():
self.player_dead_aircraft_dict[a.type] = self.player_dead_aircraft_dict[a.type] + 1
else:
self.player_dead_aircraft_dict[a.type] = 1
self.enemy_dead_aircraft_dict = {}
for a in self.enemy_dead_aircraft:
if a.type in self.enemy_dead_aircraft_dict.keys():
self.enemy_dead_aircraft_dict[a.type] = self.enemy_dead_aircraft_dict[a.type] + 1
else:
self.enemy_dead_aircraft_dict[a.type] = 1
self.player_dead_units_dict = {}
for a in self.player_dead_units:
if a.type in self.player_dead_units_dict.keys():
self.player_dead_units_dict[a.type] = self.player_dead_units_dict[a.type] + 1
else:
self.player_dead_units_dict[a.type] = 1
self.enemy_dead_units_dict = {}
for a in self.enemy_dead_units:
if a.type in self.enemy_dead_units_dict.keys():
self.enemy_dead_units_dict[a.type] = self.enemy_dead_units_dict[a.type] + 1
else:
self.enemy_dead_units_dict[a.type] = 1
self.player_dead_buildings_dict = {}
for a in self.player_dead_buildings:
if a.type in self.player_dead_buildings_dict.keys():
self.player_dead_buildings_dict[a.type] = self.player_dead_buildings_dict[a.type] + 1
else:
self.player_dead_buildings_dict[a.type] = 1
self.enemy_dead_buildings_dict = {}
for a in self.enemy_dead_buildings:
if a.type in self.enemy_dead_buildings_dict.keys():
self.enemy_dead_buildings_dict[a.type] = self.enemy_dead_buildings_dict[a.type] + 1
else:
self.enemy_dead_buildings_dict[a.type] = 1
logging.info("--------------------------------")
logging.info("Debriefing pre process results :")
logging.info("--------------------------------")
logging.info(self.player_dead_aircraft_dict)
logging.info(self.enemy_dead_aircraft_dict)
logging.info(self.player_dead_units_dict)
logging.info(self.enemy_dead_units_dict)
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
regularly for the stopped() condition."""
def __init__(self, callback: typing.Callable, game):
super(PollDebriefingFileThread, self).__init__()
self._stop_event = threading.Event()
self.callback = callback
self.game = game
def stop(self):
self._stop_event.set()
def stopped(self):
return self._stop_event.is_set()
def run(self):
if os.path.isfile("state.json"):
last_modified = os.path.getmtime("state.json")
else:
last_modified = 0
while not self.stopped():
if os.path.isfile("state.json") and os.path.getmtime("state.json") > last_modified:
with open("state.json", "r") as json_file:
json_data = json.load(json_file)
debriefing = Debriefing(json_data, self.game)
self.callback(debriefing)
break
time.sleep(5)
def wait_for_debriefing(callback: typing.Callable, game)->PollDebriefingFileThread:
thread = PollDebriefingFileThread(callback, game)
thread.start()
return thread

View File

@@ -1,8 +1,2 @@
from .event import *
from .groundintercept import *
from .intercept import *
from .capture import *
from .navalintercept import *
from .antiaastrike import *
from .groundattack import *
from .infantrytransport import *
from .frontlineattack import *

View File

@@ -1,86 +0,0 @@
import math
import random
from dcs.task import *
from game import *
from game.event import *
from game.operation.antiaastrike import AntiAAStrikeOperation
from userdata.debriefing import Debriefing
class AntiAAStrikeEvent(Event):
TARGET_AMOUNT_MAX = 2
STRENGTH_INFLUENCE = 0.3
SUCCESS_TARGETS_HIT_PERCENTAGE = 0.5
targets = None # type: db.ArmorDict
def __str__(self):
return "Anti-AA strike from {} at {}".format(self.from_cp, self.to_cp)
def is_successfull(self, debriefing: Debriefing):
total_targets = sum(self.targets.values())
destroyed_targets = 0
for unit, count in debriefing.destroyed_units[self.defender_name].items():
if unit in self.targets:
destroyed_targets += count
if self.from_cp.captured:
return math.ceil(float(destroyed_targets) / total_targets) >= self.SUCCESS_TARGETS_HIT_PERCENTAGE
else:
return math.ceil(float(destroyed_targets) / total_targets) < self.SUCCESS_TARGETS_HIT_PERCENTAGE
def commit(self, debriefing: Debriefing):
super(AntiAAStrikeEvent, self).commit(debriefing)
if self.from_cp.captured:
if self.is_successfull(debriefing):
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.to_cp.base.affect_strength(+self.STRENGTH_INFLUENCE)
else:
if self.is_successfull(debriefing):
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def skip(self):
if self.to_cp.captured:
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
self.targets = self.to_cp.base.assemble_aa(count=self.to_cp.base.total_aa)
op = AntiAAStrikeOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients=clients,
defender_clients={},
from_cp=self.from_cp,
to_cp=self.to_cp)
op.setup(target=self.targets,
strikegroup=strikegroup,
interceptors={})
self.operation = op
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
self.targets = self.to_cp.base.assemble_aa()
op = AntiAAStrikeOperation(
self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients={},
defender_clients=clients,
from_cp=self.from_cp,
to_cp=self.to_cp
)
strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
op.setup(target=self.targets,
strikegroup=strikegroup,
interceptors=interceptors)
self.operation = op

View File

@@ -1,89 +0,0 @@
import math
import random
from dcs.task import *
from game import db
from game.operation.capture import CaptureOperation
from userdata.debriefing import Debriefing
from .event import Event
class CaptureEvent(Event):
silent = True
BONUS_BASE = 15
STRENGTH_RECOVERY = 0.35
def __str__(self):
return "Attack from {} to {}".format(self.from_cp, self.to_cp)
def is_successfull(self, debriefing: Debriefing):
alive_attackers = sum([v for k, v in debriefing.alive_units[self.attacker_name].items() if db.unit_task(k) == PinpointStrike])
alive_defenders = sum([v for k, v in debriefing.alive_units[self.defender_name].items() if db.unit_task(k) == PinpointStrike])
attackers_success = alive_attackers >= alive_defenders
if self.from_cp.captured:
return attackers_success
else:
return not attackers_success
def commit(self, debriefing: Debriefing):
super(CaptureEvent, self).commit(debriefing)
if self.is_successfull(debriefing):
if self.from_cp.captured:
self.to_cp.captured = True
self.to_cp.base.filter_units(db.UNIT_BY_COUNTRY[self.attacker_name])
self.to_cp.base.affect_strength(+self.STRENGTH_RECOVERY)
else:
if not self.from_cp.captured:
self.to_cp.captured = False
self.to_cp.base.affect_strength(+self.STRENGTH_RECOVERY)
def skip(self):
if self.to_cp.captured:
self.to_cp.captured = False
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
cas = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
escort = self.from_cp.base.scramble_sweep(self.game.settings.multiplier)
attackers = self.from_cp.base.armor
op = CaptureOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients={},
defender_clients=clients,
from_cp=self.from_cp,
to_cp=self.to_cp)
op.setup(cas=cas,
escort=escort,
attack=attackers,
intercept=interceptors,
defense=self.to_cp.base.armor,
aa=self.to_cp.base.aa)
self.operation = op
def player_attacking(self, cas: db.PlaneDict, escort: db.PlaneDict, armor: db.ArmorDict, clients: db.PlaneDict):
op = CaptureOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients=clients,
defender_clients={},
from_cp=self.from_cp,
to_cp=self.to_cp)
defenders = self.to_cp.base.scramble_sweep(self.game.settings.multiplier)
defenders.update(self.to_cp.base.scramble_cas(self.game.settings.multiplier))
op.setup(cas=cas,
escort=escort,
attack=armor,
intercept=defenders,
defense=self.to_cp.base.armor,
aa=self.to_cp.base.assemble_aa())
self.operation = op

View File

@@ -1,46 +1,86 @@
from __future__ import annotations
import logging
import math
from typing import Dict, List, Optional, Type, TYPE_CHECKING
from dcs.mapping import Point
from dcs.task import Task
from dcs.unittype import UnitType
from game import *
from theater import *
from gen.environmentgen import EnvironmentSettings
from game import db, persistency
from game.debriefing import Debriefing
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 userdata.debriefing import Debriefing
from userdata import persistency
if TYPE_CHECKING:
from ..game import Game
DIFFICULTY_LOG_BASE = 1.1
EVENT_DEPARTURE_MAX_DISTANCE = 340000
MINOR_DEFEAT_INFLUENCE = 0.1
DEFEAT_INFLUENCE = 0.3
STRONG_DEFEAT_INFLUENCE = 0.5
class Event:
silent = False
informational = False
is_awacs_enabled = False
ca_slots = 0
game = None # type: Game
location = None # type: Point
from_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
operation = None # type: Operation
difficulty = 1 # type: int
game = None # type: Game
environment_settings = None # type: EnvironmentSettings
BONUS_BASE = 5
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
def __init__(self, game, from_cp: ControlPoint, target_cp: ControlPoint, location: Point, attacker_name: str, defender_name: str):
self.game = game
self.departure_cp: Optional[ControlPoint] = None
self.from_cp = from_cp
self.to_cp = target_cp
self.location = location
self.attacker_name = attacker_name
self.defender_name = defender_name
self.to_cp = to_cp
self.from_cp = from_cp
self.game = game
@property
def is_player_attacking(self) -> bool:
return self.attacker_name == self.game.player
return self.attacker_name == self.game.player_name
@property
def enemy_cp(self) -> ControlPoint:
if self.attacker_name == self.game.player:
def enemy_cp(self) -> Optional[ControlPoint]:
if self.attacker_name == self.game.player_name:
return self.to_cp
else:
return self.from_cp
return self.departure_cp
@property
def threat_description(self) -> str:
return ""
def tasks(self) -> List[Type[Task]]:
return []
@property
def global_cp_available(self) -> bool:
return False
def is_departure_available_from(self, cp: ControlPoint) -> bool:
if not cp.captured:
return False
if self.location.distance_to_point(cp.position) > EVENT_DEPARTURE_MAX_DISTANCE:
return False
if cp.is_global and not self.global_cp_available:
return False
return True
def bonus(self) -> int:
return int(math.log(self.to_cp.importance + 1, DIFFICULTY_LOG_BASE) * self.BONUS_BASE)
@@ -50,52 +90,293 @@ class Event:
def generate(self):
self.operation.is_awacs_enabled = self.is_awacs_enabled
self.operation.ca_slots = self.ca_slots
self.operation.prepare(self.game.theater.terrain, is_quick=False)
self.operation.generate()
self.operation.mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
self.operation.current_mission.save(persistency.mission_path_for("liberation_nextturn.miz"))
self.environment_settings = self.operation.environment_settings
def generate_quick(self):
self.operation.is_awacs_enabled = self.is_awacs_enabled
self.operation.environment_settings = self.environment_settings
self.operation.prepare(self.game.theater.terrain, is_quick=True)
self.operation.generate()
self.operation.mission.save(persistency.mission_path_for("liberation_nextturn_quick.miz"))
def commit(self, debriefing: Debriefing):
for country, losses in debriefing.destroyed_units.items():
if country == self.attacker_name:
cp = self.from_cp
else:
cp = self.to_cp
cp.base.commit_losses(losses)
logging.info("Commiting mission results")
# ------------------------------
# Destroyed aircrafts
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
for destroyed_aircraft in debriefing.killed_aircrafts:
try:
cpid = int(destroyed_aircraft.split("|")[3])
type = db.unit_type_from_name(destroyed_aircraft.split("|")[4])
if cpid in cp_map.keys():
cp = cp_map[cpid]
if type in cp.base.aircraft.keys():
logging.info("Aircraft destroyed : " + str(type))
cp.base.aircraft[type] = max(0, cp.base.aircraft[type]-1)
except Exception as e:
print(e)
# ------------------------------
# Destroyed ground units
killed_unit_count_by_cp = {cp.id: 0 for cp in self.game.theater.controlpoints}
cp_map = {cp.id: cp for cp in self.game.theater.controlpoints}
for killed_ground_unit in debriefing.killed_ground_units:
try:
cpid = int(killed_ground_unit.split("|")[3])
type = db.unit_type_from_name(killed_ground_unit.split("|")[4])
if cpid in cp_map.keys():
killed_unit_count_by_cp[cpid] = killed_unit_count_by_cp[cpid] + 1
cp = cp_map[cpid]
if type in cp.base.armor.keys():
logging.info("Ground unit destroyed : " + str(type))
cp.base.armor[type] = max(0, cp.base.armor[type] - 1)
except Exception as e:
print(e)
# ------------------------------
# Static ground objects
for destroyed_ground_unit_name in debriefing.killed_ground_units:
for cp in self.game.theater.controlpoints:
if not cp.ground_objects:
continue
# -- Static ground objects
for i, ground_object in enumerate(cp.ground_objects):
if ground_object.is_dead:
continue
if (
(ground_object.group_name == destroyed_ground_unit_name)
or
(ground_object.is_same_group(destroyed_ground_unit_name))
):
logging.info("cp {} killing ground object {}".format(cp, ground_object.group_name))
cp.ground_objects[i].is_dead = True
info = Information("Building destroyed",
ground_object.dcs_identifier + " has been destroyed at location " + ground_object.obj_name,
self.game.turn)
self.game.informations.append(info)
# -- AA Site groups
destroyed_units = 0
info = Information("Units destroyed at " + ground_object.obj_name,
"",
self.game.turn)
for i, ground_object in enumerate(cp.ground_objects):
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 = []
for u in g.units:
if u.name == destroyed_ground_unit_name:
g.units.remove(u)
g.units_losts.append(u)
destroyed_units = destroyed_units + 1
info.text = u.type
ucount = sum([len(g.units) for g in ground_object.groups])
if ucount == 0:
ground_object.is_dead = True
if destroyed_units > 0:
self.game.informations.append(info)
# ------------------------------
# Captured bases
#if self.game.player_country in db.BLUEFOR_FACTIONS:
coalition = 2 # Value in DCS mission event for BLUE
#else:
# coalition = 1 # Value in DCS mission event for RED
for captured in debriefing.base_capture_events:
try:
id = int(captured.split("||")[0])
new_owner_coalition = int(captured.split("||")[1])
captured_cps = []
for cp in self.game.theater.controlpoints:
if cp.id == id:
if cp.captured and new_owner_coalition != coalition:
for_player = False
info = Information(cp.name + " lost !", "The ennemy took control of " + cp.name + "\nShame on us !", self.game.turn)
self.game.informations.append(info)
captured_cps.append(cp)
elif not(cp.captured) and new_owner_coalition == coalition:
for_player = True
info = Information(cp.name + " captured !", "We took control of " + cp.name + "! Great job !", self.game.turn)
self.game.informations.append(info)
captured_cps.append(cp)
else:
continue
cp.capture(self.game, for_player)
for cp in captured_cps:
logging.info("Will run redeploy for " + cp.name)
self.redeploy_units(cp)
except Exception as e:
print(e)
# Destroyed units carcass
# -------------------------
for destroyed_unit in debriefing.destroyed_units:
self.game.add_destroyed_units(destroyed_unit)
# -----------------------------------
# Compute damage to bases
for cp in self.game.theater.player_points():
enemy_cps = [e for e in cp.connected_points if not e.captured]
for enemy_cp in enemy_cps:
print("Compute frontline progression for : " + cp.name + " to " + enemy_cp.name)
delta = 0.0
player_won = True
ally_casualties = killed_unit_count_by_cp[cp.id]
enemy_casualties = killed_unit_count_by_cp[enemy_cp.id]
ally_units_alive = cp.base.total_armor
enemy_units_alive = enemy_cp.base.total_armor
print(ally_units_alive)
print(enemy_units_alive)
print(ally_casualties)
print(enemy_casualties)
ratio = (1.0 + enemy_casualties) / (1.0 + ally_casualties)
player_aggresive = cp.stances[enemy_cp.id] in [CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]
if ally_units_alive == 0:
player_won = False
delta = STRONG_DEFEAT_INFLUENCE
elif enemy_units_alive == 0:
player_won = True
delta = STRONG_DEFEAT_INFLUENCE
elif cp.stances[enemy_cp.id] == CombatStance.RETREAT:
player_won = False
delta = STRONG_DEFEAT_INFLUENCE
else:
if enemy_casualties > ally_casualties:
player_won = True
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
delta = STRONG_DEFEAT_INFLUENCE
else:
if ratio > 3:
delta = STRONG_DEFEAT_INFLUENCE
elif ratio < 1.5:
delta = MINOR_DEFEAT_INFLUENCE
else:
delta = DEFEAT_INFLUENCE
elif ally_casualties > enemy_casualties:
if ally_units_alive > 2*enemy_units_alive and player_aggresive:
# Even with casualties if the enemy is overwhelmed, they are going to lose ground
player_won = True
delta = MINOR_DEFEAT_INFLUENCE
elif ally_units_alive > 3*enemy_units_alive and player_aggresive:
player_won = True
delta = STRONG_DEFEAT_INFLUENCE
else:
# But is the enemy is not outnumbered, we lose
player_won = False
if cp.stances[enemy_cp.id] == CombatStance.BREAKTHROUGH:
delta = STRONG_DEFEAT_INFLUENCE
else:
delta = STRONG_DEFEAT_INFLUENCE
# No progress with defensive strategies
if player_won and cp.stances[enemy_cp.id] in [CombatStance.DEFENSIVE, CombatStance.AMBUSH]:
print("Defensive stance, progress is limited")
delta = MINOR_DEFEAT_INFLUENCE
if player_won:
print(cp.name + " won ! factor > " + str(delta))
cp.base.affect_strength(delta)
enemy_cp.base.affect_strength(-delta)
info = Information("Frontline Report",
"Our ground forces from " + cp.name + " are making progress toward " + enemy_cp.name,
self.game.turn)
self.game.informations.append(info)
else:
print(cp.name + " lost ! factor > " + str(delta))
enemy_cp.base.affect_strength(delta)
cp.base.affect_strength(-delta)
info = Information("Frontline Report",
"Our ground forces from " + cp.name + " are losing ground against the enemy forces from " + enemy_cp.name,
self.game.turn)
self.game.informations.append(info)
def skip(self):
pass
def redeploy_units(self, cp):
""""
Auto redeploy units to newly captured base
"""
ally_connected_cps = [ocp for ocp in cp.connected_points if cp.captured == ocp.captured]
enemy_connected_cps = [ocp for ocp in cp.connected_points if cp.captured != ocp.captured]
# If the newly captured cp does not have enemy connected cp,
# then it is not necessary to redeploy frontline units there.
if len(enemy_connected_cps) == 0:
return
else:
# From each ally cp, send reinforcements
for ally_cp in ally_connected_cps:
total_units_redeployed = 0
own_enemy_cp = [ocp for ocp in ally_cp.connected_points if ally_cp.captured != ocp.captured]
moved_units = {}
# If the connected base, does not have any more enemy cp connected.
# Or if it is not the opponent redeploying forces there (enemy AI will never redeploy all their forces at once)
if len(own_enemy_cp) > 0 or not cp.captured:
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = int(count/2)
total_units_redeployed = total_units_redeployed + int(count/2)
else: # So if the old base, does not have any more enemy cp connected, or if it is an enemy base
for frontline_unit, count in ally_cp.base.armor.items():
moved_units[frontline_unit] = count
total_units_redeployed = total_units_redeployed + count
cp.base.commision_units(moved_units)
ally_cp.base.commit_losses(moved_units)
if total_units_redeployed > 0:
info = Information("Units redeployed", "", self.game.turn)
info.text = str(total_units_redeployed) + " units have been redeployed from " + ally_cp.name + " to " + cp.name
self.game.informations.append(info)
logging.info(info.text)
class UnitsDeliveryEvent(Event):
informational = True
units = None # type: typing.Dict[UnitType, int]
def __init__(self, attacker_name: str, defender_name: str, from_cp: ControlPoint, to_cp: ControlPoint, game):
super(UnitsDeliveryEvent, self).__init__(attacker_name=attacker_name,
defender_name=defender_name,
super(UnitsDeliveryEvent, self).__init__(game=game,
location=to_cp.position,
from_cp=from_cp,
to_cp=to_cp,
game=game)
target_cp=to_cp,
attacker_name=attacker_name,
defender_name=defender_name)
self.units = {}
self.units: Dict[UnitType, int] = {}
def __str__(self):
return "Pending delivery to {}".format(self.to_cp)
def deliver(self, units: typing.Dict[UnitType, int]):
def deliver(self, units: Dict[UnitType, int]):
for k, v in units.items():
self.units[k] = self.units.get(k, 0) + v
def skip(self):
for k, v in self.units.items():
info = Information("Ally Reinforcement", str(k.id) + " x " + str(v) + " at " + self.to_cp.name, self.game.turn)
self.game.informations.append(info)
self.to_cp.base.commision_units(self.units)

View File

@@ -0,0 +1,49 @@
from typing import List, Type
from dcs.task import CAP, CAS, Task
from game import db
from game.operation.frontlineattack import FrontlineAttackOperation
from .event import Event
from ..debriefing import Debriefing
class FrontlineAttackEvent(Event):
@property
def tasks(self) -> List[Type[Task]]:
if self.is_player_attacking:
return [CAS, CAP]
else:
return [CAP]
@property
def global_cp_available(self) -> bool:
return True
def __str__(self):
return "Frontline attack"
def is_successfull(self, debriefing: Debriefing):
attackers_success = True
if self.from_cp.captured:
return attackers_success
else:
return not attackers_success
def commit(self, debriefing: Debriefing):
super(FrontlineAttackEvent, self).commit(debriefing)
def skip(self):
if self.to_cp.captured:
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, flights: db.TaskForceDict):
assert self.departure_cp is not None
op = FrontlineAttackOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
from_cp=self.from_cp,
departure_cp=self.departure_cp,
to_cp=self.to_cp)
self.operation = op

View File

@@ -1,40 +0,0 @@
import math
import random
from dcs.task import *
from game import *
from game.event import *
from game.event.groundintercept import GroundInterceptEvent
from game.operation.groundattack import GroundAttackOperation
class GroundAttackEvent(GroundInterceptEvent):
def __str__(self):
return "Destroy insurgents at {}".format(self.to_cp)
@property
def threat_description(self):
return ""
def player_defending(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
suitable_unittypes = db.find_unittype(Reconnaissance, self.attacker_name)
random.shuffle(suitable_unittypes)
unittypes = suitable_unittypes[:self.TARGET_VARIETY]
typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1)
self.targets = {unittype: typecount for unittype in unittypes}
op = GroundAttackOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients={},
defender_clients=clients,
from_cp=self.from_cp,
to_cp=self.to_cp)
op.setup(target=self.targets,
strikegroup=strikegroup)
self.operation = op
def player_attacking(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
assert False

View File

@@ -1,106 +0,0 @@
import math
import random
from dcs.task import *
from dcs.vehicles import AirDefence
from game import *
from game.event import *
from game.operation.groundintercept import GroundInterceptOperation
from userdata.debriefing import Debriefing
class GroundInterceptEvent(Event):
TARGET_AMOUNT_FACTOR = 2
TARGET_VARIETY = 2
STRENGTH_INFLUENCE = 0.3
SUCCESS_TARGETS_HIT_PERCENTAGE = 0.5
targets = None # type: db.ArmorDict
@property
def threat_description(self):
if not self.game.is_player_attack(self):
return "{} aicraft".format(self.from_cp.base.scramble_count(self.game.settings.multiplier, CAS))
else:
return super(GroundInterceptEvent, self).threat_description
def __str__(self):
return "Fontline CAS from {} at {}".format(self.from_cp, self.to_cp)
def is_successfull(self, debriefing: Debriefing):
total_targets = sum(self.targets.values())
destroyed_targets = 0
for unit, count in debriefing.destroyed_units[self.defender_name].items():
if unit in self.targets:
destroyed_targets += count
if self.from_cp.captured:
return float(destroyed_targets) / total_targets >= self.SUCCESS_TARGETS_HIT_PERCENTAGE
else:
return float(destroyed_targets) / total_targets < self.SUCCESS_TARGETS_HIT_PERCENTAGE
def commit(self, debriefing: Debriefing):
super(GroundInterceptEvent, self).commit(debriefing)
if self.from_cp.captured:
if self.is_successfull(debriefing):
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.to_cp.base.affect_strength(+self.STRENGTH_INFLUENCE)
else:
if self.is_successfull(debriefing):
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def skip(self):
if self.to_cp.captured:
self.to_cp.base.affect_strength(-0.1)
def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
suitable_unittypes = db.find_unittype(PinpointStrike, self.defender_name)
random.shuffle(suitable_unittypes)
unittypes = suitable_unittypes[:self.TARGET_VARIETY]
typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1)
self.targets = {unittype: typecount for unittype in unittypes}
defense_aa_unit = random.choice(self.game.commision_unit_types(self.to_cp, AirDefence))
self.targets[defense_aa_unit] = 1
op = GroundInterceptOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients=clients,
defender_clients={},
from_cp=self.from_cp,
to_cp=self.to_cp)
op.setup(target=self.targets,
strikegroup=strikegroup,
interceptors={})
self.operation = op
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
suitable_unittypes = db.find_unittype(PinpointStrike, self.defender_name)
random.shuffle(suitable_unittypes)
unittypes = suitable_unittypes[:self.TARGET_VARIETY]
typecount = max(math.floor(self.difficulty * self.TARGET_AMOUNT_FACTOR), 1)
self.targets = {unittype: typecount for unittype in unittypes}
op = GroundInterceptOperation(
self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients={},
defender_clients=clients,
from_cp=self.from_cp,
to_cp=self.to_cp
)
strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
op.setup(target=self.targets,
strikegroup=strikegroup,
interceptors=interceptors)
self.operation = op

View File

@@ -1,47 +0,0 @@
import math
import random
from dcs.task import *
from dcs.vehicles import *
from game import db
from game.operation.infantrytransport import InfantryTransportOperation
from theater.conflicttheater import *
from userdata.debriefing import Debriefing
from .event import Event
class InfantryTransportEvent(Event):
STRENGTH_INFLUENCE = 0.3
def __str__(self):
return "Frontline transport troops to {}".format(self.to_cp)
def is_successfull(self, debriefing: Debriefing):
return True
def commit(self, debriefing: Debriefing):
super(InfantryTransportEvent, self).commit(debriefing)
if self.is_successfull(debriefing):
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def player_attacking(self, transport: db.HeliDict, clients: db.HeliDict):
op = InfantryTransportOperation(
game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients=clients,
defender_clients={},
from_cp=self.from_cp,
to_cp=self.to_cp
)
air_defense = db.find_unittype(AirDefence, self.defender_name)[0]
op.setup(transport=transport,
aa={air_defense: 2})
self.operation = op

View File

@@ -1,100 +0,0 @@
import math
import random
from dcs.task import *
from dcs.vehicles import *
from game import db
from game.operation.intercept import InterceptOperation
from theater.conflicttheater import *
from userdata.debriefing import Debriefing
from .event import Event
class InterceptEvent(Event):
STRENGTH_INFLUENCE = 0.3
GLOBAL_STRENGTH_INFLUENCE = 0.3
AIRDEFENSE_COUNT = 3
transport_unit = None # type: FlyingType
def __str__(self):
return "Intercept from {} at {}".format(self.from_cp, self.to_cp)
@property
def threat_description(self):
return "{} aircraft".format(self.enemy_cp.base.scramble_count(self.game.settings.multiplier, CAP))
def is_successfull(self, debriefing: Debriefing):
units_destroyed = debriefing.destroyed_units[self.defender_name].get(self.transport_unit, 0)
if self.from_cp.captured:
return units_destroyed > 0
else:
return units_destroyed == 0
def commit(self, debriefing: Debriefing):
super(InterceptEvent, self).commit(debriefing)
if self.attacker_name == self.game.player:
if self.is_successfull(debriefing):
for _, cp in self.game.theater.conflicts(True):
cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
# enemy attacking
if self.is_successfull(debriefing):
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def skip(self):
if self.to_cp.captured:
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def player_attacking(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
escort = self.to_cp.base.scramble_sweep(self.game.settings.multiplier)
self.transport_unit = random.choice(db.find_unittype(Transport, self.defender_name))
assert self.transport_unit is not None
airdefense_unit = db.find_unittype(AirDefence, self.defender_name)[-1]
op = InterceptOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients=clients,
defender_clients={},
from_cp=self.from_cp,
to_cp=self.to_cp)
op.setup(escort=escort,
transport={self.transport_unit: 1},
airdefense={airdefense_unit: self.AIRDEFENSE_COUNT},
interceptors=interceptors)
self.operation = op
def player_defending(self, escort: db.PlaneDict, clients: db.PlaneDict):
# TODO: even not quick mission is too quick
interceptors = self.from_cp.base.scramble_interceptors(self.game.settings.multiplier)
self.transport_unit = random.choice(db.find_unittype(Transport, self.defender_name))
assert self.transport_unit is not None
op = InterceptOperation(game=self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients={},
defender_clients=clients,
from_cp=self.from_cp,
to_cp=self.to_cp)
op.setup(escort=escort,
transport={self.transport_unit: 1},
interceptors=interceptors,
airdefense={})
self.operation = op

View File

@@ -1,108 +0,0 @@
import typing
import math
import random
from dcs.task import *
from dcs.vehicles import *
from game import db
from game.operation.navalintercept import NavalInterceptionOperation
from userdata.debriefing import Debriefing
from .event import Event
class NavalInterceptEvent(Event):
STRENGTH_INFLUENCE = 0.3
SUCCESS_RATE = 0.5
targets = None # type: db.ShipDict
def _targets_count(self) -> int:
from gen.conflictgen import IMPORTANCE_LOW, IMPORTANCE_HIGH
factor = (self.to_cp.importance - IMPORTANCE_LOW) * 10
return max(int(factor), 1)
def __str__(self) -> str:
return "Naval intercept at {}".format(self.to_cp)
@property
def threat_description(self):
s = "{} ship(s)".format(self._targets_count())
if not self.from_cp.captured:
s += ", {} aircraft".format(self.from_cp.base.scramble_count(self.game.settings.multiplier))
return s
def is_successfull(self, debriefing: Debriefing):
total_targets = sum(self.targets.values())
destroyed_targets = 0
for unit, count in debriefing.destroyed_units[self.defender_name].items():
if unit in self.targets:
destroyed_targets += count
if self.from_cp.captured:
return math.ceil(float(destroyed_targets) / total_targets) > self.SUCCESS_RATE
else:
return math.ceil(float(destroyed_targets) / total_targets) < self.SUCCESS_RATE
def commit(self, debriefing: Debriefing):
super(NavalInterceptEvent, self).commit(debriefing)
if self.attacker_name == self.game.player:
if self.is_successfull(debriefing):
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
# enemy attacking
if self.is_successfull(debriefing):
self.from_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
else:
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def skip(self):
if self.to_cp.captured:
self.to_cp.base.affect_strength(-self.STRENGTH_INFLUENCE)
def player_attacking(self, strikegroup: db.PlaneDict, clients: db.PlaneDict):
self.targets = {
random.choice(db.find_unittype(CargoTransportation, self.defender_name)): self._targets_count(),
}
op = NavalInterceptionOperation(
self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients=clients,
defender_clients={},
from_cp=self.from_cp,
to_cp=self.to_cp
)
op.setup(strikegroup=strikegroup,
interceptors={},
targets=self.targets)
self.operation = op
def player_defending(self, interceptors: db.PlaneDict, clients: db.PlaneDict):
self.targets = {
random.choice(db.find_unittype(CargoTransportation, self.defender_name)): self._targets_count(),
}
op = NavalInterceptionOperation(
self.game,
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker_clients={},
defender_clients=clients,
from_cp=self.from_cp,
to_cp=self.to_cp
)
strikegroup = self.from_cp.base.scramble_cas(self.game.settings.multiplier)
op.setup(strikegroup=strikegroup,
interceptors=interceptors,
targets=self.targets)
self.operation = op

269
game/factions/faction.py Normal file
View File

@@ -0,0 +1,269 @@
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import Optional, Dict, Type, List, Any, cast
import dcs
from dcs.countries import country_dict
from dcs.planes import plane_map
from dcs.unittype import FlyingType, ShipType, VehicleType, UnitType
from dcs.vehicles import Armor, Unarmed, Infantry, Artillery, AirDefence
from game.data.building_data import WW2_ALLIES_BUILDINGS, DEFAULT_AVAILABLE_BUILDINGS, WW2_GERMANY_BUILDINGS, WW2_FREE
from game.data.doctrine import Doctrine, MODERN_DOCTRINE, COLDWAR_DOCTRINE, WWII_DOCTRINE
from pydcs_extensions.mod_units import MODDED_VEHICLES, MODDED_AIRPLANES
@dataclass
class Faction:
# Country used by this faction
country: str = field(default="")
# Nice name of the faction
name: str = field(default="")
# List of faction file authors
authors: str = field(default="")
# A description of the faction
description: str = field(default="")
# Available aircraft
aircrafts: List[UnitType] = field(default_factory=list)
# Available awacs aircraft
awacs: List[UnitType] = field(default_factory=list)
# Available tanker aircraft
tankers: List[UnitType] = field(default_factory=list)
# Available frontline units
frontline_units: List[VehicleType] = field(default_factory=list)
# Available artillery units
artillery_units: List[VehicleType] = field(default_factory=list)
# Infantry units used
infantry_units: List[VehicleType] = field(default_factory=list)
# Logistics units used
logistics_units: List[VehicleType] = field(default_factory=list)
# List of units that can be deployed as SHORAD
shorads: List[str] = field(default_factory=list)
# 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)
# Required mods or asset packs
requirements: Dict[str, str] = field(default_factory=dict)
# possible aircraft carrier units
aircraft_carrier: List[UnitType] = field(default_factory=list)
# possible helicopter carrier units
helicopter_carrier: List[UnitType] = field(default_factory=list)
# Possible carrier names
carrier_names: List[str] = field(default_factory=list)
# Possible helicopter carrier names
helicopter_carrier_names: List[str] = field(default_factory=list)
# Navy group generators
navy_generators: List[str] = field(default_factory=list)
# Available destroyers
destroyers: List[str] = field(default_factory=list)
# Available cruisers
cruisers: List[str] = field(default_factory=list)
# How many navy group should we try to generate per CP on startup for this faction
navy_group_count: int = field(default=1)
# How many missiles group should we try to generate per CP on startup for this faction
missiles_group_count: int = field(default=1)
# Whether this faction has JTAC access
has_jtac: bool = field(default=False)
# Unit to use as JTAC for this faction
jtac_unit: Optional[FlyingType] = field(default=None)
# doctrine
doctrine: Doctrine = field(default=MODERN_DOCTRINE)
# 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:
faction = Faction()
faction.country = json.get("country", "/")
if faction.country not in [c.name for c in country_dict.values()]:
raise AssertionError("Faction's country (\"{}\") is not a valid DCS country ID".format(faction.country))
faction.name = json.get("name", "")
if not faction.name:
raise AssertionError("Faction has no valid name")
faction.authors = json.get("authors", "")
faction.description = json.get("description", "")
faction.aircrafts = load_all_aircraft(json.get("aircrafts", []))
faction.awacs = load_all_aircraft(json.get("awacs", []))
faction.tankers = load_all_aircraft(json.get("tankers", []))
faction.frontline_units = load_all_vehicles(
json.get("frontline_units", []))
faction.artillery_units = load_all_vehicles(
json.get("artillery_units", []))
faction.infantry_units = load_all_vehicles(
json.get("infantry_units", []))
faction.logistics_units = load_all_vehicles(
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", {})
faction.carrier_names = json.get("carrier_names", [])
faction.helicopter_carrier_names = json.get(
"helicopter_carrier_names", [])
faction.navy_generators = json.get("navy_generators", [])
faction.aircraft_carrier = load_all_ships(
json.get("aircraft_carrier", []))
faction.helicopter_carrier = load_all_ships(
json.get("helicopter_carrier", []))
faction.destroyers = load_all_ships(json.get("destroyers", []))
faction.cruisers = load_all_ships(json.get("cruisers", []))
faction.has_jtac = json.get("has_jtac", False)
jtac_name = json.get("jtac_unit", None)
if jtac_name is not None:
faction.jtac_unit = load_aircraft(jtac_name)
else:
faction.jtac_unit = None
faction.navy_group_count = int(json.get("navy_group_count", 1))
faction.missiles_group_count = int(json.get("missiles_group_count", 0))
# Load doctrine
doctrine = json.get("doctrine", "modern")
if doctrine == "modern":
faction.doctrine = MODERN_DOCTRINE
elif doctrine == "coldwar":
faction.doctrine = COLDWAR_DOCTRINE
elif doctrine == "ww2":
faction.doctrine = WWII_DOCTRINE
else:
faction.doctrine = MODERN_DOCTRINE
# Load the building set
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":
faction.building_set = WW2_GERMANY_BUILDINGS
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
def units(self) -> List[UnitType]:
return (self.infantry_units + self.aircrafts + self.awacs +
self.artillery_units + self.frontline_units +
self.tankers + self.logistics_units)
def unit_loader(unit: str, class_repository: List[Any]) -> Optional[UnitType]:
"""
Find unit by name
:param unit: Unit name as string
:param class_repository: Repository of classes (Either a module, a class, or a list of classes)
:return: The unit as a PyDCS type
"""
if unit is None:
return None
elif unit in plane_map.keys():
return plane_map[unit]
else:
for mother_class in class_repository:
if getattr(mother_class, unit, None) is not None:
return getattr(mother_class, unit)
if type(mother_class) is list:
for m in mother_class:
if m.__name__ == unit:
return m
logging.error(f"FACTION ERROR : Unable to find {unit} in pydcs")
return None
def load_aircraft(name: str) -> Optional[FlyingType]:
return cast(Optional[FlyingType], unit_loader(
name, [dcs.planes, dcs.helicopters, MODDED_AIRPLANES]
))
def load_all_aircraft(data) -> List[FlyingType]:
items = []
for name in data:
item = load_aircraft(name)
if item is not None:
items.append(item)
return items
def load_vehicle(name: str) -> Optional[VehicleType]:
return cast(Optional[FlyingType], unit_loader(
name, [Infantry, Unarmed, Armor, AirDefence, Artillery, MODDED_VEHICLES]
))
def load_all_vehicles(data) -> List[VehicleType]:
items = []
for name in data:
item = load_vehicle(name)
if item is not None:
items.append(item)
return items
def load_ship(name: str) -> Optional[ShipType]:
return cast(Optional[FlyingType], unit_loader(name, [dcs.ships]))
def load_all_ships(data) -> List[ShipType]:
items = []
for name in data:
item = load_ship(name)
if item is not None:
items.append(item)
return items

View File

@@ -0,0 +1,46 @@
from __future__ import annotations
import json
import logging
from pathlib import Path
from typing import Dict, Iterator, Optional, Type
from game.factions.faction import Faction
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]:
files = [f for f in FACTION_DIRECTORY.glob("*.json") if f.is_file()]
factions = {}
for f in files:
try:
with f.open("r", encoding="utf-8") as fdata:
data = json.load(fdata, encoding="utf-8")
factions[data["name"]] = Faction.from_json(data)
logging.info("Loaded faction : " + str(f))
except Exception:
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

@@ -1,16 +1,36 @@
import typing
import random
import logging
import math
import random
import sys
from datetime import date, datetime, timedelta
from typing import Dict, List
from dcs.task import *
from dcs.vehicles import *
from dcs.action import Coalition
from dcs.mapping import Point
from dcs.task import CAP, CAS, PinpointStrike, Task
from dcs.unittype import UnitType
from dcs.vehicles import AirDefence
from userdata.debriefing import Debriefing
from theater import *
from . import db
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
from gen.flights.closestairfields import ObjectiveDistanceCache
from gen.ground_forces.ai_ground_planner import GroundPlanner
from theater import ConflictTheater, ControlPoint
from theater.conflicttheater import IMPORTANCE_HIGH, IMPORTANCE_LOW
from . import persistency
from .debriefing import Debriefing
from .event.event import Event, UnitsDeliveryEvent
from .event.frontlineattack import FrontlineAttackEvent
from .factions.faction import Faction
from .infos.information import Information
from .settings import Settings
from .event import *
from .weather import Conditions, TimeOfDay
COMMISION_UNIT_VARIETY = 4
COMMISION_LIMITS_SCALE = 1.5
@@ -18,42 +38,20 @@ COMMISION_LIMITS_FACTORS = {
PinpointStrike: 10,
CAS: 5,
CAP: 8,
AirDefence: 1,
AirDefence: 8,
}
COMMISION_AMOUNTS_SCALE = 1.5
COMMISION_AMOUNTS_FACTORS = {
PinpointStrike: 2,
PinpointStrike: 3,
CAS: 1,
CAP: 2,
AirDefence: 0.3,
AirDefence: 0.8,
}
PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE = 25
PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE = 30
PLAYER_INTERCEPT_GLOBAL_PROBABILITY_LOG = 2
"""
Various events probabilities. First key is player probabilty, second is enemy probability.
For the enemy events, only 1 event of each type could be generated for a turn.
Events:
* CaptureEvent - capture base
* InterceptEvent - air intercept
* GroundInterceptEvent - frontline CAS
* GroundAttackEvent - destroy insurgents
* NavalInterceptEvent - naval intercept
* AntiAAStrikeEvent - anti-AA strike
* InfantryTransportEvent - helicopter infantry transport
"""
EVENT_PROBABILITIES = {
CaptureEvent: [100, 10],
InterceptEvent: [25, 10],
GroundInterceptEvent: [25, 10],
GroundAttackEvent: [0, 10],
NavalInterceptEvent: [25, 10],
AntiAAStrikeEvent: [25, 10],
InfantryTransportEvent: [25, 0],
}
PLAYER_BASEATTACK_THRESHOLD = 0.4
# amount of strength player bases recover for the turn
PLAYER_BASE_STRENGTH_RECOVERY = 0.2
@@ -65,112 +63,102 @@ ENEMY_BASE_STRENGTH_RECOVERY = 0.05
AWACS_BUDGET_COST = 4
# Initial budget value
PLAYER_BUDGET_INITIAL = 120
# Base post-turn bonus value
PLAYER_BUDGET_BASE = 10
PLAYER_BUDGET_INITIAL = 650
# Bonus multiplier logarithm base
PLAYER_BUDGET_IMPORTANCE_LOG = 2
class Game:
settings = None # type: Settings
budget = PLAYER_BUDGET_INITIAL
events = None # type: typing.List[Event]
pending_transfers = None # type: typing.Dict[]
ignored_cps = None # type: typing.Collection[ControlPoint]
def __init__(self, player_name: str, enemy_name: str, theater: ConflictTheater):
self.settings = Settings()
self.events = []
def __init__(self, player_name: str, enemy_name: str,
theater: ConflictTheater, start_date: datetime,
settings: Settings):
self.settings = settings
self.events: List[Event] = []
self.theater = theater
self.player = player_name
self.enemy = enemy_name
self.player_name = player_name
self.player_country = db.FACTIONS[player_name].country
self.enemy_name = enemy_name
self.enemy_country = db.FACTIONS[enemy_name].country
self.turn = 0
self.date = date(start_date.year, start_date.month, start_date.day)
self.game_stats = GameStats()
self.game_stats.update(self)
self.ground_planners: Dict[int, GroundPlanner] = {}
self.informations = []
self.informations.append(Information("Game Start", "-" * 40, 0))
self.__culling_points = self.compute_conflicts_position()
self.__destroyed_units: List[str] = []
self.savepath = ""
self.budget = PLAYER_BUDGET_INITIAL
self.current_unit_id = 0
self.current_group_id = 0
self.conditions = self.generate_conditions()
self.blue_ato = AirTaskingOrder()
self.red_ato = AirTaskingOrder()
self.aircraft_inventory = GlobalAircraftInventory(
self.theater.controlpoints
)
self.sanitize_sides()
self.on_load()
def generate_conditions(self) -> Conditions:
return Conditions.generate(self.theater, self.date,
self.current_turn_time_of_day, self.settings)
def sanitize_sides(self):
"""
Make sure the opposing factions are using different countries
:return:
"""
if self.player_country == self.enemy_country:
if self.player_country == "USA":
self.enemy_country = "USAF Aggressors"
elif self.player_country == "Russia":
self.enemy_country = "USSR"
else:
self.enemy_country = "Russia"
@property
def player_faction(self) -> Faction:
return db.FACTIONS[self.player_name]
@property
def enemy_faction(self) -> Faction:
return db.FACTIONS[self.enemy_name]
def _roll(self, prob, mult):
return random.randint(1, 100) <= prob * mult
if self.settings.version == "dev":
# always generate all events for dev
return 100
else:
return random.randint(1, 100) <= prob * mult
def _generate_globalinterceptions(self):
global_count = len([x for x in self.theater.player_points() if x.is_global])
for from_cp in [x for x in self.theater.player_points() if x.is_global]:
probability_base = max(PLAYER_INTERCEPT_GLOBAL_PROBABILITY_BASE / global_count, 1)
probability = probability_base * math.log(len(self.theater.player_points()) + 1, PLAYER_INTERCEPT_GLOBAL_PROBABILITY_LOG)
if self._roll(probability, from_cp.base.strength):
to_cp = random.choice([x for x in self.theater.enemy_points() if x not in self.theater.conflicts()])
self.events.append(InterceptEvent(attacker_name=self.player,
defender_name=self.enemy,
from_cp=from_cp,
to_cp=to_cp,
game=self))
break
def _generate_player_event(self, event_class, player_cp, enemy_cp):
self.events.append(event_class(self, player_cp, enemy_cp, enemy_cp.position, self.player_name, self.enemy_name))
def _generate_events(self):
enemy_cap_generated = False
enemy_generated_types = []
for player_cp, enemy_cp in self.theater.conflicts(True):
if player_cp.is_global or enemy_cp.is_global:
continue
for event_class, (player_probability, enemy_probability) in EVENT_PROBABILITIES.items():
if self._roll(player_probability, player_cp.base.strength):
if event_class == NavalInterceptEvent and enemy_cp.radials == LAND:
pass
else:
self.events.append(event_class(self.player, self.enemy, player_cp, enemy_cp, self))
elif self._roll(enemy_probability, enemy_cp.base.strength):
if event_class in enemy_generated_types:
continue
if player_cp in self.ignored_cps:
continue
if enemy_cp.base.total_planes == 0:
continue
if event_class == NavalInterceptEvent:
if player_cp.radials == LAND:
continue
elif event_class == CaptureEvent:
if enemy_cap_generated:
continue
if enemy_cp.base.total_armor == 0:
continue
enemy_cap_generated = True
elif event_class == AntiAAStrikeEvent:
if player_cp.base.total_aa == 0:
continue
enemy_generated_types.append(event_class)
self.events.append(event_class(self.enemy, self.player, enemy_cp, player_cp, self))
def commision_unit_types(self, cp: ControlPoint, for_task: Task) -> typing.Collection[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) if x not in db.SAM_BAN]
else:
return db.choose_units(for_task, importance_factor, COMMISION_UNIT_VARIETY, self.enemy)
def _commision_units(self, cp: ControlPoint):
for for_task in [PinpointStrike, 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)
d = {random.choice(unittypes): points_to_spend}
print("Commision {}: {}".format(cp, d))
cp.base.commision_units(d)
for front_line in self.theater.conflicts(True):
self._generate_player_event(FrontlineAttackEvent,
front_line.control_point_a,
front_line.control_point_b)
@property
def budget_reward_amount(self):
reward = 0
if len(self.theater.player_points()) > 0:
total_importance = sum([x.importance * x.base.strength for x in self.theater.player_points()])
return math.ceil(math.log(total_importance + 1, PLAYER_BUDGET_IMPORTANCE_LOG) * PLAYER_BUDGET_BASE * self.settings.multiplier)
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():
reward = reward + REWARDS[g.category]
return reward
else:
return 0
return reward
def _budget_player(self):
self.budget += self.budget_reward_amount
@@ -179,8 +167,8 @@ class Game:
self.budget -= AWACS_BUDGET_COST
def units_delivery_event(self, to_cp: ControlPoint) -> UnitsDeliveryEvent:
event = UnitsDeliveryEvent(attacker_name=self.player,
defender_name=self.player,
event = UnitsDeliveryEvent(attacker_name=self.player_name,
defender_name=self.player_name,
from_cp=to_cp,
to_cp=to_cp,
game=self)
@@ -192,12 +180,12 @@ class Game:
self.events.remove(event)
def initiate_event(self, event: Event):
assert event in self.events
#assert event in self.events
logging.info("Generating {} (regular)".format(event))
event.generate()
event.generate_quick()
def finish_event(self, event: Event, debriefing: Debriefing):
logging.info("Finishing event {}".format(event))
event.commit(debriefing)
if event.is_successfull(debriefing):
self.budget += event.bonus()
@@ -205,29 +193,258 @@ class Game:
if event in self.events:
self.events.remove(event)
else:
print("finish_event: event not in the events!")
logging.info("finish_event: event not in the events!")
def is_player_attack(self, event: Event):
return event.attacker_name == self.player
def is_player_attack(self, event):
if isinstance(event, Event):
return event and event.attacker_name and event.attacker_name == self.player_name
else:
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)
# Save game compatibility.
# TODO: Remove in 2.3.
if not hasattr(self, "conditions"):
self.conditions = self.generate_conditions()
def pass_turn(self, no_action: bool = False) -> None:
logging.info("Pass turn")
self.informations.append(Information("End of turn #" + str(self.turn), "-" * 40, 0))
self.turn += 1
def pass_turn(self, no_action=False, ignored_cps: typing.Collection[ControlPoint]=None):
for event in self.events:
event.skip()
if self.settings.version == "dev":
# don't damage player CPs in by skipping in dev mode
if isinstance(event, UnitsDeliveryEvent):
event.skip()
else:
event.skip()
if not no_action:
self._budget_player()
for cp in self.theater.enemy_points():
self._commision_units(cp)
self._enemy_reinforcement()
self._budget_player()
if not no_action and self.turn > 1:
for cp in self.theater.player_points():
cp.base.affect_strength(+PLAYER_BASE_STRENGTH_RECOVERY)
else:
for cp in self.theater.player_points():
if not cp.is_carrier and not cp.is_lha:
cp.base.affect_strength(-PLAYER_BASE_STRENGTH_RECOVERY)
self.ignored_cps = []
if ignored_cps:
self.ignored_cps = ignored_cps
self.conditions = self.generate_conditions()
self.events = [] # type: typing.List[Event]
self.initialize_turn()
# Autosave progress
persistency.autosave(self)
def initialize_turn(self) -> None:
self.events = []
self._generate_events()
self._generate_globalinterceptions()
# Update statistics
self.game_stats.update(self)
self.aircraft_inventory.reset()
for cp in self.theater.controlpoints:
self.aircraft_inventory.set_from_control_point(cp)
# Plan flights & combat for next turn
self.__culling_points = self.compute_conflicts_position()
self.ground_planners = {}
self.blue_ato.clear()
self.red_ato.clear()
CoalitionMissionPlanner(self, is_player=True).plan_missions()
CoalitionMissionPlanner(self, is_player=False).plan_missions()
for cp in self.theater.controlpoints:
if cp.has_frontline:
gplanner = GroundPlanner(cp, self)
gplanner.plan_groundwar()
self.ground_planners[cp.id] = gplanner
def _enemy_reinforcement(self):
"""
Compute and commision reinforcement for enemy bases
"""
MAX_ARMOR = 30 * self.settings.multiplier
MAX_AIRCRAFT = 25 * self.settings.multiplier
production = 0.0
for enemy_point in self.theater.enemy_points():
for g in enemy_point.ground_objects:
if g.category in REWARDS.keys():
production = production + REWARDS[g.category]
production = production * 0.75
budget_for_armored_units = production / 2
budget_for_aircraft = production / 2
potential_cp_armor = []
for cp in self.theater.enemy_points():
for cpe in cp.connected_points:
if cpe.captured and cp.base.total_armor < MAX_ARMOR:
potential_cp_armor.append(cp)
if len(potential_cp_armor) == 0:
potential_cp_armor = self.theater.enemy_points()
i = 0
potential_units = db.FACTIONS[self.enemy_name].frontline_units
print("Enemy Recruiting")
print(potential_cp_armor)
print(budget_for_armored_units)
print(potential_units)
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
while budget_for_armored_units > 0:
i = i + 1
if i > 50 or budget_for_armored_units <= 0:
break
target_cp = random.choice(potential_cp_armor)
if target_cp.base.total_armor >= MAX_ARMOR:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
budget_for_armored_units -= price * 2
target_cp.base.armor[unit] = target_cp.base.armor.get(unit, 0) + 2
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
print(str(info))
self.informations.append(info)
if budget_for_armored_units > 0:
budget_for_aircraft += budget_for_armored_units
potential_units = [u for u in db.FACTIONS[self.enemy_name].aircrafts
if u in db.UNIT_BY_TASK[CAS] or u in db.UNIT_BY_TASK[CAP]]
if len(potential_units) > 0 and len(potential_cp_armor) > 0:
while budget_for_aircraft > 0:
i = i + 1
if i > 50 or budget_for_aircraft <= 0:
break
target_cp = random.choice(potential_cp_armor)
if target_cp.base.total_planes >= MAX_AIRCRAFT:
continue
unit = random.choice(potential_units)
price = db.PRICES[unit] * 2
budget_for_aircraft -= price * 2
target_cp.base.aircraft[unit] = target_cp.base.aircraft.get(unit, 0) + 2
info = Information("Enemy Reinforcement", unit.id + " x 2 at " + target_cp.name, self.turn)
print(str(info))
self.informations.append(info)
@property
def current_turn_time_of_day(self) -> TimeOfDay:
return list(TimeOfDay)[self.turn % 4]
@property
def current_day(self) -> date:
return self.date + timedelta(days=self.turn // 4)
def next_unit_id(self):
"""
Next unit id for pre-generated units
"""
self.current_unit_id += 1
return self.current_unit_id
def next_group_id(self):
"""
Next unit id for pre-generated units
"""
self.current_group_id += 1
return self.current_group_id
def compute_conflicts_position(self):
"""
Compute the current conflict center position(s), mainly used for culling calculation
:return: List of points of interests
"""
points = []
# By default, use the existing frontline conflict position
for front_line in self.theater.conflicts():
position = Conflict.frontline_position(self.theater,
front_line.control_point_a,
front_line.control_point_b)
points.append(position[0])
points.append(front_line.control_point_a.position)
points.append(front_line.control_point_b.position)
# If there is no conflict take the center point between the two nearest opposing bases
if len(points) == 0:
cpoint = None
min_distance = sys.maxsize
for cp in self.theater.player_points():
for cp2 in self.theater.enemy_points():
d = cp.position.distance_to_point(cp2.position)
if d < min_distance:
min_distance = d
cpoint = Point((cp.position.x + cp2.position.x) / 2, (cp.position.y + cp2.position.y) / 2)
points.append(cp.position)
points.append(cp2.position)
break
if cpoint is not None:
break
if cpoint is not None:
points.append(cpoint)
# Else 0,0, since we need a default value
# (in this case this means the whole map is owned by the same player, so it is not an issue)
if len(points) == 0:
points.append(Point(0, 0))
return points
def add_destroyed_units(self, data):
pos = Point(data["x"], data["z"])
if self.theater.is_on_land(pos):
self.__destroyed_units.append(data)
def get_destroyed_units(self):
return self.__destroyed_units
def position_culled(self, pos):
"""
Check if unit can be generated at given position depending on culling performance settings
:param pos: Position you are tryng to spawn stuff at
:return: True if units can not be added at given position
"""
if self.settings.perf_culling == False:
return False
else:
for c in self.__culling_points:
if c.distance_to_point(pos) < self.settings.perf_culling_distance * 1000:
return False
return True
def get_culling_points(self):
"""
Check culling points
:return: List of culling points
"""
return self.__culling_points
# 1 = red, 2 = blue
def get_player_coalition_id(self):
return 2
def get_enemy_coalition_id(self):
return 1
def get_player_coalition(self):
return Coalition.Blue
def get_enemy_coalition(self):
return Coalition.Red
def get_player_color(self):
return "blue"
def get_enemy_color(self):
return "red"

11
game/infos/information.py Normal file
View File

@@ -0,0 +1,11 @@
class Information():
def __init__(self, title="", text="", turn=0):
self.title = title
self.text = text
self.turn = turn
def __str__(self):
s = "[" + str(self.turn) + "] " + self.title + "\n" + self.text
return s

123
game/inventory.py Normal file
View File

@@ -0,0 +1,123 @@
"""Inventory management APIs."""
from collections import defaultdict
from typing import Dict, Iterable, Iterator, Set, Tuple
from dcs.unittype import UnitType
from gen.flights.flight import Flight
from theater import ControlPoint
class ControlPointAircraftInventory:
"""Aircraft inventory for a single control point."""
def __init__(self, control_point: ControlPoint) -> None:
self.control_point = control_point
self.inventory: Dict[UnitType, int] = defaultdict(int)
def add_aircraft(self, aircraft: UnitType, count: int) -> None:
"""Adds aircraft to the inventory.
Args:
aircraft: The type of aircraft to add.
count: The number of aircraft to add.
"""
self.inventory[aircraft] += count
def remove_aircraft(self, aircraft: UnitType, count: int) -> None:
"""Removes aircraft from the inventory.
Args:
aircraft: The type of aircraft to remove.
count: The number of aircraft to remove.
Raises:
ValueError: The control point cannot fulfill the requested number of
aircraft.
"""
available = self.inventory[aircraft]
if available < count:
raise ValueError(
f"Cannot remove {count} {aircraft.id} from "
f"{self.control_point.name}. Only have {available}."
)
self.inventory[aircraft] -= count
def available(self, aircraft: UnitType) -> int:
"""Returns the number of available aircraft of the given type.
Args:
aircraft: The type of aircraft to query.
"""
try:
return self.inventory[aircraft]
except KeyError:
return 0
@property
def types_available(self) -> Iterator[UnitType]:
"""Iterates over all available aircraft types."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft
@property
def all_aircraft(self) -> Iterator[Tuple[UnitType, int]]:
"""Iterates over all available aircraft types, including amounts."""
for aircraft, count in self.inventory.items():
if count > 0:
yield aircraft, count
def clear(self) -> None:
"""Clears all aircraft from the inventory."""
self.inventory.clear()
class GlobalAircraftInventory:
"""Game-wide aircraft inventory."""
def __init__(self, control_points: Iterable[ControlPoint]) -> None:
self.inventories: Dict[ControlPoint, ControlPointAircraftInventory] = {
cp: ControlPointAircraftInventory(cp) for cp in control_points
}
def reset(self) -> None:
"""Clears all control points and their inventories."""
for inventory in self.inventories.values():
inventory.clear()
def set_from_control_point(self, control_point: ControlPoint) -> None:
"""Set the control point's aircraft inventory.
If the inventory for the given control point has already been set for
the turn, it will be overwritten.
"""
inventory = self.inventories[control_point]
for aircraft, count in control_point.base.aircraft.items():
inventory.add_aircraft(aircraft, count)
def for_control_point(
self,
control_point: ControlPoint) -> ControlPointAircraftInventory:
"""Returns the inventory specific to the given control point."""
return self.inventories[control_point]
@property
def available_types_for_player(self) -> Iterator[UnitType]:
"""Iterates over all aircraft types available to the player."""
seen: Set[UnitType] = set()
for control_point, inventory in self.inventories.items():
if control_point.captured:
for aircraft in inventory.types_available:
if aircraft not in seen:
seen.add(aircraft)
yield aircraft
def claim_for_flight(self, flight: Flight) -> None:
"""Removes aircraft from the inventory for the given flight."""
inventory = self.for_control_point(flight.from_cp)
inventory.remove_aircraft(flight.unit_type, flight.count)
def return_from_flight(self, flight: Flight) -> None:
"""Returns a flight's aircraft to the inventory."""
inventory = self.for_control_point(flight.from_cp)
inventory.add_aircraft(flight.unit_type, flight.count)

View File

@@ -0,0 +1,14 @@
class DestroyedUnit:
"""
Store info about a destroyed unit
"""
x: int
y: int
name: str
def __init__(self, x , y, name):
self.x = x
self.y = y
self.name = name

View File

@@ -0,0 +1,13 @@
from theater import ControlPoint
class FrontlineData:
"""
This Data structure will store information about an existing frontline
"""
def __init__(self, from_cp:ControlPoint, to_cp: ControlPoint):
self.to_cp = to_cp
self.from_cp = from_cp
self.enemy_units_position = []
self.blue_units_position = []

56
game/models/game_stats.py Normal file
View File

@@ -0,0 +1,56 @@
from typing import List
class FactionTurnMetadata:
"""
Store metadata about a faction
"""
aircraft_count: int = 0
vehicles_count: int = 0
sam_count: int = 0
def __init__(self):
self.aircraft_count = 0
self.vehicles_count = 0
self.sam_count = 0
class GameTurnMetadata:
"""
Store metadata about a game turn
"""
allied_units:FactionTurnMetadata
enemy_units:FactionTurnMetadata
def __init__(self):
self.allied_units = FactionTurnMetadata()
self.enemy_units = FactionTurnMetadata()
class GameStats:
"""
Store statistics for the current game
"""
def __init__(self):
self.data_per_turn: List[GameTurnMetadata] = []
def update(self, game):
"""
Save data for current turn
:param game: Game we want to save the data about
"""
turn_data = GameTurnMetadata()
for cp in game.theater.controlpoints:
if cp.captured:
turn_data.allied_units.aircraft_count += sum(cp.base.aircraft.values())
turn_data.allied_units.vehicles_count += sum(cp.base.armor.values())
else:
turn_data.enemy_units.aircraft_count += sum(cp.base.aircraft.values())
turn_data.enemy_units.vehicles_count += sum(cp.base.armor.values())
self.data_per_turn.append(turn_data)

View File

@@ -1,53 +0,0 @@
from dcs.terrain import Terrain
from game import db
from gen.armor import *
from gen.aircraft import *
from gen.aaa import *
from gen.shipgen import *
from gen.triggergen import *
from gen.awacsgen import *
from gen.visualgen import *
from gen.conflictgen import Conflict
from .operation import Operation
class AntiAAStrikeOperation(Operation):
strikegroup = None # type: db.PlaneDict
interceptors = None # type: db.PlaneDict
target = None # type: db.ArmorDict
def setup(self,
target: db.ArmorDict,
strikegroup: db.PlaneDict,
interceptors: db.PlaneDict):
self.strikegroup = strikegroup
self.interceptors = interceptors
self.target = target
def prepare(self, terrain: Terrain, is_quick: bool):
super(AntiAAStrikeOperation, self).prepare(terrain, is_quick)
if self.defender_name == self.game.player:
self.attackers_starting_position = None
self.defenders_starting_position = None
conflict = Conflict.ground_base_attack(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.mission,
conflict=conflict)
def generate(self):
self.airgen.generate_cas_strikegroup(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position)
if self.interceptors:
self.airgen.generate_defense(self.interceptors, clients=self.defender_clients, at=self.defenders_starting_position)
self.armorgen.generate({}, self.target)
super(AntiAAStrikeOperation, self).generate()

View File

@@ -1,67 +0,0 @@
from game import db
from gen.conflictgen import Conflict
from gen.armor import *
from gen.aircraft import *
from gen.aaa import *
from gen.shipgen import *
from gen.triggergen import *
from gen.awacsgen import *
from gen.visualgen import *
from .operation import Operation
class CaptureOperation(Operation):
cas = None # type: db.PlaneDict
escort = None # type: db.PlaneDict
intercept = None # type: db.PlaneDict
attack = None # type: db.ArmorDict
defense = None # type: db.ArmorDict
aa = None # type: db.AirDefenseDict
trigger_radius = TRIGGER_RADIUS_SMALL
def setup(self,
cas: db.PlaneDict,
escort: db.PlaneDict,
attack: db.ArmorDict,
intercept: db.PlaneDict,
defense: db.ArmorDict,
aa: db.AirDefenseDict):
self.cas = cas
self.escort = escort
self.intercept = intercept
self.attack = attack
self.defense = defense
self.aa = aa
def prepare(self, terrain: dcs.terrain.Terrain, is_quick: bool):
super(CaptureOperation, self).prepare(terrain, is_quick)
self.defenders_starting_position = None
if self.game.player == self.defender_name:
self.attackers_starting_position = None
conflict = Conflict.capture_conflict(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.mission,
conflict=conflict)
def generate(self):
self.armorgen.generate(self.attack, self.defense)
self.aagen.generate(self.aa)
self.airgen.generate_defense(self.intercept, clients=self.defender_clients, at=self.defenders_starting_position)
self.airgen.generate_cas_strikegroup(self.cas, clients=self.attacker_clients, at=self.attackers_starting_position)
self.airgen.generate_strikegroup_escort(self.escort, clients=self.attacker_clients, at=self.attackers_starting_position)
self.visualgen.generate_target_smokes(self.to_cp)
super(CaptureOperation, self).generate()

View File

@@ -0,0 +1,38 @@
from dcs.terrain.terrain import Terrain
from gen.conflictgen import Conflict
from .operation import Operation
from .. import db
MAX_DISTANCE_BETWEEN_GROUPS = 12000
class FrontlineAttackOperation(Operation):
interceptors = None # type: db.AssignedUnitsDict
escort = None # type: db.AssignedUnitsDict
strikegroup = None # type: db.AssignedUnitsDict
attackers = None # type: db.ArmorDict
defenders = None # type: db.ArmorDict
def prepare(self, terrain: Terrain, is_quick: bool):
super(FrontlineAttackOperation, self).prepare(terrain, is_quick)
if self.defender_name == self.game.player_name:
self.attackers_starting_position = None
self.defenders_starting_position = None
conflict = Conflict.frontline_cas_conflict(
attacker_name=self.attacker_name,
defender_name=self.defender_name,
attacker=self.current_mission.country(self.attacker_country),
defender=self.current_mission.country(self.defender_country),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.current_mission,
conflict=conflict)
def generate(self):
super(FrontlineAttackOperation, self).generate()

View File

@@ -1,44 +0,0 @@
from dcs.terrain import Terrain
from game import db
from gen.armor import *
from gen.aircraft import *
from gen.aaa import *
from gen.shipgen import *
from gen.triggergen import *
from gen.awacsgen import *
from gen.visualgen import *
from gen.conflictgen import Conflict
from .operation import Operation
class GroundAttackOperation(Operation):
strikegroup = None # type: db.PlaneDict
target = None # type: db.ArmorDict
def setup(self,
target: db.ArmorDict,
strikegroup: db.PlaneDict):
self.strikegroup = strikegroup
self.target = target
def prepare(self, terrain: Terrain, is_quick: bool):
super(GroundAttackOperation, self).prepare(terrain, is_quick)
conflict = Conflict.ground_attack_conflict(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.mission,
conflict=conflict)
def generate(self):
self.airgen.generate_defense(self.strikegroup, self.defender_clients, self.defenders_starting_position)
self.armorgen.generate(self.target, {})
super(GroundAttackOperation, self).generate()

View File

@@ -1,53 +0,0 @@
from dcs.terrain import Terrain
from game import db
from gen.armor import *
from gen.aircraft import *
from gen.aaa import *
from gen.shipgen import *
from gen.triggergen import *
from gen.awacsgen import *
from gen.visualgen import *
from gen.conflictgen import Conflict
from .operation import Operation
class GroundInterceptOperation(Operation):
strikegroup = None # type: db.PlaneDict
interceptors = None # type: db.PlaneDict
target = None # type: db.ArmorDict
def setup(self,
target: db.ArmorDict,
strikegroup: db.PlaneDict,
interceptors: db.PlaneDict):
self.strikegroup = strikegroup
self.interceptors = interceptors
self.target = target
def prepare(self, terrain: Terrain, is_quick: bool):
super(GroundInterceptOperation, self).prepare(terrain, is_quick)
if self.defender_name == self.game.player:
self.attackers_starting_position = None
self.defenders_starting_position = None
conflict = Conflict.ground_intercept_conflict(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.mission,
conflict=conflict)
def generate(self):
self.airgen.generate_cas_strikegroup(self.strikegroup, clients=self.attacker_clients, at=self.attackers_starting_position)
if self.interceptors:
self.airgen.generate_defense(self.interceptors, clients=self.defender_clients, at=self.defenders_starting_position)
self.armorgen.generate({}, self.target)
super(GroundInterceptOperation, self).generate()

View File

@@ -1,56 +0,0 @@
from dcs.terrain import Terrain
from game import db
from gen.armor import *
from gen.aircraft import *
from gen.aaa import *
from gen.shipgen import *
from gen.triggergen import *
from gen.awacsgen import *
from gen.visualgen import *
from gen.conflictgen import Conflict
from .operation import Operation
class InfantryTransportOperation(Operation):
transport = None # type: db.HeliDict
aa = None # type: db.AirDefenseDict
def setup(self, transport: db.HeliDict, aa: db.AirDefenseDict):
self.transport = transport
self.aa = aa
def prepare(self, terrain: Terrain, is_quick: bool):
super(InfantryTransportOperation, self).prepare(terrain, is_quick)
conflict = Conflict.transport_conflict(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.mission,
conflict=conflict)
def generate(self):
self.airgen.generate_passenger_transport(
helis=self.transport,
clients=self.attacker_clients,
at=self.attackers_starting_position
)
self.armorgen.generate_passengers(count=6)
self.aagen.generate_at_defenders_location(self.aa)
self.visualgen.generate_transportation_marker(self.conflict.ground_attackers_location)
self.visualgen.generate_transportation_destination(self.conflict.position)
# TODO: horrible, horrible hack
# this will disable vehicle activation triggers,
# which aren't needed on this type of missions
self.is_quick = True
super(InfantryTransportOperation, self).generate()
self.is_quick = False

View File

@@ -1,52 +0,0 @@
from dcs.terrain import Terrain
from gen import *
from .operation import Operation
class InterceptOperation(Operation):
escort = None # type: db.PlaneDict
transport = None # type: db.PlaneDict
interceptors = None # type: db.PlaneDict
airdefense = None # type: db.AirDefenseDict
trigger_radius = TRIGGER_RADIUS_LARGE
def setup(self,
escort: db.PlaneDict,
transport: db.PlaneDict,
airdefense: db.AirDefenseDict,
interceptors: db.PlaneDict):
self.escort = escort
self.transport = transport
self.airdefense = airdefense
self.interceptors = interceptors
def prepare(self, terrain: Terrain, is_quick: bool):
super(InterceptOperation, self).prepare(terrain, is_quick)
self.defenders_starting_position = None
if self.defender_name == self.game.player:
self.attackers_starting_position = None
conflict = Conflict.intercept_conflict(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(mission=self.mission,
conflict=conflict)
def generate(self):
self.airgen.generate_transport(self.transport, self.to_cp.at)
self.airgen.generate_transport_escort(self.escort, clients=self.defender_clients)
if self.from_cp.is_global:
super(InterceptOperation, self).generate()
self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=self.attackers_starting_position)
else:
self.airgen.generate_interception(self.interceptors, clients=self.attacker_clients, at=self.attackers_starting_position)
super(InterceptOperation, self).generate()

View File

@@ -1,53 +0,0 @@
from dcs.terrain import Terrain
from gen import *
from .operation import Operation
class NavalInterceptionOperation(Operation):
strikegroup = None # type: db.PlaneDict
interceptors = None # type: db.PlaneDict
targets = None # type: db.ShipDict
trigger_radius = TRIGGER_RADIUS_LARGE
def setup(self,
strikegroup: db.PlaneDict,
interceptors: db.PlaneDict,
targets: db.ShipDict):
self.strikegroup = strikegroup
self.interceptors = interceptors
self.targets = targets
def prepare(self, terrain: Terrain, is_quick: bool):
super(NavalInterceptionOperation, self).prepare(terrain, is_quick)
if self.defender_name == self.game.player:
self.attackers_starting_position = None
conflict = Conflict.naval_intercept_conflict(
attacker=self.mission.country(self.attacker_name),
defender=self.mission.country(self.defender_name),
from_cp=self.from_cp,
to_cp=self.to_cp,
theater=self.game.theater
)
self.initialize(self.mission, conflict)
def generate(self):
super(NavalInterceptionOperation, self).generate()
target_groups = self.shipgen.generate_cargo(units=self.targets)
self.airgen.generate_ship_strikegroup(
attackers=self.strikegroup,
clients=self.attacker_clients,
target_groups=target_groups,
at=self.attackers_starting_position
)
if self.interceptors:
self.airgen.generate_defense(
defenders=self.interceptors,
clients=self.defender_clients,
at=self.defenders_starting_position
)

View File

@@ -1,103 +1,507 @@
from dcs.terrain import Terrain
import logging
import os
from pathlib import Path
from typing import List, Optional, Set
from userdata.debriefing import *
from dcs import Mission
from dcs.action import DoScript, DoScriptFile
from dcs.coalition import Coalition
from dcs.countries import country_dict
from dcs.lua.parse import loads
from dcs.mapping import Point
from dcs.terrain.terrain import Terrain
from dcs.translation import String
from dcs.triggers import TriggerStart
from dcs.unittype import UnitType
from theater import *
from gen import *
from game.plugins import LuaPluginManager
from gen import Conflict, FlightType, VisualGenerator
from gen.aircraft import AIRCRAFT_DATA, AircraftConflictGenerator, FlightData
from gen.airfields import AIRFIELD_DATA
from gen.airsupportgen import AirSupport, AirSupportConflictGenerator
from gen.armor import GroundConflictGenerator, JtacInfo
from gen.beacons import load_beacons_for_terrain
from gen.briefinggen import BriefingGenerator, MissionInfoGenerator
from gen.environmentgen import EnvironmentGenerator
from gen.forcedoptionsgen import ForcedOptionsGenerator
from gen.groundobjectsgen import GroundObjectsGenerator
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 theater import ControlPoint
from .. import db
from ..debriefing import Debriefing
class Operation:
attackers_starting_position = None # type: db.StartingPosition
defenders_starting_position = None # type: db.StartingPosition
mission = None # type: dcs.Mission
current_mission = None # type: Mission
regular_mission = None # type: Mission
quick_mission = None # type: Mission
conflict = None # type: Conflict
armorgen = None # type: ArmorConflictGenerator
airgen = None # type: AircraftConflictGenerator
aagen = None # type: AAConflictGenerator
extra_aagen = None # type: ExtraAAConflictGenerator
shipgen = None # type: ShipGenerator
triggersgen = None # type: TriggersGenerator
awacsgen = None # type: AWACSConflictGenerator
airsupportgen = None # type: AirSupportConflictGenerator
visualgen = None # type: VisualGenerator
envgen = None # type: EnvironmentGenerator
groundobjectgen = None # type: GroundObjectsGenerator
briefinggen = None # type: BriefingGenerator
forcedoptionsgen = None # type: ForcedOptionsGenerator
radio_registry: Optional[RadioRegistry] = None
tacan_registry: Optional[TacanRegistry] = None
environment_settings = None
trigger_radius = TRIGGER_RADIUS_MEDIUM
is_quick = None
is_awacs_enabled = False
ca_slots = 0
def __init__(self,
game,
attacker_name: str,
defender_name: str,
attacker_clients: db.PlaneDict,
defender_clients: db.PlaneDict,
from_cp: ControlPoint,
to_cp: ControlPoint = None):
departure_cp: ControlPoint,
to_cp: ControlPoint):
self.game = game
self.attacker_name = attacker_name
self.attacker_country = db.FACTIONS[attacker_name].country
self.defender_name = defender_name
self.attacker_clients = attacker_clients
self.defender_clients = defender_clients
self.defender_country = db.FACTIONS[defender_name].country
print(self.defender_country, self.attacker_country)
self.from_cp = from_cp
self.departure_cp = departure_cp
self.to_cp = to_cp
self.is_quick = False
self.plugin_scripts: List[str] = []
def units_of(self, country_name: str) -> List[UnitType]:
return []
def is_successfull(self, debriefing: Debriefing) -> bool:
return True
@property
def is_player_attack(self) -> bool:
return self.from_cp.captured
def initialize(self, mission: Mission, conflict: Conflict):
self.mission = mission
self.current_mission = mission
self.conflict = conflict
self.armorgen = ArmorConflictGenerator(mission, conflict)
self.airgen = AircraftConflictGenerator(mission, conflict, self.game.settings)
self.aagen = AAConflictGenerator(mission, conflict)
self.shipgen = ShipGenerator(mission, conflict)
self.awacsgen = AWACSConflictGenerator(mission, conflict, self.game)
self.triggersgen = TriggersGenerator(mission, conflict, self.game)
self.visualgen = VisualGenerator(mission, conflict, self.game)
self.envgen = EnviromentGenerator(mission, conflict, self.game)
player_name = self.from_cp.captured and self.attacker_name or self.defender_name
enemy_name = self.from_cp.captured and self.defender_name or self.attacker_name
self.extra_aagen = ExtraAAConflictGenerator(mission, conflict, self.game, player_name, enemy_name)
# 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):
self.mission = dcs.Mission(terrain)
with open("resources/default_options.lua", "r") as f:
options_dict = loads(f.read())["options"]
self.current_mission = Mission(terrain)
print(self.game.player_country)
print(country_dict[db.country_id_from_name(self.game.player_country)])
print(country_dict[db.country_id_from_name(self.game.player_country)]())
# Setup coalition :
self.current_mission.coalition["blue"] = Coalition("blue")
self.current_mission.coalition["red"] = Coalition("red")
p_country = self.game.player_country
e_country = self.game.enemy_country
self.current_mission.coalition["blue"].add_country(country_dict[db.country_id_from_name(p_country)]())
self.current_mission.coalition["red"].add_country(country_dict[db.country_id_from_name(e_country)]())
print([c for c in self.current_mission.coalition["blue"].countries.keys()])
print([c for c in self.current_mission.coalition["red"].countries.keys()])
if is_quick:
self.quick_mission = self.current_mission
else:
self.regular_mission = self.current_mission
self.current_mission.options.load_from_dict(options_dict)
self.is_quick = is_quick
if is_quick:
self.attackers_starting_position = None
self.defenders_starting_position = None
else:
self.attackers_starting_position = self.from_cp.at
self.defenders_starting_position = self.to_cp.at
self.attackers_starting_position = self.departure_cp.at
# TODO: Is this possible?
if self.to_cp is not None:
self.defenders_starting_position = self.to_cp.at
else:
self.defenders_starting_position = None
def inject_lua_trigger(self, contents: str, comment: str) -> None:
trigger = TriggerStart(comment=comment)
trigger.add_action(DoScript(String(contents)))
self.current_mission.triggerrules.triggers.append(trigger)
def bypass_plugin_script(self, mnemonic: str) -> None:
self.plugin_scripts.append(mnemonic)
def inject_plugin_script(self, plugin_mnemonic: str, script: str,
script_mnemonic: str) -> None:
if script_mnemonic in self.plugin_scripts:
logging.debug(
f"Skipping already loaded {script} for {plugin_mnemonic}"
)
else:
self.plugin_scripts.append(script_mnemonic)
plugin_path = Path("./resources/plugins", plugin_mnemonic)
script_path = Path(plugin_path, script)
if not script_path.exists():
logging.error(
f"Cannot find {script_path} for plugin {plugin_mnemonic}"
)
return
trigger = TriggerStart(comment=f"Load {script_mnemonic}")
filename = script_path.resolve()
fileref = self.current_mission.map_resource.add_resource_file(filename)
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):
self.visualgen.generate()
radio_registry = RadioRegistry()
tacan_registry = TacanRegistry()
if self.is_awacs_enabled:
self.awacsgen.generate()
# Dedup beacon/radio frequencies, since some maps have some frequencies
# used multiple times.
beacons = load_beacons_for_terrain(self.game.theater.terrain.name)
unique_map_frequencies: Set[RadioFrequency] = set()
for beacon in beacons:
unique_map_frequencies.add(beacon.frequency)
if beacon.is_tacan:
if beacon.channel is None:
logging.error(
f"TACAN beacon has no channel: {beacon.callsign}")
else:
tacan_registry.reserve(beacon.tacan_channel)
self.extra_aagen.generate()
self.triggersgen.generate(self.is_quick, self.trigger_radius)
for airfield, data in AIRFIELD_DATA.items():
if data.theater == self.game.theater.terrain.name:
unique_map_frequencies.add(data.atc.hf)
unique_map_frequencies.add(data.atc.vhf_fm)
unique_map_frequencies.add(data.atc.vhf_am)
unique_map_frequencies.add(data.atc.uhf)
# No need to reserve ILS or TACAN because those are in the
# beacon list.
if self.environment_settings is None:
self.environment_settings = self.envgen.generate()
else:
self.envgen.load(self.environment_settings)
for frequency in unique_map_frequencies:
radio_registry.reserve(frequency)
for global_cp in self.game.theater.controlpoints:
if not global_cp.is_global:
# Set mission time and weather conditions.
EnvironmentGenerator(self.current_mission,
self.game.conditions).generate()
# Generate ground object first
groundobjectgen = GroundObjectsGenerator(
self.current_mission,
self.conflict,
self.game,
radio_registry,
tacan_registry
)
groundobjectgen.generate()
# Generate destroyed units
for d in self.game.get_destroyed_units():
try:
utype = db.unit_type_from_name(d["type"])
except KeyError:
continue
ship = self.shipgen.generate_carrier(type=db.find_unittype(Carriage, self.game.player)[0],
country=self.game.player,
at=global_cp.at)
pos = Point(d["x"], d["z"])
if utype is not None and not self.game.position_culled(pos) and self.game.settings.perf_destroyed_units:
self.current_mission.static_group(
country=self.current_mission.country(self.game.player_country),
name="",
_type=utype,
hidden=True,
position=pos,
heading=d["orientation"],
dead=True,
)
if global_cp == self.from_cp and not self.is_quick:
self.attackers_starting_position = ship
# Air Support (Tanker & Awacs)
airsupportgen = AirSupportConflictGenerator(
self.current_mission, self.conflict, self.game, radio_registry,
tacan_registry)
airsupportgen.generate(self.is_awacs_enabled)
def units_of(self, country_name: str) -> typing.Collection[UnitType]:
return []
# Generate Activity on the map
airgen = AircraftConflictGenerator(
self.current_mission, self.conflict, self.game.settings, self.game,
radio_registry)
def is_successfull(self, debriefing: Debriefing) -> bool:
return True
airgen.generate_flights(
self.current_mission.country(self.game.player_country),
self.game.blue_ato,
groundobjectgen.runways
)
airgen.generate_flights(
self.current_mission.country(self.game.enemy_country),
self.game.red_ato,
groundobjectgen.runways
)
# Generate ground units on frontline everywhere
jtacs: List[JtacInfo] = []
for front_line in self.game.theater.conflicts(True):
player_cp = front_line.control_point_a
enemy_cp = front_line.control_point_b
conflict = Conflict.frontline_cas_conflict(self.attacker_name, self.defender_name,
self.current_mission.country(self.attacker_country),
self.current_mission.country(self.defender_country),
player_cp, enemy_cp, self.game.theater)
# Generate frontline ops
player_gp = self.game.ground_planners[player_cp.id].units_per_cp[enemy_cp.id]
enemy_gp = self.game.ground_planners[enemy_cp.id].units_per_cp[player_cp.id]
groundConflictGen = GroundConflictGenerator(self.current_mission, conflict, self.game, player_gp, enemy_gp, player_cp.stances[enemy_cp.id])
groundConflictGen.generate()
jtacs.extend(groundConflictGen.jtacs)
# Setup combined arms parameters
self.current_mission.groundControl.pilot_can_control_vehicles = self.ca_slots > 0
if self.game.player_country in [country.name for country in self.current_mission.coalition["blue"].countries.values()]:
self.current_mission.groundControl.blue_tactical_commander = self.ca_slots
else:
self.current_mission.groundControl.red_tactical_commander = self.ca_slots
# Triggers
triggersgen = TriggersGenerator(self.current_mission, self.conflict,
self.game)
triggersgen.generate()
# Options
forcedoptionsgen = ForcedOptionsGenerator(self.current_mission,
self.conflict, self.game)
forcedoptionsgen.generate()
# Generate Visuals Smoke Effects
visualgen = VisualGenerator(self.current_mission, self.conflict,
self.game)
if self.game.settings.perf_smoke_gen:
visualgen.generate()
luaData = {}
luaData["AircraftCarriers"] = {}
luaData["Tankers"] = {}
luaData["AWACs"] = {}
luaData["JTACs"] = {}
luaData["TargetPoints"] = {}
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
for tanker in airsupportgen.air_support.tankers:
luaData["Tankers"][tanker.callsign] = {
"dcsGroupName": tanker.dcsGroupName,
"callsign": tanker.callsign,
"variant": tanker.variant,
"radio": tanker.freq.mhz,
"tacan": str(tanker.tacan.number) + tanker.tacan.band.name
}
if self.is_awacs_enabled:
for awacs in airsupportgen.air_support.awacs:
luaData["AWACs"][awacs.callsign] = {
"dcsGroupName": awacs.dcsGroupName,
"callsign": awacs.callsign,
"radio": awacs.freq.mhz
}
for jtac in jtacs:
luaData["JTACs"][jtac.callsign] = {
"dcsGroupName": jtac.dcsGroupName,
"callsign": jtac.callsign,
"zone": jtac.region,
"dcsUnit": jtac.unit_name,
"laserCode": jtac.code
}
for flight in airgen.flights:
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
if flightTarget:
flightTargetName = None
flightTargetType = None
if hasattr(flightTarget, 'obj_name'):
flightTargetName = flightTarget.obj_name
flightTargetType = flightType + f" TGT ({flightTarget.category})"
elif hasattr(flightTarget, 'name'):
flightTargetName = flightTarget.name
flightTargetType = flightType + " TGT (Airbase)"
luaData["TargetPoints"][flightTargetName] = {
"name": flightTargetName,
"type": flightTargetType,
"position": { "x": flightTarget.position.x, "y": flightTarget.position.y}
}
# 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
# later, we'll add data about the units and points having been generated, in order to facilitate the configuration of the plugin lua scripts
state_location = "[[" + os.path.abspath(".") + "]]"
lua = """
-- setting configuration table
env.info("DCSLiberation|: setting configuration table")
-- all data in this table is overridable.
dcsLiberation = {}
-- the base location for state.json; if non-existent, it'll be replaced with LIBERATION_EXPORT_DIR, TEMP, or DCS working directory
dcsLiberation.installPath=""" + state_location + """
"""
# Process the tankers
lua += """
-- list the tankers generated by Liberation
dcsLiberation.Tankers = {
"""
for key in luaData["Tankers"]:
data = luaData["Tankers"][key]
dcsGroupName= data["dcsGroupName"]
callsign = data["callsign"]
variant = data["variant"]
tacan = data["tacan"]
radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', variant='{variant}', tacan='{tacan}', radio='{radio}' }}, \n"
#lua += f" {{name='{dcsGroupName}', description='{callsign} ({variant})', information='Tacan:{tacan} Radio:{radio}' }}, \n"
lua += "}"
# Process the AWACSes
lua += """
-- list the AWACs generated by Liberation
dcsLiberation.AWACs = {
"""
for key in luaData["AWACs"]:
data = luaData["AWACs"][key]
dcsGroupName= data["dcsGroupName"]
callsign = data["callsign"]
radio = data["radio"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', radio='{radio}' }}, \n"
#lua += f" {{name='{dcsGroupName}', description='{callsign} (AWACS)', information='Radio:{radio}' }}, \n"
lua += "}"
# Process the JTACs
lua += """
-- list the JTACs generated by Liberation
dcsLiberation.JTACs = {
"""
for key in luaData["JTACs"]:
data = luaData["JTACs"][key]
dcsGroupName= data["dcsGroupName"]
callsign = data["callsign"]
zone = data["zone"]
laserCode = data["laserCode"]
dcsUnit = data["dcsUnit"]
lua += f" {{dcsGroupName='{dcsGroupName}', callsign='{callsign}', zone='{zone}', laserCode='{laserCode}', dcsUnit='{dcsUnit}' }}, \n"
#lua += f" {{name='{dcsGroupName}', description='JTAC {callsign} ', information='Laser:{laserCode}', jtac={laserCode} }}, \n"
lua += "}"
# Process the Target Points
lua += """
-- list the target points generated by Liberation
dcsLiberation.TargetPoints = {
"""
for key in luaData["TargetPoints"]:
data = luaData["TargetPoints"][key]
name = data["name"]
pointType = data["type"]
positionX = data["position"]["x"]
positionY = data["position"]["y"]
lua += f" {{name='{name}', pointType='{pointType}', positionX='{positionX}', positionY='{positionY}' }}, \n"
#lua += f" {{name='{pointType} {name}', point{{x={positionX}, z={positionY} }} }}, \n"
lua += "}"
lua += """
-- list the airbases generated by Liberation
-- dcsLiberation.Airbases = {}
-- list the aircraft carriers generated by Liberation
-- dcsLiberation.Carriers = {}
-- later, we'll add more data to the table
"""
trigger = TriggerStart(comment="Set DCS Liberation data")
trigger.add_action(DoScript(String(lua)))
self.current_mission.triggerrules.triggers.append(trigger)
# Inject Plugins Lua Scripts and data
for plugin in LuaPluginManager.plugins():
if plugin.enabled:
plugin.inject_scripts(self)
plugin.inject_configuration(self)
self.assign_channels_to_flights(airgen.flights,
airsupportgen.air_support)
self.notify_info_generators(groundobjectgen, airsupportgen, jtacs, airgen)
def assign_channels_to_flights(self, flights: List[FlightData],
air_support: AirSupport) -> None:
"""Assigns preset radio channels for client flights."""
for flight in flights:
if not flight.client_units:
continue
self.assign_channels_to_flight(flight, air_support)
def assign_channels_to_flight(self, flight: FlightData,
air_support: AirSupport) -> None:
"""Assigns preset radio channels for a client flight."""
airframe = flight.aircraft_type
try:
aircraft_data = AIRCRAFT_DATA[airframe.id]
except KeyError:
logging.warning(f"No aircraft data for {airframe.id}")
return
if aircraft_data.channel_allocator is not None:
aircraft_data.channel_allocator.assign_channels_for_flight(
flight, air_support
)

84
game/persistency.py Normal file
View File

@@ -0,0 +1,84 @@
import logging
import os
import pickle
import shutil
from typing import Optional
_dcs_saved_game_folder: Optional[str] = None
_file_abs_path = None
def setup(user_folder: str):
global _dcs_saved_game_folder
_dcs_saved_game_folder = user_folder
_file_abs_path = os.path.join(base_path(), "default.liberation")
def base_path() -> str:
global _dcs_saved_game_folder
assert _dcs_saved_game_folder
return _dcs_saved_game_folder
def _save_file() -> str:
return os.path.join(base_path(), "default.liberation")
def _temporary_save_file() -> str:
return os.path.join(base_path(), "tmpsave.liberation")
def _autosave_path() -> str:
return os.path.join(base_path(), "autosave.liberation")
def _save_file_exists() -> bool:
return os.path.exists(_save_file())
def mission_path_for(name: str) -> str:
return os.path.join(base_path(), "Missions", "{}".format(name))
def restore_game():
if not _save_file_exists():
return None
with open(_save_file(), "rb") as f:
try:
save = pickle.load(f)
return save
except Exception:
logging.exception("Invalid Save game")
return None
def load_game(path):
with open(path, "rb") as f:
try:
save = pickle.load(f)
save.savepath = path
return save
except Exception:
logging.exception("Invalid Save game")
return None
def save_game(game) -> bool:
try:
with open(_temporary_save_file(), "wb") as f:
pickle.dump(game, f)
shutil.copy(_temporary_save_file(), game.savepath)
return True
except Exception:
logging.exception("Could not save game")
return False
def autosave(game) -> bool:
"""
Autosave to the autosave location
:param game: Game to save
:return: True if saved succesfully
"""
try:
with open(_autosave_path(), "wb") as f:
pickle.dump(game, f)
return True
except Exception:
logging.exception("Could not save game")
return False

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,8 +1,76 @@
from typing import Dict
class Settings:
player_skill = "Good"
enemy_skill = "Average"
only_player_takeoff = False
night_disabled = False
multiplier = 1
sams = True
def __init__(self):
# Generator settings
self.inverted = False
self.do_not_generate_carrier = False # TODO : implement
self.do_not_generate_lha = False # TODO : implement
self.do_not_generate_player_navy = True # TODO : implement
self.do_not_generate_enemy_navy = True # TODO : implement
# Difficulty settings
self.player_skill = "Good"
self.enemy_skill = "Average"
self.enemy_vehicle_skill = "Average"
self.map_coalition_visibility = "All Units"
self.labels = "Full"
self.only_player_takeoff = True # Legacy parameter do not use
self.night_disabled = False
self.external_views_allowed = True
self.supercarrier = False
self.multiplier = 1
self.generate_marks = True
self.sams = True # Legacy parameter do not use
self.cold_start = False # Legacy parameter do not use
self.version = None
# Performance oriented
self.perf_red_alert_state = True
self.perf_smoke_gen = True
self.perf_artillery = True
self.perf_moving_units = True
self.perf_infantry = True
self.perf_ai_parking_start = True
self.perf_destroyed_units = True
# Performance culling
self.perf_culling = False
self.perf_culling_distance = 100
# LUA Plugins system
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
# normally would not be present in the unpickled object) by creating a
# new settings object, updating it with the unpickled state, and
# updating our dict with that.
new_state = Settings().__dict__
new_state.update(state)
self.__dict__.update(new_state)

14
game/utils.py Normal file
View File

@@ -0,0 +1,14 @@
def meter_to_feet(value_in_meter: float) -> int:
return int(3.28084 * value_in_meter)
def feet_to_meter(value_in_feet: float) -> int:
return int(value_in_feet / 3.28084)
def meter_to_nm(value_in_meter: float) -> int:
return int(value_in_meter / 1852)
def nm_to_meter(value_in_nm: float) -> int:
return int(value_in_nm * 1852)

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()

183
game/weather.py Normal file
View File

@@ -0,0 +1,183 @@
from __future__ import annotations
import datetime
import logging
import random
from dataclasses import dataclass
from enum import Enum
from typing import Optional
from dcs.weather import Weather as PydcsWeather, Wind
from game.settings import Settings
from theater import ConflictTheater
class TimeOfDay(Enum):
Dawn = "dawn"
Day = "day"
Dusk = "dusk"
Night = "night"
@dataclass(frozen=True)
class WindConditions:
at_0m: Wind
at_2000m: Wind
at_8000m: Wind
@dataclass(frozen=True)
class Clouds:
base: int
density: int
thickness: int
precipitation: PydcsWeather.Preceptions
@dataclass(frozen=True)
class Fog:
visibility: int
thickness: int
class Weather:
def __init__(self) -> None:
self.clouds = self.generate_clouds()
self.fog = self.generate_fog()
self.wind = self.generate_wind()
def generate_clouds(self) -> Optional[Clouds]:
raise NotImplementedError
def generate_fog(self) -> Optional[Fog]:
if random.randrange(5) != 0:
return None
return Fog(
visibility=random.randint(2500, 5000),
thickness=random.randint(100, 500)
)
def generate_wind(self) -> WindConditions:
raise NotImplementedError
@staticmethod
def random_wind(minimum: int, maximum) -> WindConditions:
wind_direction = random.randint(0, 360)
at_0m_factor = 1
at_2000m_factor = 2
at_8000m_factor = 3
base_wind = random.randint(minimum, maximum)
return WindConditions(
# Always some wind to make the smoke move a bit.
at_0m=Wind(wind_direction, max(1, base_wind * at_0m_factor)),
at_2000m=Wind(wind_direction, base_wind * at_2000m_factor),
at_8000m=Wind(wind_direction, base_wind * at_8000m_factor)
)
@staticmethod
def random_cloud_base() -> int:
return random.randint(2000, 3000)
@staticmethod
def random_cloud_thickness() -> int:
return random.randint(100, 400)
class ClearSkies(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return None
def generate_fog(self) -> Optional[Fog]:
return None
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 0)
class Cloudy(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
density=random.randint(1, 8),
thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.None_
)
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 4)
class Raining(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
density=random.randint(5, 8),
thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Rain
)
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 6)
class Thunderstorm(Weather):
def generate_clouds(self) -> Optional[Clouds]:
return Clouds(
base=self.random_cloud_base(),
density=random.randint(9, 10),
thickness=self.random_cloud_thickness(),
precipitation=PydcsWeather.Preceptions.Thunderstorm
)
def generate_wind(self) -> WindConditions:
return self.random_wind(0, 8)
@dataclass
class Conditions:
time_of_day: TimeOfDay
start_time: datetime.datetime
weather: Weather
@classmethod
def generate(cls, theater: ConflictTheater, day: datetime.date,
time_of_day: TimeOfDay, settings: Settings) -> Conditions:
return cls(
time_of_day=time_of_day,
start_time=cls.generate_start_time(
theater, day, time_of_day, settings.night_disabled
),
weather=cls.generate_weather()
)
@classmethod
def generate_start_time(cls, theater: ConflictTheater, day: datetime.date,
time_of_day: TimeOfDay,
night_disabled: bool) -> datetime.datetime:
if night_disabled:
logging.info("Skip Night mission due to user settings")
time_range = {
TimeOfDay.Dawn: (8, 9),
TimeOfDay.Day: (10, 12),
TimeOfDay.Dusk: (12, 14),
TimeOfDay.Night: (14, 17),
}[time_of_day]
else:
time_range = theater.daytime_map[time_of_day.value]
time = datetime.time(hour=random.randint(*time_range))
return datetime.datetime.combine(day, time)
@classmethod
def generate_weather(cls) -> Weather:
chances = {
Thunderstorm: 1,
Raining: 20,
Cloudy: 60,
ClearSkies: 20,
}
weather_type = random.choices(list(chances.keys()),
weights=list(chances.values()))[0]
return weather_type()

View File

@@ -1,12 +1,13 @@
from .aaa import *
from .aircraft import *
from .armor import *
from .awacsgen import *
from .airsupportgen import *
from .conflictgen import *
from .shipgen import *
from .visualgen import *
from .triggergen import *
from .environmentgen import *
from .groundobjectsgen import *
from .briefinggen import *
from .forcedoptionsgen import *
from .kneeboard import *
from . import naming

View File

@@ -1,74 +0,0 @@
from game import *
from theater.conflicttheater import ConflictTheater
from .conflictgen import *
from .naming import *
from dcs.mission import *
DISTANCE_FACTOR = 0.5, 1
EXTRA_AA_MIN_DISTANCE = 35000
EXTRA_AA_POSITION_FROM_CP = 550
class AAConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict):
self.m = mission
self.conflict = conflict
def generate_at_defenders_location(self, units: db.AirDefenseDict):
for unit_type, count in units.items():
for _ in range(count):
self.m.vehicle_group(
country=self.conflict.defenders_side,
name=namegen.next_ground_group_name(),
_type=unit_type,
position=self.conflict.ground_defenders_location.random_point_within(100, 100),
group_size=1)
def generate(self, units: db.AirDefenseDict):
for type, count in units.items():
for _, radial in zip(range(count), self.conflict.radials):
distance = randint(self.conflict.size * DISTANCE_FACTOR[0], self.conflict.size * DISTANCE_FACTOR[1])
p = self.conflict.position.point_from_heading(radial, distance)
self.m.vehicle_group(
country=self.conflict.defenders_side,
name=namegen.next_ground_group_name(),
_type=type,
position=p,
group_size=1)
class ExtraAAConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_name: Country, enemy_name: Country):
self.mission = mission
self.game = game
self.conflict = conflict
self.player_name = player_name
self.enemy_name = enemy_name
def generate(self):
from theater.conflicttheater import ControlPoint
for cp in self.game.theater.controlpoints:
if cp.is_global:
continue
if cp.position.distance_to_point(self.conflict.position) < EXTRA_AA_MIN_DISTANCE:
continue
if cp.position.distance_to_point(self.conflict.from_cp.position) < EXTRA_AA_MIN_DISTANCE:
continue
country_name = cp.captured and self.player_name or self.enemy_name
position = cp.position.point_from_heading(0, EXTRA_AA_POSITION_FROM_CP)
self.mission.vehicle_group(
country=self.mission.country(country_name),
name=namegen.next_ground_group_name(),
_type=db.EXTRA_AA[country_name],
position=position,
group_size=2
)

File diff suppressed because it is too large Load Diff

1505
gen/airfields.py Normal file

File diff suppressed because it is too large Load Diff

145
gen/airsupportgen.py Normal file
View File

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

View File

@@ -1,64 +1,490 @@
from game import db
from .conflictgen import *
from .naming import *
import logging
import random
from dataclasses import dataclass
from typing import List
from dcs.mission import *
from dcs.unittype import *
from dcs.point import *
from dcs.task import *
from dcs.country import *
from dcs import Mission
from dcs.action import AITaskPush
from dcs.condition import GroupLifeLess, Or, TimeAfter, UnitDamaged
from dcs.country import Country
from dcs.mapping import Point
from dcs.planes import MQ_9_Reaper
from dcs.point import PointAction
from dcs.task import (
AttackGroup,
ControlledTask,
EPLRS,
FireAtPoint,
GoToWaypoint,
Hold,
OrbitAction,
SetImmortalCommand,
SetInvisibleCommand,
)
from dcs.triggers import Event, TriggerOnce
from dcs.unit import Vehicle
from dcs.unittype import VehicleType
from game import db
from .naming import namegen
from gen.ground_forces.ai_ground_planner import (
CombatGroupRole,
DISTANCE_FROM_FRONTLINE,
)
from .callsigns import callsign_for_support_unit
from .conflictgen import Conflict
from .ground_forces.combat_stance import CombatStance
from game.plugins import LuaPluginManager
SPREAD_DISTANCE_FACTOR = 0.1, 0.3
SPREAD_DISTANCE_SIZE_FACTOR = 0.1
FRONTLINE_CAS_FIGHTS_COUNT = 16, 24
FRONTLINE_CAS_GROUP_MIN = 1, 2
FRONTLINE_CAS_PADDING = 12000
class ArmorConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict):
self.m = mission
RETREAT_DISTANCE = 20000
BREAKTHROUGH_OFFENSIVE_DISTANCE = 35000
AGGRESIVE_MOVE_DISTANCE = 16000
FIGHT_DISTANCE = 3500
RANDOM_OFFSET_ATTACK = 250
@dataclass(frozen=True)
class JtacInfo:
"""JTAC information."""
dcsGroupName: str
unit_name: str
callsign: str
region: str
code: str
# TODO: Radio info? Type?
class GroundConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game, player_planned_combat_groups, enemy_planned_combat_groups, player_stance):
self.mission = mission
self.conflict = conflict
self.enemy_planned_combat_groups = enemy_planned_combat_groups
self.player_planned_combat_groups = player_planned_combat_groups
self.player_stance = CombatStance(player_stance)
self.enemy_stance = random.choice([CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.AGGRESSIVE, CombatStance.ELIMINATION, CombatStance.BREAKTHROUGH]) if len(enemy_planned_combat_groups) > len(player_planned_combat_groups) else random.choice([CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.DEFENSIVE, CombatStance.AMBUSH, CombatStance.AGGRESSIVE])
self.game = game
self.jtacs: List[JtacInfo] = []
def _group_point(self, point) -> Point:
distance = randint(
distance = random.randint(
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[0]),
int(self.conflict.size * SPREAD_DISTANCE_FACTOR[1]),
)
return point.random_point_within(distance, self.conflict.size * SPREAD_DISTANCE_SIZE_FACTOR)
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point):
def generate(self):
player_groups = []
enemy_groups = []
combat_width = self.conflict.distance/2
if combat_width > 500000:
combat_width = 500000
if combat_width < 35000:
combat_width = 35000
position = Conflict.frontline_position(self.game.theater, self.conflict.from_cp, self.conflict.to_cp)
# Create player groups at random position
for group in self.player_planned_combat_groups:
if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
else:
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
final_position = self.get_valid_position_for_group(position, True, combat_width, distance_from_frontline)
if final_position is not None:
g = self._generate_group(
side=self.mission.country(self.game.player_country),
unit=group.units[0],
heading=self.conflict.heading+90,
count=len(group.units),
at=final_position)
g.set_skill(self.game.settings.player_skill)
player_groups.append((g,group))
self.gen_infantry_group_for_group(g, True, self.mission.country(self.game.player_country), self.conflict.heading + 90)
# Create enemy groups at random position
for group in self.enemy_planned_combat_groups:
if group.role == CombatGroupRole.ARTILLERY:
distance_from_frontline = self.get_artilery_group_distance_from_frontline(group)
else:
distance_from_frontline = DISTANCE_FROM_FRONTLINE[group.role]
final_position = self.get_valid_position_for_group(position, False, combat_width, distance_from_frontline)
if final_position is not None:
g = self._generate_group(
side=self.mission.country(self.game.enemy_country),
unit=group.units[0],
heading=self.conflict.heading - 90,
count=len(group.units),
at=final_position)
g.set_skill(self.game.settings.enemy_vehicle_skill)
enemy_groups.append((g, group))
self.gen_infantry_group_for_group(g, False, self.mission.country(self.game.enemy_country), self.conflict.heading - 90)
# Plan combat actions for groups
self.plan_action_for_groups(self.player_stance, player_groups, enemy_groups, self.conflict.heading + 90, self.conflict.from_cp, self.conflict.to_cp)
self.plan_action_for_groups(self.enemy_stance, enemy_groups, player_groups, self.conflict.heading - 90, self.conflict.to_cp, self.conflict.from_cp)
# Add JTAC
if self.game.player_faction.has_jtac:
n = "JTAC" + str(self.conflict.from_cp.id) + str(self.conflict.to_cp.id)
code = 1688 - len(self.jtacs)
utype = MQ_9_Reaper
if self.game.player_faction.jtac_unit is not None:
utype = self.game.player_faction.jtac_unit
jtac = self.mission.flight_group(country=self.mission.country(self.game.player_country),
name=n,
aircraft_type=utype,
position=position[0],
airport=None,
altitude=5000)
jtac.points[0].tasks.append(SetInvisibleCommand(True))
jtac.points[0].tasks.append(SetImmortalCommand(True))
jtac.points[0].tasks.append(OrbitAction(5000, 300, OrbitAction.OrbitPattern.Circle))
frontline = f"Frontline {self.conflict.from_cp.name}/{self.conflict.to_cp.name}"
# Note: Will need to change if we ever add ground based JTAC.
callsign = callsign_for_support_unit(jtac)
self.jtacs.append(JtacInfo(str(jtac.name), n, callsign, frontline, str(code)))
def gen_infantry_group_for_group(self, group, is_player, side:Country, forward_heading):
# Disable infantry unit gen if disabled
if not self.game.settings.perf_infantry:
return
infantry_position = group.points[0].position.random_point_within(250, 50)
if side == self.conflict.attackers_country:
cp = self.conflict.from_cp
else:
cp = self.conflict.to_cp
if is_player:
faction = self.game.player_name
else:
faction = self.game.enemy_name
possible_infantry_units = db.find_infantry(faction)
if len(possible_infantry_units) == 0:
return
u = random.choice(possible_infantry_units)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp, u), u,
position=infantry_position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad)
for i in range(random.randint(3, 10)):
u = random.choice(possible_infantry_units)
position = infantry_position.random_point_within(55, 5)
self.mission.vehicle_group(
side,
namegen.next_infantry_name(side, cp, u), u,
position=position,
group_size=1,
heading=forward_heading,
move_formation=PointAction.OffRoad)
def plan_action_for_groups(self, stance, ally_groups, enemy_groups, forward_heading, from_cp, to_cp):
if not self.game.settings.perf_moving_units:
return
for dcs_group, group in ally_groups:
if hasattr(group.units[0], 'eplrs'):
if group.units[0].eplrs:
dcs_group.points[0].tasks.append(EPLRS(dcs_group.id))
if group.role == CombatGroupRole.ARTILLERY:
# Fire on any ennemy in range
if self.game.settings.perf_artillery:
target = self.get_artillery_target_in_range(dcs_group, group, enemy_groups)
if target is not None:
if stance != CombatStance.RETREAT:
hold_task = Hold()
hold_task.number = 1
dcs_group.add_trigger_action(hold_task)
# Artillery strike random start
artillery_trigger = TriggerOnce(Event.NoEvent, "ArtilleryFireTask #" + str(dcs_group.id))
artillery_trigger.add_condition(TimeAfter(seconds=random.randint(1, 45)* 60))
fire_task = FireAtPoint(target, len(group.units) * 10, 100)
if stance != CombatStance.RETREAT:
fire_task.number = 2
else:
fire_task.number = 1
dcs_group.add_trigger_action(fire_task)
artillery_trigger.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
self.mission.triggerrules.triggers.append(artillery_trigger)
# Artillery will fall back when under attack
if stance != CombatStance.RETREAT:
# Hold position
dcs_group.points[0].tasks.append(Hold())
retreat = self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE/3))
dcs_group.add_waypoint(dcs_group.position.point_from_heading(forward_heading, 1), PointAction.OffRoad)
dcs_group.points[1].tasks.append(Hold())
dcs_group.add_waypoint(retreat, PointAction.OffRoad)
artillery_fallback = TriggerOnce(Event.NoEvent, "ArtilleryRetreat #" + str(dcs_group.id))
for i, u in enumerate(dcs_group.units):
artillery_fallback.add_condition(UnitDamaged(u.id))
if i < len(dcs_group.units) - 1:
artillery_fallback.add_condition(Or())
hold_2 = Hold()
hold_2.number = 3
dcs_group.add_trigger_action(hold_2)
retreat_task = GoToWaypoint(toIndex=3)
retreat_task.number = 4
dcs_group.add_trigger_action(retreat_task)
artillery_fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
self.mission.triggerrules.triggers.append(artillery_fallback)
for u in dcs_group.units:
u.initial = True
u.heading = forward_heading + random.randint(-5,5)
elif group.role in [CombatGroupRole.TANK, CombatGroupRole.IFV]:
if stance == CombatStance.AGGRESSIVE:
# Attack nearest enemy if any
# Then move forward OR Attack enemy base if it is not too far away
target = self.find_nearest_enemy_group(dcs_group, enemy_groups)
if target is not None:
rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
dcs_group.add_waypoint(target.points[0].position + rand_offset, PointAction.OffRoad)
dcs_group.points[1].tasks.append(AttackGroup(target.id))
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
elif stance == CombatStance.BREAKTHROUGH:
# In breakthrough mode, the units will move forward
# If the enemy base is close enough, the units will attack the base
if to_cp.position.distance_to_point(
dcs_group.points[0].position) <= BREAKTHROUGH_OFFENSIVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, BREAKTHROUGH_OFFENSIVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
elif stance == CombatStance.ELIMINATION:
# In elimination mode, the units focus on destroying as much enemy groups as possible
targets = self.find_n_nearest_enemy_groups(dcs_group, enemy_groups, 3)
i = 1
for target in targets:
rand_offset = Point(random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK), random.randint(-RANDOM_OFFSET_ATTACK, RANDOM_OFFSET_ATTACK))
dcs_group.add_waypoint(target.points[0].position+rand_offset, PointAction.OffRoad)
dcs_group.points[i].tasks.append(AttackGroup(target.id))
i = i + 1
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
dcs_group.add_waypoint(attack_point)
if stance != CombatStance.RETREAT:
self.add_morale_trigger(dcs_group, forward_heading)
elif group.role in [CombatGroupRole.APC, CombatGroupRole.ATGM]:
if stance in [CombatStance.AGGRESSIVE, CombatStance.BREAKTHROUGH, CombatStance.ELIMINATION]:
# APC & ATGM will never move too much forward, but will follow along any offensive
if to_cp.position.distance_to_point(dcs_group.points[0].position) <= AGGRESIVE_MOVE_DISTANCE:
attack_point = to_cp.position.random_point_within(500, 0)
else:
attack_point = self.find_offensive_point(dcs_group, forward_heading, AGGRESIVE_MOVE_DISTANCE)
dcs_group.add_waypoint(attack_point, PointAction.OnRoad)
if stance != CombatStance.RETREAT:
self.add_morale_trigger(dcs_group, forward_heading)
if stance == CombatStance.RETREAT:
# In retreat mode, the units will fall back
# If the ally base is close enough, the units will even regroup there
if from_cp.position.distance_to_point(dcs_group.points[0].position) <= RETREAT_DISTANCE:
retreat_point = from_cp.position.random_point_within(500, 250)
else:
retreat_point = self.find_retreat_point(dcs_group, forward_heading)
reposition_point = retreat_point.point_from_heading(forward_heading, 10) # Another point to make the unit face the enemy
dcs_group.add_waypoint(retreat_point, PointAction.OnRoad)
dcs_group.add_waypoint(reposition_point, PointAction.OffRoad)
def add_morale_trigger(self, dcs_group, forward_heading):
"""
This add a trigger to manage units fleeing whenever their group is hit hard, or being engaged by CAS
"""
if len(dcs_group.units) == 1:
return
# Units should hold position on last waypoint
dcs_group.points[len(dcs_group.points) - 1].tasks.append(Hold())
# Force unit heading
for unit in dcs_group.units:
unit.heading = forward_heading
dcs_group.manualHeading = True
# We add a new retreat waypoint
dcs_group.add_waypoint(self.find_retreat_point(dcs_group, forward_heading, (int)(RETREAT_DISTANCE / 8)), PointAction.OffRoad)
# Fallback task
fallback = ControlledTask(GoToWaypoint(toIndex=len(dcs_group.points)))
fallback.enabled = False
dcs_group.add_trigger_action(Hold())
dcs_group.add_trigger_action(fallback)
# Create trigger
fallback = TriggerOnce(Event.NoEvent, "Morale manager #" + str(dcs_group.id))
# Usually more than 50% casualties = RETREAT
fallback.add_condition(GroupLifeLess(dcs_group.id, random.randint(51, 76)))
# Do retreat to the configured retreat waypoint
fallback.add_action(AITaskPush(dcs_group.id, len(dcs_group.tasks)))
self.mission.triggerrules.triggers.append(fallback)
def find_retreat_point(self, dcs_group, frontline_heading, distance=RETREAT_DISTANCE):
"""
Find a point to retreat to
:param dcs_group: DCS mission group we are searching a retreat point for
:param frontline_heading: Heading of the frontline
:return: dcs.mapping.Point object with the desired position
"""
return dcs_group.points[0].position.point_from_heading(frontline_heading-180, distance)
def find_offensive_point(self, dcs_group, frontline_heading, distance):
"""
Find a point to attack
:param dcs_group: DCS mission group we are searching an attack point for
:param frontline_heading: Heading of the frontline
:param distance: Distance of the offensive (how far unit should move)
:return: dcs.mapping.Point object with the desired position
"""
return dcs_group.points[0].position.point_from_heading(frontline_heading, distance)
def find_n_nearest_enemy_groups(self, player_group, enemy_groups, n):
"""
Return the neaarest enemy group for the player group
@param group Group for which we should find the nearest ennemies
@param enemy_groups Potential enemy groups
@param n number of nearby groups to take
"""
targets = []
sorted_list = sorted(enemy_groups, key=lambda group: player_group.points[0].position.distance_to_point(group[0].points[0].position))
for i in range(n):
if len(sorted_list) <= i:
break
else:
targets.append(sorted_list[i][0])
return targets
def find_nearest_enemy_group(self, player_group, enemy_groups):
"""
Search the enemy groups for a potential target suitable to armored assault
@param group Group for which we should find the nearest ennemy
@param enemy_groups Potential enemy groups
"""
min_distance = 99999999
target = None
for dcs_group, group in enemy_groups:
dist = player_group.points[0].position.distance_to_point(dcs_group.points[0].position)
if dist < min_distance:
min_distance = dist
target = dcs_group
return target
def get_artillery_target_in_range(self, dcs_group, group, enemy_groups):
"""
Search the enemy groups for a potential target suitable to an artillery unit
"""
rng = group.units[0].threat_range
if len(enemy_groups) == 0:
return None
for o in range(10):
potential_target = random.choice(enemy_groups)[0]
distance_to_target = dcs_group.points[0].position.distance_to_point(potential_target.points[0].position)
if distance_to_target < rng:
return potential_target.points[0].position
return None
def get_artilery_group_distance_from_frontline(self, group):
"""
For artilery group, decide the distance from frontline with the range of the unit
"""
rg = group.units[0].threat_range - 7500
if rg > DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]:
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.ARTILLERY]
if rg < DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK]:
rg = DISTANCE_FROM_FRONTLINE[CombatGroupRole.TANK] + 100
return rg
def get_valid_position_for_group(self, conflict_position, isplayer, combat_width, distance_from_frontline):
i = 0
while i < 25: # 25 attempt for valid position
heading_diff = -90 if isplayer else 90
shifted = conflict_position[0].point_from_heading(self.conflict.heading,
random.randint((int)(-combat_width / 2), (int)(combat_width / 2)))
final_position = shifted.point_from_heading(self.conflict.heading + heading_diff, distance_from_frontline)
if self.conflict.theater.is_on_land(final_position):
return final_position
else:
i = i + 1
continue
return None
def _generate_group(self, side: Country, unit: VehicleType, count: int, at: Point, move_formation: PointAction = PointAction.OffRoad, heading=0):
if side == self.conflict.attackers_country:
cp = self.conflict.from_cp
else:
cp = self.conflict.to_cp
logging.info("armorgen: {} for {}".format(unit, side.id))
group = self.mission.vehicle_group(
side,
namegen.next_unit_name(side, cp.id, unit), unit,
position=self._group_point(at),
group_size=count,
heading=heading,
move_formation=move_formation)
for c in range(count):
group = self.m.vehicle_group(
side,
namegen.next_armor_group_name(),
unit,
position=self._group_point(at),
group_size=1,
move_formation=PointAction.OffRoad)
wayp = group.add_waypoint(self.conflict.position.point_from_heading(0, 500))
wayp.tasks = []
vehicle: Vehicle = group.units[c]
vehicle.player_can_drive = True
def generate(self, attackers: db.ArmorDict, defenders: db.ArmorDict):
for type, count in attackers.items():
self._generate_group(
side=self.conflict.attackers_side,
unit=type,
count=count,
at=self.conflict.ground_attackers_location)
for type, count in defenders.items():
self._generate_group(
side=self.conflict.defenders_side,
unit=type,
count=count,
at=self.conflict.ground_defenders_location)
def generate_passengers(self, count: int):
unit_type = random.choice(db.find_unittype(Nothing, self.conflict.attackers_side.name))
self.m.vehicle_group(
country=self.conflict.attackers_side,
name=namegen.next_passenger_group_name(),
_type=unit_type,
position=self.conflict.ground_attackers_location,
group_size=count
)
return group

205
gen/ato.py Normal file
View File

@@ -0,0 +1,205 @@
"""Air Tasking Orders.
The classes of the Air Tasking Order (ATO) define all of the missions that have
been planned, and which aircraft have been assigned to them. Each planned
mission, or "package" is composed of individual flights. The package may contain
dissimilar aircraft performing different roles, but all for the same goal. For
example, the package to strike an enemy airfield may contain an escort flight,
a SEAD flight, and the strike aircraft themselves. CAP packages may contain only
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)
class Task:
"""The main task of a flight or package."""
#: The type of task.
task_type: FlightType
#: The location of the objective.
location: str
@dataclass(frozen=True)
class PackageWaypoints:
join: Point
ingress: Point
egress: Point
split: Point
@dataclass
class Package:
"""A mission package."""
#: The mission target. Currently can be either a ControlPoint or a
#: TheaterGroundObject (non-ControlPoint map objectives).
target: MissionTarget
#: The set of flights in the package.
flights: List[Flight] = field(default_factory=list)
delay: 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)
def remove_flight(self, flight: Flight) -> None:
"""Removes a flight from the package."""
self.flights.remove(flight)
if not self.flights:
self.waypoints = None
@property
def primary_task(self) -> Optional[FlightType]:
if not self.flights:
return None
flight_counts: Dict[FlightType, int] = defaultdict(lambda: 0)
for flight in self.flights:
flight_counts[flight.flight_type] += 1
# The package will contain a mix of mission types, but in general we can
# determine the goal of the mission because some mission types are more
# likely to be the main task than others. For example, a package with
# only CAP flights is a CAP package, a flight with CAP and strike is a
# strike package, a flight with CAP and DEAD is a DEAD package, and a
# flight with strike and SEAD is an OCA/Strike package. The type of
# package is determined by the highest priority flight in the package.
task_priorities = [
FlightType.CAS,
FlightType.STRIKE,
FlightType.ANTISHIP,
FlightType.BAI,
FlightType.EVAC,
FlightType.TROOP_TRANSPORT,
FlightType.RECON,
FlightType.ELINT,
FlightType.DEAD,
FlightType.SEAD,
FlightType.LOGISTICS,
FlightType.INTERCEPTION,
FlightType.TARCAP,
FlightType.CAP,
FlightType.BARCAP,
FlightType.EWAR,
FlightType.ESCORT,
]
for task in task_priorities:
if flight_counts[task]:
return task
# If we get here, our task_priorities list above is incomplete. Log the
# issue and return the type of *any* flight in the package.
some_mission = next(iter(self.flights)).flight_type
logging.warning(f"Unhandled mission type: {some_mission}")
return some_mission
@property
def package_description(self) -> str:
"""Generates a package description based on flight composition."""
task = self.primary_task
if task is None:
return "No mission"
return task.name
def __hash__(self) -> int:
# TODO: Far from perfect. Number packages?
return hash(self.target.name)
@dataclass
class AirTaskingOrder:
"""The entire ATO for one coalition."""
#: The set of all planned packages in the ATO.
packages: List[Package] = field(default_factory=list)
def add_package(self, package: Package) -> None:
"""Adds a package to the ATO."""
self.packages.append(package)
def remove_package(self, package: Package) -> None:
"""Removes a package from the ATO."""
self.packages.remove(package)
def clear(self) -> None:
"""Removes all packages from the ATO."""
self.packages.clear()

View File

@@ -1,30 +0,0 @@
from game import db
from .conflictgen import *
from .naming import *
from dcs.mission import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.task import *
from dcs.terrain.terrain import NoParkingSlotError
AWACS_DISTANCE = 150000
AWACS_ALT = 10000
class AWACSConflictGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game):
self.mission = mission
self.conflict = conflict
self.game = game
def generate(self):
plane = db.find_unittype(AWACS, self.conflict.attackers_side.name)[0]
self.mission.awacs_flight(
country=self.mission.country(self.game.player),
name=namegen.next_awacs_group_name(),
plane_type=plane,
altitude=AWACS_ALT,
airport=None,
position=self.conflict.position.random_point_within(AWACS_DISTANCE, AWACS_DISTANCE))

74
gen/beacons.py Normal file
View File

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

164
gen/briefinggen.py Normal file
View File

@@ -0,0 +1,164 @@
"""
Briefing generation logic
"""
from __future__ import annotations
import os
import random
import logging
from dataclasses import dataclass
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 .aircraft import FlightData
from .airsupportgen import AwacsInfo, TankerInfo
from .armor import JtacInfo
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:
"""Communications information for the kneeboard."""
name: str
freq: RadioFrequency
class FrontLineInfo:
def __init__(self, front_line: FrontLine):
self.front_line: FrontLine = front_line
self.player_base: ControlPoint = front_line.control_point_a
self.enemy_base: ControlPoint = front_line.control_point_b
self.player_zero: bool = self.player_base.base.total_armor == 0
self.enemy_zero: bool = self.enemy_base.base.total_armor == 0
self.advantage: bool = self.player_base.base.total_armor > self.enemy_base.base.total_armor
self.stance: CombatStance = self.player_base.stances[self.enemy_base.id]
self.combat_stances = CombatStance
class MissionInfoGenerator:
"""Base type for generators of mission information for the player.
Examples of subtypes include briefing generators, kneeboard generators, etc.
"""
def __init__(self, mission: Mission, game: Game) -> None:
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.
Args:
awacs: AWACS information.
"""
self.awacs.append(awacs)
def add_comm(self, name: str, freq: RadioFrequency) -> None:
"""Adds communications info to the mission.
Args:
name: Name of the radio channel.
freq: Frequency of the radio channel.
"""
self.comms.append(CommInfo(name, freq))
def add_flight(self, flight: FlightData) -> None:
"""Adds flight info to the mission.
Args:
flight: Flight information.
"""
self.flights.append(flight)
def add_jtac(self, jtac: JtacInfo) -> None:
"""Adds a JTAC to the mission.
Args:
jtac: JTAC information.
"""
self.jtacs.append(jtac)
def add_tanker(self, tanker: TankerInfo) -> None:
"""Adds a tanker to the mission.
Args:
tanker: Tanker information.
"""
self.tankers.append(tanker)
def add_frontline(self, frontline: FrontLineInfo) -> None:
"""Adds a frontline to the briefing
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.
Dynamic runways are any valid landing point that is a unit rather than a
map feature. These include carriers, ships with a helipad, and FARPs.
"""
self.dynamic_runways.append(runway)
def generate(self) -> None:
"""Generates the mission information."""
raise NotImplementedError
class BriefingGenerator(MissionInfoGenerator):
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_frontline_info(self) -> None:
"""Build FrontLineInfo objects from FrontLine type and append to briefing.
"""
for front_line in self.game.theater.conflicts(from_player=True):
self.add_frontline(FrontLineInfo(front_line))
# 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]

34
gen/callsigns.py Normal file
View File

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

View File

@@ -1,27 +1,28 @@
import typing
import pdb
import dcs
import logging
import random
from typing import Tuple
from random import randint
from dcs import Mission
from dcs.country import Country
from dcs.mapping import Point
from dcs.mission import *
from dcs.vehicles import *
from dcs.unitgroup import *
from dcs.unittype import *
from dcs.mapping import *
from dcs.point import *
from dcs.task import *
from dcs.country import *
from theater import *
from theater import ConflictTheater, ControlPoint
AIR_DISTANCE = 40000
CAPTURE_AIR_ATTACKERS_DISTANCE = 25000
CAPTURE_AIR_DEFENDERS_DISTANCE = 60000
STRIKE_AIR_ATTACKERS_DISTANCE = 45000
STRIKE_AIR_DEFENDERS_DISTANCE = 25000
GROUND_DISTANCE_FACTOR = 1
GROUNDINTERCEPT_DISTANCE_FACTOR = 6
AIR_DISTANCE = 32000
CAP_CAS_DISTANCE = 10000, 120000
GROUND_INTERCEPT_SPREAD = 5000
GROUND_DISTANCE_FACTOR = 1.4
GROUND_DISTANCE = 2000
GROUND_ATTACK_DISTANCE = 25000, 13000
TRANSPORT_FRONTLINE_DIST = 1800
INTERCEPT_ATTACKERS_HEADING = -45, 45
INTERCEPT_DEFENDERS_HEADING = -10, 10
@@ -30,10 +31,14 @@ INTERCEPT_ATTACKERS_DISTANCE = 100000
INTERCEPT_MAX_DISTANCE = 160000
INTERCEPT_MIN_DISTANCE = 100000
NAVAL_INTERCEPT_DISTANCE_FACTOR = 0.4
NAVAL_INTERCEPT_DISTANCE_FACTOR = 1
NAVAL_INTERCEPT_DISTANCE_MAX = 40000
NAVAL_INTERCEPT_STEP = 5000
FRONTLINE_LENGTH = 80000
FRONTLINE_MIN_CP_DISTANCE = 5000
FRONTLINE_DISTANCE_STRENGTH_FACTOR = 0.7
def _opposite_heading(h):
return h+180
@@ -50,36 +55,33 @@ def _heading_sum(h, a) -> int:
class Conflict:
attackers_side = None # type: Country
defenders_side = None # type: Country
from_cp = None # type: ControlPoint
to_cp = None # type: ControlPoint
position = None # type: Point
size = None # type: int
radials = None # type: typing.List[int]
ground_attackers_location = None # type: Point
ground_defenders_location = None # type: Point
air_attackers_location = None # type: Point
air_defenders_location = None # type: Point
def __init__(self,
position: Point,
theater: ConflictTheater,
from_cp: ControlPoint,
to_cp: ControlPoint,
attackers_side: Country,
defenders_side: Country,
ground_attackers_location: Point,
ground_defenders_location: Point,
air_attackers_location: Point,
air_defenders_location: Point):
attackers_side: str,
defenders_side: str,
attackers_country: Country,
defenders_country: Country,
position: Point,
heading=None,
distance=None,
ground_attackers_location: Point = None,
ground_defenders_location: Point = None,
air_attackers_location: Point = None,
air_defenders_location: Point = None):
self.attackers_side = attackers_side
self.defenders_side = defenders_side
self.attackers_country = attackers_country
self.defenders_country = defenders_country
self.from_cp = from_cp
self.to_cp = to_cp
self.theater = theater
self.position = position
self.heading = heading
self.distance = distance
self.size = to_cp.size
self.radials = to_cp.radials
self.ground_attackers_location = ground_attackers_location
@@ -87,41 +89,171 @@ class Conflict:
self.air_attackers_location = air_attackers_location
self.air_defenders_location = air_defenders_location
@property
def center(self) -> Point:
return self.position.point_from_heading(self.heading, self.distance / 2)
@property
def tail(self) -> Point:
return self.position.point_from_heading(self.heading, self.distance)
@property
def is_vector(self) -> bool:
return self.heading is not None
@property
def opposite_heading(self) -> int:
return _heading_sum(self.heading, 180)
@property
def to_size(self):
return self.to_cp.size * GROUND_DISTANCE_FACTOR
def find_insertion_point(self, other_point: Point) -> Point:
if self.is_vector:
dx = self.position.x - self.tail.x
dy = self.position.y - self.tail.y
dr2 = float(dx ** 2 + dy ** 2)
lerp = ((other_point.x - self.tail.x) * dx + (other_point.y - self.tail.y) * dy) / dr2
if lerp < 0:
lerp = 0
elif lerp > 1:
lerp = 1
x = lerp * dx + self.tail.x
y = lerp * dy + self.tail.y
return Point(x, y)
else:
return self.position
def find_ground_position(self, at: Point, heading: int, max_distance: int = 40000) -> Point:
return Conflict._find_ground_position(at, max_distance, heading, self.theater)
@classmethod
def _find_ground_location(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
for _ in range(0, int(max_distance), 800):
for _ in range(3):
if theater.is_on_land(initial):
return initial
def has_frontline_between(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> bool:
return from_cp.has_frontline and to_cp.has_frontline
initial = initial.random_point_within(1000, 1000)
@classmethod
def frontline_position(cls, theater: ConflictTheater, from_cp: ControlPoint, to_cp: ControlPoint) -> Tuple[Point, int]:
attack_heading = from_cp.position.heading_between_point(to_cp.position)
attack_distance = from_cp.position.distance_to_point(to_cp.position)
middle_point = from_cp.position.point_from_heading(attack_heading, attack_distance / 2)
initial = initial.point_from_heading(heading, 800)
strength_delta = (from_cp.base.strength - to_cp.base.strength) / 1.0
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 frontline_vector(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater) -> Tuple[Point, int, int]:
"""
probe_end_point = initial.point_from_heading(heading, FRONTLINE_LENGTH)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
intersection = probe.intersection(theater.land_poly)
if isinstance(intersection, geometry.LineString):
intersection = intersection
elif isinstance(intersection, geometry.MultiLineString):
intersection = intersection.geoms[0]
else:
print(intersection)
return None
return Point(*intersection.xy[0]), _heading_sum(heading, 90), intersection.length
"""
frontline = cls.frontline_position(theater, from_cp, to_cp)
center_position, heading = frontline
left_position, right_position = None, None
if not theater.is_on_land(center_position):
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, -90), theater)
if pos:
right_position = pos
center_position = pos
else:
pos = cls._find_ground_position(center_position, FRONTLINE_LENGTH, _heading_sum(heading, +90), theater)
if pos:
left_position = pos
center_position = pos
if left_position is None:
left_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, -90), theater)
if right_position is None:
right_position = cls._extend_ground_position(center_position, int(FRONTLINE_LENGTH/2), _heading_sum(heading, 90), theater)
return left_position, _heading_sum(heading, 90), int(right_position.distance_to_point(left_position))
@classmethod
def _extend_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
pos = initial
for offset in range(0, int(max_distance), 500):
new_pos = initial.point_from_heading(heading, offset)
if theater.is_on_land(new_pos):
pos = new_pos
else:
return pos
return pos
"""
probe_end_point = initial.point_from_heading(heading, max_distance)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y)])
intersection = probe.intersection(theater.land_poly)
if intersection is geometry.LineString:
return Point(*intersection.xy[1])
elif intersection is geometry.MultiLineString:
return Point(*intersection.geoms[0].xy[1])
print("Didn't find ground position!")
return None
"""
@classmethod
def capture_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
def _find_ground_position(cls, initial: Point, max_distance: int, heading: int, theater: ConflictTheater) -> Point:
pos = initial
for _ in range(0, int(max_distance), 500):
if theater.is_on_land(pos):
return pos
pos = pos.point_from_heading(heading, 500)
"""
probe_end_point = initial.point_from_heading(heading, max_distance)
probe = geometry.LineString([(initial.x, initial.y), (probe_end_point.x, probe_end_point.y) ])
intersection = probe.intersection(theater.land_poly)
if isinstance(intersection, geometry.LineString):
return Point(*intersection.xy[1])
elif isinstance(intersection, geometry.MultiLineString):
return Point(*intersection.geoms[0].xy[1])
"""
logging.error("Didn't find ground position ({})!".format(initial))
return initial
@classmethod
def capture_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
position = to_cp.position
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
attack_heading = to_cp.find_radial(attack_raw_heading)
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
distance = to_cp.size * GROUND_DISTANCE_FACTOR
distance = GROUND_DISTANCE
attackers_location = position.point_from_heading(attack_heading, distance)
attackers_location = Conflict._find_ground_location(attackers_location, distance * 2, _heading_sum(attack_heading, 180), theater)
attackers_location = Conflict._find_ground_position(attackers_location, distance * 2, attack_heading, theater)
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_location(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
defenders_location = position.point_from_heading(defense_heading, 0)
defenders_location = Conflict._find_ground_position(defenders_location, distance * 2, defense_heading, theater)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=attackers_location,
ground_defenders_location=defenders_location,
air_attackers_location=position.point_from_heading(attack_raw_heading, CAPTURE_AIR_ATTACKERS_DISTANCE),
@@ -129,20 +261,57 @@ class Conflict:
)
@classmethod
def intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
def strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
position = to_cp.position
attack_raw_heading = to_cp.position.heading_between_point(from_cp.position)
attack_heading = to_cp.find_radial(attack_raw_heading)
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
distance = to_cp.size * GROUND_DISTANCE_FACTOR
attackers_location = position.point_from_heading(attack_heading, distance)
attackers_location = Conflict._find_ground_position(
attackers_location, int(distance * 2),
_heading_sum(attack_heading, 180), theater)
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_position(
defenders_location, int(distance * 2),
_heading_sum(defense_heading, 180), theater)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=attackers_location,
ground_defenders_location=defenders_location,
air_attackers_location=position.point_from_heading(attack_raw_heading, STRIKE_AIR_ATTACKERS_DISTANCE),
air_defenders_location=position.point_from_heading(_opposite_heading(attack_raw_heading), STRIKE_AIR_DEFENDERS_DISTANCE)
)
@classmethod
def intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint) -> Point:
raw_distance = from_cp.position.distance_to_point(to_cp.position) * 1.5
distance = max(min(raw_distance, INTERCEPT_MAX_DISTANCE), INTERCEPT_MIN_DISTANCE)
heading = _heading_sum(from_cp.position.heading_between_point(to_cp.position), random.choice([-1, 1]) * random.randint(60, 100))
position = from_cp.position.point_from_heading(heading, distance)
return from_cp.position.point_from_heading(heading, distance)
@classmethod
def intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
heading = from_cp.position.heading_between_point(position)
return cls(
position=position.point_from_heading(position.heading_between_point(to_cp.position), INTERCEPT_CONFLICT_DISTANCE),
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=None,
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, INTERCEPT_ATTACKERS_DISTANCE),
@@ -150,10 +319,10 @@ class Conflict:
)
@classmethod
def ground_attack_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
def ground_attack_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
heading = random.choice(to_cp.radials)
initial_location = to_cp.position.random_point_within(*GROUND_ATTACK_DISTANCE)
position = Conflict._find_ground_location(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
position = Conflict._find_ground_position(initial_location, GROUND_INTERCEPT_SPREAD, _heading_sum(heading, 180), theater)
if not position:
heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
position = to_cp.position.point_from_heading(heading, to_cp.size * GROUND_DISTANCE_FACTOR)
@@ -163,8 +332,10 @@ class Conflict:
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=position,
ground_defenders_location=None,
air_attackers_location=None,
@@ -172,42 +343,103 @@ class Conflict:
)
@classmethod
def ground_intercept_conflict(cls, attacker: Country, defender: Country, heading: int, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
heading = random.choice(to_cp.radials)
initial_location = to_cp.position.point_from_heading(heading, to_cp.size * GROUNDINTERCEPT_DISTANCE_FACTOR)
max_distance = to_cp.size * GROUNDINTERCEPT_DISTANCE_FACTOR
ground_location = Conflict._find_ground_location(initial_location, max_distance, _heading_sum(heading, 180), theater)
def convoy_strike_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
frontline_position, frontline_heading, frontline_length = Conflict.frontline_vector(from_cp, to_cp, theater)
if not frontline_position:
assert False
heading = frontline_heading
starting_position = Conflict._find_ground_position(frontline_position.point_from_heading(heading, 7000),
GROUND_INTERCEPT_SPREAD,
_opposite_heading(heading), theater)
if not starting_position:
starting_position = frontline_position
destination_position = frontline_position
else:
destination_position = frontline_position
return cls(
position=position,
position=destination_position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=position,
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE)
ground_defenders_location=starting_position,
air_attackers_location=starting_position.point_from_heading(_opposite_heading(heading), AIR_DISTANCE),
air_defenders_location=starting_position.point_from_heading(heading, AIR_DISTANCE),
)
@classmethod
def ground_base_attack(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
def frontline_cas_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
return cls(
position=position,
heading=heading,
distance=distance,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=None,
air_attackers_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + heading, AIR_DISTANCE),
air_defenders_location=position.point_from_heading(random.randint(*INTERCEPT_ATTACKERS_HEADING) + _opposite_heading(heading), AIR_DISTANCE),
)
@classmethod
def frontline_cap_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
assert cls.has_frontline_between(from_cp, to_cp)
position, heading, distance = cls.frontline_vector(from_cp, to_cp, theater)
attack_position = position.point_from_heading(heading, random.randint(0, int(distance)))
attackers_position = attack_position.point_from_heading(heading - 90, AIR_DISTANCE)
defenders_position = attack_position.point_from_heading(heading + 90, random.randint(*CAP_CAS_DISTANCE))
return cls(
position=position,
heading=heading,
distance=distance,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
air_attackers_location=attackers_position,
air_defenders_location=defenders_position,
)
@classmethod
def ground_base_attack(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
position = to_cp.position
attack_heading = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
defense_heading = to_cp.find_radial(from_cp.position.heading_between_point(to_cp.position), ignored_radial=attack_heading)
distance = to_cp.size * GROUND_DISTANCE_FACTOR
defenders_location = position.point_from_heading(defense_heading, distance)
defenders_location = Conflict._find_ground_location(defenders_location, distance * 2, _heading_sum(defense_heading, 180), theater)
defenders_location = Conflict._find_ground_position(
defenders_location, int(distance * 2),
_heading_sum(defense_heading, 180), theater)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=defenders_location,
air_attackers_location=position.point_from_heading(attack_heading, AIR_DISTANCE),
@@ -215,25 +447,30 @@ class Conflict:
)
@classmethod
def naval_intercept_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
def naval_intercept_position(cls, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
radial = random.choice(to_cp.sea_radials)
initial_distance = min(int(from_cp.position.distance_to_point(to_cp.position) * NAVAL_INTERCEPT_DISTANCE_FACTOR), NAVAL_INTERCEPT_DISTANCE_MAX)
position = to_cp.position.point_from_heading(radial, initial_distance)
initial_position = to_cp.position.point_from_heading(radial, initial_distance)
for offset in range(0, initial_distance, NAVAL_INTERCEPT_STEP):
position = to_cp.position.point_from_heading(_opposite_heading(radial), initial_distance - offset)
position = initial_position.point_from_heading(_opposite_heading(radial), offset)
if not theater.is_on_land(position):
break
return position
@classmethod
def naval_intercept_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, position: Point, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
attacker_heading = from_cp.position.heading_between_point(to_cp.position)
return cls(
position=position,
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=None,
ground_defenders_location=position,
air_attackers_location=position.point_from_heading(attacker_heading, AIR_DISTANCE),
@@ -241,11 +478,10 @@ class Conflict:
)
@classmethod
def transport_conflict(cls, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
frontline_position = cls._frontline_position(from_cp, to_cp)
heading = to_cp.position.heading_between_point(from_cp.position)
def transport_conflict(cls, attacker_name: str, defender_name: str, attacker: Country, defender: Country, from_cp: ControlPoint, to_cp: ControlPoint, theater: ConflictTheater):
frontline_position, heading = cls.frontline_position(theater, from_cp, to_cp)
initial_dest = frontline_position.point_from_heading(heading, TRANSPORT_FRONTLINE_DIST)
dest = cls._find_ground_location(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
dest = cls._find_ground_position(initial_dest, from_cp.position.distance_to_point(to_cp.position) / 3, heading, theater)
if not dest:
radial = to_cp.find_radial(to_cp.position.heading_between_point(from_cp.position))
dest = to_cp.position.point_from_heading(radial, to_cp.size * GROUND_DISTANCE_FACTOR)
@@ -255,8 +491,10 @@ class Conflict:
theater=theater,
from_cp=from_cp,
to_cp=to_cp,
attackers_side=attacker,
defenders_side=defender,
attackers_side=attacker_name,
defenders_side=defender_name,
attackers_country=attacker,
defenders_country=defender,
ground_attackers_location=from_cp.position,
ground_defenders_location=frontline_position,
air_attackers_location=from_cp.position.point_from_heading(0, 100),

View File

@@ -0,0 +1,39 @@
import random
from dcs.vehicles import Armor
from game import db
from gen.defenses.armored_group_generator import ArmoredGroupGenerator, FixedSizeArmorGroupGenerator
def generate_armor_group(faction:str, game, ground_object):
"""
This generate a group of ground units
:return: Generated group
"""
possible_unit = [u for u in db.FACTIONS[faction].frontline_units if u in Armor.__dict__.values()]
if len(possible_unit) > 0:
unit_type = random.choice(possible_unit)
return generate_armor_group_of_type(game, ground_object, unit_type)
return None
def generate_armor_group_of_type(game, ground_object, unit_type):
"""
This generate a group of ground units of given type
:return: Generated group
"""
generator = ArmoredGroupGenerator(game, ground_object, unit_type)
generator.generate()
return generator.get_generated_group()
def generate_armor_group_of_type_and_size(game, ground_object, unit_type, size: int):
"""
This generate a group of ground units of given type and size
:return: Generated group
"""
generator = FixedSizeArmorGroupGenerator(game, ground_object, unit_type, size)
generator.generate()
return generator.get_generated_group()

View File

@@ -0,0 +1,44 @@
import random
from gen.sam.group_generator import GroupGenerator
class ArmoredGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type):
super(ArmoredGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type
def generate(self):
grid_x = random.randint(2, 3)
grid_y = random.randint(1, 2)
spacing = random.randint(30, 80)
index = 0
for i in range(grid_x):
for j in range(grid_y):
index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y + spacing * j, self.heading)
class FixedSizeArmorGroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, unit_type, size):
super(FixedSizeArmorGroupGenerator, self).__init__(game, ground_object)
self.unit_type = unit_type
self.size = size
def generate(self):
spacing = random.randint(20, 70)
index = 0
for i in range(self.size):
index = index + 1
self.add_unit(self.unit_type, "Armor#" + str(index),
self.position.x + spacing * i,
self.position.y, self.heading)

View File

@@ -1,107 +1,36 @@
import typing
import random
from datetime import datetime, timedelta, time
from typing import Optional
from dcs.mission import Mission
from dcs.triggers import *
from dcs.condition import *
from dcs.action import *
from dcs.unit import Skill
from dcs.point import MovingPoint, PointProperties
from dcs.action import *
from dcs.weather import *
from game import db
from theater import *
from gen import *
WEATHER_CLOUD_BASE = 2000, 3000
WEATHER_CLOUD_DENSITY = 1, 8
WEATHER_CLOUD_THICKNESS = 100, 400
WEATHER_CLOUD_BASE_MIN = 1200
RANDOM_TIME = {
"night": 5,
"dusk": 30,
"dawn": 30,
"day": 100,
}
RANDOM_WEATHER = {
1: 5, # heavy rain
2: 15, # rain
3: 25, # dynamic
4: 35, # clear
5: 100, # random
}
from game.weather import Clouds, Fog, Conditions, WindConditions
class EnvironmentSettings:
weather_dict = None
start_time = None
class EnviromentGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game):
class EnvironmentGenerator:
def __init__(self, mission: Mission, conditions: Conditions) -> None:
self.mission = mission
self.conflict = conflict
self.game = game
self.conditions = conditions
def _gen_random_time(self):
start_time = datetime.combine(datetime.today(), time())
time_range = None
for k, v in RANDOM_TIME.items():
if self.game.settings.night_disabled and k == "night":
continue
def set_clouds(self, clouds: Optional[Clouds]) -> None:
if clouds is None:
return
self.mission.weather.clouds_base = clouds.base
self.mission.weather.clouds_thickness = clouds.thickness
self.mission.weather.clouds_density = clouds.density
self.mission.weather.clouds_iprecptns = clouds.precipitation
if random.randint(0, 100) <= v:
time_range = self.game.theater.daytime_map[k]
break
def set_fog(self, fog: Optional[Fog]) -> None:
if fog is None:
return
self.mission.weather.fog_visibility = fog.visibility
self.mission.weather.fog_thickness = fog.thickness
start_time += timedelta(hours=random.randint(*time_range))
self.mission.start_time = start_time
def _gen_random_weather(self):
weather_type = None
for k, v in RANDOM_WEATHER.items():
if random.randint(0, 100) <= v:
weather_type = k
break
print("generated weather {}".format(weather_type))
if weather_type == 1:
self.mission.weather.heavy_rain()
elif weather_type == 2:
self.mission.weather.heavy_rain()
self.mission.weather.enable_fog = False
elif weather_type == 3:
self.mission.weather.random(self.mission.start_time, self.conflict.theater.terrain)
elif weather_type == 4:
pass
elif weather_type == 5:
self.mission.weather.clouds_base = random.randint(*WEATHER_CLOUD_BASE)
self.mission.weather.clouds_density = random.randint(*WEATHER_CLOUD_DENSITY)
self.mission.weather.clouds_thickness = random.randint(*WEATHER_CLOUD_THICKNESS)
wind_direction = random.randint(0, 360)
wind_speed = random.randint(0, 13)
self.mission.weather.wind_at_ground = Wind(wind_direction, wind_speed)
self.mission.weather.wind_at_2000 = Wind(wind_direction, wind_speed * 2)
self.mission.weather.wind_at_8000 = Wind(wind_direction, wind_speed * 3)
if self.mission.weather.clouds_density > 0:
self.mission.weather.clouds_base = max(self.mission.weather.clouds_base, WEATHER_CLOUD_BASE_MIN)
def generate(self) -> EnvironmentSettings:
self._gen_random_time()
self._gen_random_weather()
settings = EnvironmentSettings()
settings.start_time = self.mission.start_time
settings.weather_dict = self.mission.weather.dict()
return settings
def load(self, settings: EnvironmentSettings):
self.mission.start_time = settings.start_time
self.mission.weather.load_from_dict(settings.weather_dict)
def set_wind(self, wind: WindConditions) -> None:
self.mission.weather.wind_at_ground = wind.at_0m
self.mission.weather.wind_at_2000 = wind.at_2000m
self.mission.weather.wind_at_8000 = wind.at_8000m
def generate(self):
self.mission.start_time = self.conditions.start_time
self.set_clouds(self.conditions.weather.clouds)
self.set_fog(self.conditions.weather.fog)
self.set_wind(self.conditions.weather.wind)

View File

@@ -0,0 +1,26 @@
import random
from gen.sam.group_generator import ShipGroupGenerator
class CarrierGroupGenerator(ShipGroupGenerator):
def generate(self):
# Add carrier
if len(self.faction.aircraft_carrier) > 0:
carrier_type = random.choice(self.faction.aircraft_carrier)
self.add_unit(carrier_type, "Carrier", self.position.x, self.position.y, self.heading)
else:
return
# Add destroyers escort
if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers)
self.add_unit(dd_type, "DD1", self.position.x + 2500, self.position.y + 4500, self.heading)
self.add_unit(dd_type, "DD2", self.position.x + 2500, self.position.y - 4500, self.heading)
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

53
gen/fleet/cn_dd_group.py Normal file
View File

@@ -0,0 +1,53 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
Type_052C_Destroyer,
Type_052B_Destroyer,
Type_054A_Frigate,
CGN_1144_2_Pyotr_Velikiy,
)
from game.factions.faction import Faction
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
class ChineseNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False])
if include_dd:
include_cc = random.choice([True, False])
else:
include_cc = False
if include_frigate:
self.add_unit(Type_054A_Frigate, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
self.add_unit(Type_054A_Frigate, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
if include_dd:
dd_type = random.choice([Type_052C_Destroyer, Type_052B_Destroyer])
self.add_unit(dd_type, "FF1", self.position.x + 2400, self.position.y + 900, self.heading)
self.add_unit(dd_type, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
if include_cc:
cc_type = random.choice([CGN_1144_2_Pyotr_Velikiy])
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
self.get_generated_group().points[0].speed = 20
class Type54GroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(Type54GroupGenerator, self).__init__(game, ground_object, faction, Type_054A_Frigate)

34
gen/fleet/dd_group.py Normal file
View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from typing import TYPE_CHECKING
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(ShipGroupGenerator):
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):
self.add_unit(self.ddtype, "DD1", self.position.x + 500, self.position.y + 900, self.heading)
self.add_unit(self.ddtype, "DD2", self.position.x + 500, self.position.y - 900, self.heading)
self.get_generated_group().points[0].speed = 20
class OliverHazardPerryGroupGenerator(DDGroupGenerator):
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: Game, ground_object: TheaterGroundObject, faction: Faction):
super(ArleighBurkeGroupGenerator, self).__init__(game, ground_object, faction, USS_Arleigh_Burke_IIa)

21
gen/fleet/lha_group.py Normal file
View File

@@ -0,0 +1,21 @@
import random
from gen.sam.group_generator import ShipGroupGenerator
class LHAGroupGenerator(ShipGroupGenerator):
def generate(self):
# Add carrier
if len(self.faction.helicopter_carrier) > 0:
carrier_type = random.choice(self.faction.helicopter_carrier)
self.add_unit(carrier_type, "LHA", self.position.x, self.position.y, self.heading)
# Add destroyers escort
if len(self.faction.destroyers) > 0:
dd_type = random.choice(self.faction.destroyers)
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

72
gen/fleet/ru_dd_group.py Normal file
View File

@@ -0,0 +1,72 @@
from __future__ import annotations
import random
from typing import TYPE_CHECKING
from dcs.ships import (
FFL_1124_4_Grisha,
FSG_1241_1MP_Molniya,
FFG_11540_Neustrashimy,
FF_1135M_Rezky,
CG_1164_Moskva,
CGN_1144_2_Pyotr_Velikiy,
SSK_877,
SSK_641B
)
from gen.fleet.dd_group import DDGroupGenerator
from gen.sam.group_generator import ShipGroupGenerator
from game.factions.faction import Faction
from theater.theatergroundobject import TheaterGroundObject
if TYPE_CHECKING:
from game.game import Game
class RussianNavyGroupGenerator(ShipGroupGenerator):
def generate(self):
include_frigate = random.choice([True, True, False])
include_dd = random.choice([True, False])
if include_dd:
include_cc = random.choice([True, False])
else:
include_cc = False
if include_frigate:
frigate_type = random.choice([FFL_1124_4_Grisha, FSG_1241_1MP_Molniya])
self.add_unit(frigate_type, "FF1", self.position.x + 1200, self.position.y + 900, self.heading)
self.add_unit(frigate_type, "FF2", self.position.x + 1200, self.position.y - 900, self.heading)
if include_dd:
dd_type = random.choice([FFG_11540_Neustrashimy, FF_1135M_Rezky])
self.add_unit(dd_type, "FF1", self.position.x + 2400, self.position.y + 900, self.heading)
self.add_unit(dd_type, "FF2", self.position.x + 2400, self.position.y - 900, self.heading)
if include_cc:
cc_type = random.choice([CG_1164_Moskva, CGN_1144_2_Pyotr_Velikiy])
self.add_unit(cc_type, "CC1", self.position.x, self.position.y, self.heading)
self.get_generated_group().points[0].speed = 20
class GrishaGroupGenerator(DDGroupGenerator):
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: 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: Game, ground_object: TheaterGroundObject, faction: Faction):
super(KiloSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_877)
class TangoSubGroupGenerator(DDGroupGenerator):
def __init__(self, game: Game, ground_object: TheaterGroundObject, faction: Faction):
super(TangoSubGroupGenerator, self).__init__(game, ground_object, faction, SSK_641B)

15
gen/fleet/schnellboot.py Normal file
View File

@@ -0,0 +1,15 @@
import random
from dcs.ships import Schnellboot_type_S130
from gen.sam.group_generator import ShipGroupGenerator
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

View File

@@ -0,0 +1,72 @@
import logging
import random
from game import db
from gen.fleet.carrier_group import CarrierGroupGenerator
from gen.fleet.cn_dd_group import ChineseNavyGroupGenerator, Type54GroupGenerator
from gen.fleet.dd_group import ArleighBurkeGroupGenerator, OliverHazardPerryGroupGenerator
from gen.fleet.lha_group import LHAGroupGenerator
from gen.fleet.ru_dd_group import RussianNavyGroupGenerator, GrishaGroupGenerator, MolniyaGroupGenerator, \
KiloSubGroupGenerator, TangoSubGroupGenerator
from gen.fleet.schnellboot import SchnellbootGroupGenerator
from gen.fleet.uboat import UBoatGroupGenerator
from gen.fleet.ww2lst import WW2LSTGroupGenerator
SHIP_MAP = {
"SchnellbootGroupGenerator": SchnellbootGroupGenerator,
"WW2LSTGroupGenerator": WW2LSTGroupGenerator,
"UBoatGroupGenerator": UBoatGroupGenerator,
"OliverHazardPerryGroupGenerator": OliverHazardPerryGroupGenerator,
"ArleighBurkeGroupGenerator": ArleighBurkeGroupGenerator,
"RussianNavyGroupGenerator": RussianNavyGroupGenerator,
"ChineseNavyGroupGenerator": ChineseNavyGroupGenerator,
"GrishaGroupGenerator": GrishaGroupGenerator,
"MolniyaGroupGenerator": MolniyaGroupGenerator,
"KiloSubGroupGenerator": KiloSubGroupGenerator,
"TangoSubGroupGenerator": TangoSubGroupGenerator,
"Type54GroupGenerator": Type54GroupGenerator
}
def generate_ship_group(game, ground_object, faction_name: str):
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction_name]
if len(faction.navy_generators) > 0:
gen = random.choice(faction.navy_generators)
if gen in SHIP_MAP.keys():
generator = SHIP_MAP[gen](game, ground_object, faction)
generator.generate()
return generator.get_generated_group()
else:
logging.info("Unable to generate ship group, generator : " + str(gen) + "does not exists")
return None
def generate_carrier_group(faction: str, game, ground_object):
"""
This generate a carrier group
:param parentCp: The parent control point
:param ground_object: The ground object which will own the ship group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
"""
generator = CarrierGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()
return generator.get_generated_group()
def generate_lha_group(faction: str, game, ground_object):
"""
This generate a lha carrier group
:param parentCp: The parent control point
:param ground_object: The ground object which will own the ship group
:param country: Owner country
:return: Nothing, but put the group reference inside the ground object
"""
generator = LHAGroupGenerator(game, ground_object, db.FACTIONS[faction])
generator.generate()
return generator.get_generated_group()

15
gen/fleet/uboat.py Normal file
View File

@@ -0,0 +1,15 @@
import random
from dcs.ships import Uboat_VIIC_U_flak
from gen.sam.group_generator import ShipGroupGenerator
class UBoatGroupGenerator(ShipGroupGenerator):
def generate(self):
for i in range(random.randint(1, 4)):
self.add_unit(Uboat_VIIC_U_flak, "Uboat" + 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

18
gen/fleet/ww2lst.py Normal file
View File

@@ -0,0 +1,18 @@
import random
from dcs.ships import LS_Samuel_Chase, LST_Mk_II
from gen.sam.group_generator import ShipGroupGenerator
class WW2LSTGroupGenerator(ShipGroupGenerator):
def generate(self):
# Add LS Samuel Chase
self.add_unit(LS_Samuel_Chase, "SamuelChase", self.position.x, self.position.y, self.heading)
for i in range(1, random.randint(3, 4)):
self.add_unit(LST_Mk_II, "LST" + str(i), self.position.x + i * random.randint(800, 1200), self.position.y, self.heading)
self.get_generated_group().points[0].speed = 20

View File

@@ -0,0 +1,535 @@
from __future__ import annotations
import logging
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
from game import db
from game.data.radar_db import UNITS_WITH_RADAR
from game.infos.information import Information
from game.utils import nm_to_meter
from gen import Conflict
from gen.ato import Package
from gen.flights.ai_flight_planner_db import (
CAP_CAPABLE,
CAP_PREFERRED,
CAS_CAPABLE,
CAS_PREFERRED,
SEAD_CAPABLE,
SEAD_PREFERRED,
STRIKE_CAPABLE,
STRIKE_PREFERRED,
)
from gen.flights.closestairfields import (
ClosestAirfields,
ObjectiveDistanceCache,
)
from gen.flights.flight import (
Flight,
FlightType,
)
from gen.flights.flightplan import FlightPlanBuilder
from gen.flights.traveltime import TotEstimator
from theater import (
ControlPoint,
FrontLine,
MissionTarget,
TheaterGroundObject,
SamGroundObject,
)
# Avoid importing some types that cause circular imports unless type checking.
if TYPE_CHECKING:
from game import Game
from game.inventory import GlobalAircraftInventory
@dataclass(frozen=True)
class ProposedFlight:
"""A flight outline proposed by the mission planner.
Proposed flights haven't been assigned specific aircraft yet. They have only
a task, a required number of aircraft, and a maximum distance allowed
between the objective and the departure airfield.
"""
#: The flight's role.
task: FlightType
#: The number of aircraft required.
num_aircraft: int
#: The maximum distance between the objective and the departure airfield.
max_distance: int
def __str__(self) -> str:
return f"{self.task.name} {self.num_aircraft} ship"
@dataclass(frozen=True)
class ProposedMission:
"""A mission outline proposed by the mission planner.
Proposed missions haven't been assigned aircraft yet. They have only an
objective location and a list of proposed flights that are required for the
mission.
"""
#: The mission objective.
location: MissionTarget
#: The proposed flights that are required for the mission.
flights: List[ProposedFlight]
def __str__(self) -> str:
flights = ', '.join([str(f) for f in self.flights])
return f"{self.location.name}: {flights}"
class AircraftAllocator:
"""Finds suitable aircraft for proposed missions."""
def __init__(self, closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool) -> None:
self.closest_airfields = closest_airfields
self.global_inventory = global_inventory
self.is_player = is_player
def find_aircraft_for_flight(
self, flight: ProposedFlight
) -> Optional[Tuple[ControlPoint, UnitType]]:
"""Finds aircraft suitable for the given mission.
Searches for aircraft capable of performing the given mission within the
maximum allowed range. If insufficient aircraft are available for the
mission, None is returned.
Airfields are searched ordered nearest to farthest from the target and
searched twice. The first search looks for aircraft which prefer the
mission type, and the second search looks for any aircraft which are
capable of the mission type. For example, an F-14 from a nearby carrier
will be preferred for the CAP of an airfield that has only F-16s, but if
the carrier has only F/A-18s the F-16s will be used for CAP instead.
Note that aircraft *will* be removed from the global inventory on
success. This is to ensure that the same aircraft are not matched twice
on subsequent calls. If the found aircraft are not used, the caller is
responsible for returning them to the inventory.
"""
result = self.find_aircraft_of_type(
flight, self.preferred_aircraft_for_task(flight.task)
)
if result is not None:
return result
return self.find_aircraft_of_type(
flight, self.capable_aircraft_for_task(flight.task)
)
@staticmethod
def preferred_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_PREFERRED
elif task == FlightType.CAS:
return CAS_PREFERRED
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_PREFERRED
elif task == FlightType.STRIKE:
return STRIKE_PREFERRED
elif task == FlightType.ESCORT:
return CAP_PREFERRED
else:
return []
@staticmethod
def capable_aircraft_for_task(task: FlightType) -> List[Type[FlyingType]]:
cap_missions = (FlightType.BARCAP, FlightType.TARCAP)
if task in cap_missions:
return CAP_CAPABLE
elif task == FlightType.CAS:
return CAS_CAPABLE
elif task in (FlightType.DEAD, FlightType.SEAD):
return SEAD_CAPABLE
elif task == FlightType.STRIKE:
return STRIKE_CAPABLE
elif task == FlightType.ESCORT:
return CAP_CAPABLE
else:
logging.error(f"Unplannable flight type: {task}")
return []
def find_aircraft_of_type(
self, flight: ProposedFlight, types: List[Type[FlyingType]],
) -> Optional[Tuple[ControlPoint, UnitType]]:
airfields_in_range = self.closest_airfields.airfields_within(
flight.max_distance
)
for airfield in airfields_in_range:
if not airfield.is_friendly(self.is_player):
continue
inventory = self.global_inventory.for_control_point(airfield)
for aircraft, available in inventory.all_aircraft:
if aircraft in types and available >= flight.num_aircraft:
inventory.remove_aircraft(aircraft, flight.num_aircraft)
return airfield, aircraft
return None
class PackageBuilder:
"""Builds a Package for the flights it receives."""
def __init__(self, location: MissionTarget,
closest_airfields: ClosestAirfields,
global_inventory: GlobalAircraftInventory,
is_player: bool,
start_type: str) -> None:
self.package = Package(location)
self.allocator = AircraftAllocator(closest_airfields, global_inventory,
is_player)
self.global_inventory = global_inventory
self.start_type = start_type
def plan_flight(self, plan: ProposedFlight) -> bool:
"""Allocates aircraft for the given flight and adds them to the package.
If no suitable aircraft are available, False is returned. If the failed
flight was critical and the rest of the mission will be scrubbed, the
caller should return any previously planned flights to the inventory
using release_planned_aircraft.
"""
assignment = self.allocator.find_aircraft_for_flight(plan)
if assignment is None:
return False
airfield, aircraft = assignment
flight = Flight(self.package, aircraft, plan.num_aircraft, airfield,
plan.task, self.start_type)
self.package.add_flight(flight)
return True
def build(self) -> Package:
"""Returns the built package."""
return self.package
def release_planned_aircraft(self) -> None:
"""Returns any planned flights to the inventory."""
flights = list(self.package.flights)
for flight in flights:
self.global_inventory.return_from_flight(flight)
self.package.remove_flight(flight)
class ObjectiveFinder:
"""Identifies potential objectives for the mission planner."""
# TODO: Merge into doctrine.
AIRFIELD_THREAT_RANGE = nm_to_meter(150)
SAM_THREAT_RANGE = nm_to_meter(100)
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
self.is_player = is_player
def enemy_sams(self) -> Iterator[TheaterGroundObject]:
"""Iterates over all enemy SAM sites."""
# Control points might have the same ground object several times, for
# some reason.
found_targets: Set[str] = set()
for cp in self.enemy_control_points():
for ground_object in cp.ground_objects:
if not isinstance(ground_object, SamGroundObject):
continue
if ground_object.is_dead:
continue
if ground_object.name in found_targets:
continue
if not self.object_has_radar(ground_object):
continue
# TODO: Yield in order of most threatening.
# Need to sort in order of how close their defensive range comes
# to friendly assets. To do that we need to add effective range
# information to the database.
yield ground_object
found_targets.add(ground_object.name)
def threatening_sams(self) -> Iterator[TheaterGroundObject]:
"""Iterates over enemy SAMs in threat range of friendly control points.
SAM sites are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
sams: List[Tuple[TheaterGroundObject, int]] = []
for sam in self.enemy_sams():
ranges: List[int] = []
for cp in self.friendly_control_points():
ranges.append(sam.distance_to(cp))
sams.append((sam, min(ranges)))
sams = sorted(sams, key=operator.itemgetter(1))
for sam, _range in sams:
yield sam
def strike_targets(self) -> Iterator[TheaterGroundObject]:
"""Iterates over enemy strike targets.
Targets are sorted by their closest proximity to any friendly control
point (airfield or fleet).
"""
targets: List[Tuple[TheaterGroundObject, int]] = []
# Control points might have the same ground object several times, for
# some reason.
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] = []
for friendly_cp in self.friendly_control_points():
ranges.append(ground_object.distance_to(friendly_cp))
targets.append((ground_object, min(ranges)))
found_targets.add(ground_object.name)
targets = sorted(targets, key=operator.itemgetter(1))
for target, _range in targets:
yield target
@staticmethod
def object_has_radar(ground_object: TheaterGroundObject) -> bool:
"""Returns True if the ground object contains a unit with radar."""
for group in ground_object.groups:
for unit in group.units:
if db.unit_type_from_name(unit.type) in UNITS_WITH_RADAR:
return True
return False
def front_lines(self) -> Iterator[FrontLine]:
"""Iterates over all active front lines in the theater."""
for cp in self.friendly_control_points():
for connected in cp.connected_points:
if connected.is_friendly(self.is_player):
continue
if Conflict.has_frontline_between(cp, connected):
yield FrontLine(cp, connected)
def vulnerable_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over friendly CPs that are vulnerable to enemy CPs.
Vulnerability is defined as any enemy CP within threat range of of the
CP.
"""
for cp in self.friendly_control_points():
airfields_in_proximity = self.closest_airfields_to(cp)
airfields_in_threat_range = airfields_in_proximity.airfields_within(
self.AIRFIELD_THREAT_RANGE
)
for airfield in airfields_in_threat_range:
if not airfield.is_friendly(self.is_player):
yield cp
break
def friendly_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all friendly control points."""
return (c for c in self.game.theater.controlpoints if
c.is_friendly(self.is_player))
def enemy_control_points(self) -> Iterator[ControlPoint]:
"""Iterates over all enemy control points."""
return (c for c in self.game.theater.controlpoints if
not c.is_friendly(self.is_player))
def all_possible_targets(self) -> Iterator[MissionTarget]:
"""Iterates over all possible mission targets in the theater.
Valid mission targets are control points (airfields and carriers), front
lines, and ground objects (SAM sites, factories, resource extraction
sites, etc).
"""
for cp in self.game.theater.controlpoints:
yield cp
yield from cp.ground_objects
yield from self.front_lines()
@staticmethod
def closest_airfields_to(location: MissionTarget) -> ClosestAirfields:
"""Returns the closest airfields to the given location."""
return ObjectiveDistanceCache.get_closest_airfields(location)
class CoalitionMissionPlanner:
"""Coalition flight planning AI.
This class is responsible for automatically planning missions for the
coalition at the start of the turn.
The primary goal of the mission planner is to protect existing friendly
assets. Missions will be planned with the following priorities:
1. CAP for airfields/fleets in close proximity to the enemy to prevent heavy
losses of friendly aircraft.
2. CAP for front line areas to protect ground and CAS units.
3. DEAD to reduce necessity of SEAD for future missions.
4. CAS to protect friendly ground units.
5. Strike missions to reduce the enemy's resources.
TODO: Anti-ship and airfield strikes to reduce enemy sortie rates.
TODO: BAI to prevent enemy forces from reaching the front line.
TODO: Should fleets always have a CAP?
TODO: Stance and doctrine-specific planning behavior.
"""
# TODO: Merge into doctrine, also limit by aircraft.
MAX_CAP_RANGE = nm_to_meter(100)
MAX_CAS_RANGE = nm_to_meter(50)
MAX_SEAD_RANGE = nm_to_meter(150)
MAX_STRIKE_RANGE = nm_to_meter(150)
def __init__(self, game: Game, is_player: bool) -> None:
self.game = game
self.is_player = is_player
self.objective_finder = ObjectiveFinder(self.game, self.is_player)
self.ato = self.game.blue_ato if is_player else self.game.red_ato
def propose_missions(self) -> Iterator[ProposedMission]:
"""Identifies and iterates over potential mission in priority order."""
# Find friendly CPs within 100 nmi from an enemy airfield, plan CAP.
for cp in self.objective_finder.vulnerable_control_points():
yield ProposedMission(cp, [
ProposedFlight(FlightType.BARCAP, 2, self.MAX_CAP_RANGE),
])
# Find front lines, plan CAP.
for front_line in self.objective_finder.front_lines():
yield ProposedMission(front_line, [
ProposedFlight(FlightType.TARCAP, 2, self.MAX_CAP_RANGE),
ProposedFlight(FlightType.CAS, 2, self.MAX_CAS_RANGE),
])
# Find enemy SAM sites with ranges that cover friendly CPs, front lines,
# or objects, plan DEAD.
# Find enemy SAM sites with ranges that extend to within 50 nmi of
# friendly CPs, front, lines, or objects, plan DEAD.
for sam in self.objective_finder.threatening_sams():
yield ProposedMission(sam, [
ProposedFlight(FlightType.DEAD, 2, self.MAX_SEAD_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.ESCORT, 2, self.MAX_SEAD_RANGE),
])
# Plan strike missions.
for target in self.objective_finder.strike_targets():
yield ProposedMission(target, [
ProposedFlight(FlightType.STRIKE, 2, self.MAX_STRIKE_RANGE),
# TODO: Max escort range.
ProposedFlight(FlightType.SEAD, 2, self.MAX_STRIKE_RANGE),
ProposedFlight(FlightType.ESCORT, 2, self.MAX_STRIKE_RANGE),
])
def plan_missions(self) -> None:
"""Identifies and plans mission for the turn."""
for proposed_mission in self.propose_missions():
self.plan_mission(proposed_mission)
self.stagger_missions()
for cp in self.objective_finder.friendly_control_points():
inventory = self.game.aircraft_inventory.for_control_point(cp)
for aircraft, available in inventory.all_aircraft:
self.message("Unused aircraft",
f"{available} {aircraft.id} from {cp}")
def plan_mission(self, mission: ProposedMission) -> None:
"""Allocates aircraft for a proposed mission and adds it to the ATO."""
if self.game.settings.perf_ai_parking_start:
start_type = "Cold"
else:
start_type = "Warm"
builder = PackageBuilder(
mission.location,
self.objective_finder.closest_airfields_to(mission.location),
self.game.aircraft_inventory,
self.is_player,
start_type
)
missing_types: Set[FlightType] = set()
for proposed_flight in mission.flights:
if not builder.plan_flight(proposed_flight):
missing_types.add(proposed_flight.task)
if missing_types:
missing_types_str = ", ".join(
sorted([t.name for t in missing_types]))
builder.release_planned_aircraft()
self.message(
"Insufficient aircraft",
f"Not enough aircraft in range for {mission.location.name} "
f"capable of: {missing_types_str}")
return
package = builder.build()
flight_plan_builder = FlightPlanBuilder(self.game, package,
self.is_player)
for flight in package.flights:
flight_plan_builder.populate_flight_plan(flight)
self.ato.add_package(package)
def stagger_missions(self) -> None:
def start_time_generator(count: int, earliest: int, latest: int,
margin: int) -> Iterator[timedelta]:
interval = (latest - earliest) // count
for time in range(earliest, latest, interval):
error = random.randint(-margin, margin)
yield timedelta(minutes=max(0, time + error))
dca_types = (FlightType.BARCAP, FlightType.INTERCEPTION)
non_dca_packages = [p for p in self.ato.packages if
p.primary_task not in dca_types]
start_time = start_time_generator(
count=len(non_dca_packages),
earliest=5,
latest=90,
margin=5
)
for package in self.ato.packages:
tot = TotEstimator(package).earliest_tot()
if package.primary_task in dca_types:
# All CAP missions should be on station ASAP.
package.time_over_target = tot
else:
# But other packages should be spread out a bit. Note that take
# times are delayed, but all aircraft will become active at
# mission start. This makes it more worthwhile to attack enemy
# airfields to hit grounded aircraft, since they're more likely
# to be present. Runway and air started aircraft will be
# delayed until their takeoff time by AirConflictGenerator.
package.time_over_target = next(start_time) + tot
def message(self, title, text) -> None:
"""Emits a planning message to the player.
If the mission planner belongs to the players coalition, this emits a
message to the info panel.
"""
if self.is_player:
self.game.informations.append(
Information(title, text, self.game.turn)
)
else:
logging.info(f"{title}: {text}")

View File

@@ -0,0 +1,474 @@
from dcs.helicopters import (
AH_1W,
AH_64A,
AH_64D,
Ka_50,
Mi_24V,
Mi_28N,
Mi_8MT,
OH_58D,
SA342L,
SA342M,
UH_1H,
)
from dcs.planes import (
AJS37,
AV8BNA,
A_10A,
A_10C,
A_10C_2,
A_20G,
B_17G,
B_1B,
B_52H,
Bf_109K_4,
C_101CC,
FA_18C_hornet,
FW_190A8,
FW_190D9,
F_117A,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
F_4E,
F_5E_3,
F_86F_Sabre,
F_A_18C,
JF_17,
J_11A,
Ju_88A4,
L_39ZA,
MQ_9_Reaper,
M_2000C,
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_27K,
MiG_29A,
MiG_29G,
MiG_29K,
MiG_29S,
MiG_31,
Mirage_2000_5,
P_47D_30,
P_47D_30bl1,
P_47D_40,
P_51D,
P_51D_30_NA,
RQ_1A_Predator,
SpitfireLFMkIX,
SpitfireLFMkIXCW,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_27,
Su_30,
Su_33,
Su_34,
Tornado_GR4,
Tornado_IDS,
Tu_160,
Tu_22M3,
Tu_95MS,
WingLoong_I,
)
# Interceptor are the aircraft prioritized for interception tasks
# If none is available, the AI will use regular CAP-capable aircraft instead
from pydcs_extensions.a4ec.a4ec import A_4E_C
from pydcs_extensions.mb339.mb339 import MB_339PAN
from pydcs_extensions.rafale.rafale import Rafale_A_S, Rafale_M
# TODO: These lists really ought to be era (faction) dependent.
# Factions which have F-5s, F-86s, and A-4s will should prefer F-5s for CAP, but
# factions that also have F-4s should not.
from pydcs_extensions.su57.su57 import Su_57
INTERCEPT_CAPABLE = [
MiG_21Bis,
MiG_25PD,
MiG_31,
MiG_29S,
MiG_29A,
MiG_29G,
MiG_29K,
M_2000C,
Mirage_2000_5,
Rafale_M,
F_14A_135_GR,
F_14B,
F_15C,
]
# Used for CAP, Escort, and intercept if there is not a specialised aircraft available
CAP_CAPABLE = [
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31,
Su_27,
J_11A,
JF_17,
Su_30,
Su_33,
Su_57,
M_2000C,
Mirage_2000_5,
F_86F_Sabre,
F_4E,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15C,
F_15E,
F_16A,
F_16C_50,
FA_18C_hornet,
C_101CC,
L_39ZA,
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
A_4E_C,
Rafale_M,
]
CAP_PREFERRED = [
MiG_15bis,
MiG_19P,
MiG_21Bis,
MiG_23MLD,
MiG_25PD,
MiG_29A,
MiG_29G,
MiG_29S,
MiG_31,
Su_27,
J_11A,
Su_30,
Su_33,
Su_57,
M_2000C,
Mirage_2000_5,
F_86F_Sabre,
F_14A_135_GR,
F_14B,
F_15C,
P_51D_30_NA,
P_51D,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
Rafale_M,
]
# Used for CAS (Close air support) and BAI (Battlefield Interdiction)
CAS_CAPABLE = [
MiG_15bis,
MiG_29A,
MiG_27K,
MiG_29S,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_34,
JF_17,
M_2000C,
A_10A,
A_10C,
A_10C_2,
AV8BNA,
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,
F_16C_50,
FA_18C_hornet,
B_1B,
Tornado_IDS,
Tornado_GR4,
C_101CC,
MB_339PAN,
L_39ZA,
AJS37,
SA342M,
SA342L,
OH_58D,
AH_64A,
AH_64D,
AH_1W,
UH_1H,
Mi_8MT,
Mi_28N,
Mi_24V,
Ka_50,
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
A_4E_C,
Rafale_A_S,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator
]
CAS_PREFERRED = [
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_25TM,
Su_34,
JF_17,
A_10A,
A_10C,
A_10C_2,
AV8BNA,
F_15E,
Tornado_GR4,
C_101CC,
MB_339PAN,
L_39ZA,
AJS37,
SA342M,
SA342L,
OH_58D,
AH_64A,
AH_64D,
AH_1W,
UH_1H,
Mi_8MT,
Mi_28N,
Mi_24V,
Ka_50,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
A_4E_C,
Rafale_A_S,
WingLoong_I,
MQ_9_Reaper,
RQ_1A_Predator
]
# Aircraft used for SEAD / DEAD tasks
SEAD_CAPABLE = [
F_4E,
FA_18C_hornet,
F_15E,
F_16C_50,
AV8BNA,
JF_17,
Su_24M,
Su_25T,
Su_25TM,
Su_17M4,
Su_30,
Su_34,
MiG_27K,
Tornado_IDS,
Tornado_GR4,
A_4E_C,
Rafale_A_S
]
SEAD_PREFERRED = [
F_4E,
Su_25T,
Tornado_IDS,
]
# Aircraft used for Strike mission
STRIKE_CAPABLE = [
MiG_15bis,
MiG_27K,
MB_339PAN,
Su_17M4,
Su_24M,
Su_24MR,
Su_25,
Su_25T,
Su_34,
Tu_160,
Tu_22M3,
Tu_95MS,
JF_17,
M_2000C,
A_10A,
A_10C,
A_10C_2,
AV8BNA,
F_86F_Sabre,
F_5E_3,
F_14A_135_GR,
F_14B,
F_15E,
F_16A,
F_16C_50,
FA_18C_hornet,
B_1B,
B_52H,
F_117A,
Tornado_IDS,
Tornado_GR4,
C_101CC,
L_39ZA,
AJS37,
P_51D_30_NA,
P_51D,
P_47D_30,
P_47D_30bl1,
P_47D_40,
A_20G,
B_17G,
SpitfireLFMkIXCW,
SpitfireLFMkIX,
Bf_109K_4,
FW_190D9,
FW_190A8,
A_4E_C,
Rafale_A_S
]
STRIKE_PREFERRED = [
AJS37,
A_20G,
B_17G,
B_1B,
B_52H,
F_117A,
F_15E,
Tornado_GR4,
Tu_160,
Tu_22M3,
Tu_95MS,
]
ANTISHIP_CAPABLE = [
Su_24M,
Su_17M4,
F_A_18C,
F_15E,
AV8BNA,
JF_17,
F_16A,
F_16C_50,
A_10C,
A_10C_2,
A_10A,
Tornado_IDS,
Tornado_GR4,
Ju_88A4,
Rafale_A_S
]
DRONES = [
MQ_9_Reaper,
RQ_1A_Predator,
WingLoong_I
]

View File

@@ -0,0 +1,51 @@
"""Objective adjacency lists."""
from typing import Dict, Iterator, List, Optional
from theater import ConflictTheater, ControlPoint, MissionTarget
class ClosestAirfields:
"""Precalculates which control points are closes to the given target."""
def __init__(self, target: MissionTarget,
all_control_points: List[ControlPoint]) -> None:
self.target = target
self.closest_airfields: List[ControlPoint] = sorted(
all_control_points, key=lambda c: self.target.distance_to(c)
)
def airfields_within(self, meters: int) -> Iterator[ControlPoint]:
"""Iterates over all airfields within the given range of the target.
Note that this iterates over *all* airfields, not just friendly
airfields.
"""
for cp in self.closest_airfields:
if cp.distance_to(self.target) < meters:
yield cp
else:
break
class ObjectiveDistanceCache:
theater: Optional[ConflictTheater] = None
closest_airfields: Dict[str, ClosestAirfields] = {}
@classmethod
def set_theater(cls, theater: ConflictTheater) -> None:
if cls.theater is not None:
cls.closest_airfields = {}
cls.theater = theater
@classmethod
def get_closest_airfields(cls, location: MissionTarget) -> ClosestAirfields:
if cls.theater is None:
raise RuntimeError(
"Call ObjectiveDistanceCache.set_theater before using"
)
if location.name not in cls.closest_airfields:
cls.closest_airfields[location.name] = ClosestAirfields(
location, cls.theater.controlpoints
)
return cls.closest_airfields[location.name]

162
gen/flights/flight.py Normal file
View File

@@ -0,0 +1,162 @@
from __future__ import annotations
from datetime import timedelta
from enum import Enum
from typing import Dict, List, Optional, TYPE_CHECKING
from dcs.mapping import Point
from dcs.point import MovingPoint, PointAction
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):
CAP = 0 # Do not use. Use BARCAP or TARCAP.
TARCAP = 1
BARCAP = 2
CAS = 3
INTERCEPTION = 4
STRIKE = 5
ANTISHIP = 6
SEAD = 7
DEAD = 8
ESCORT = 9
BAI = 10
# Helos
TROOP_TRANSPORT = 11
LOGISTICS = 12
EVAC = 13
ELINT = 14
RECON = 15
EWAR = 16
class FlightWaypointType(Enum):
TAKEOFF = 0 # Take off point
ASCEND_POINT = 1 # Ascension point after take off
PATROL = 2 # Patrol point
PATROL_TRACK = 3 # Patrol race track
NAV = 4 # Nav point
INGRESS_STRIKE = 5 # Ingress strike (For generator, means that this should have bombing on next TARGET_POINT points)
INGRESS_SEAD = 6 # Ingress sead (For generator, means that this should attack groups on TARGET_GROUP_LOC points)
INGRESS_CAS = 7 # Ingress cas (should start CAS task)
CAS = 8 # Should do CAS there
EGRESS = 9 # Should stop attack
DESCENT_POINT = 10 # Should start descending to pattern alt
LANDING_POINT = 11 # Should land there
TARGET_POINT = 12 # A target building or static object, position
TARGET_GROUP_LOC = 13 # A target group approximate location
TARGET_SHIP = 14 # A target ship known location
CUSTOM = 15 # User waypoint (no specific behaviour)
JOIN = 16
SPLIT = 17
LOITER = 18
INGRESS_ESCORT = 19
INGRESS_DEAD = 20
class FlightWaypoint:
def __init__(self, waypoint_type: FlightWaypointType, x: float, y: float,
alt: int = 0) -> None:
"""Creates a flight waypoint.
Args:
waypoint_type: The waypoint type.
x: X cooidinate of the waypoint.
y: Y coordinate of the waypoint.
alt: Altitude of the waypoint. By default this is AGL, but it can be
changed to MSL by setting alt_type to "RADIO".
"""
self.waypoint_type = waypoint_type
self.x = x
self.y = y
self.alt = alt
self.alt_type = "BARO"
self.name = ""
self.description = ""
self.targets: List[MissionTarget] = []
self.obj_name = ""
self.pretty_name = ""
self.only_for_player = False
# These are set very late by the air conflict generator (part of mission
# generation). We do it late so that we don't need to propagate changes
# to waypoint times whenever the player alters the package TOT or the
# flight's offset in the UI.
self.tot: Optional[timedelta] = None
self.departure_time: Optional[timedelta] = None
@property
def position(self) -> Point:
return Point(self.x, self.y)
@classmethod
def from_pydcs(cls, point: MovingPoint,
from_cp: ControlPoint) -> "FlightWaypoint":
waypoint = FlightWaypoint(FlightWaypointType.NAV, point.position.x,
point.position.y, point.alt)
waypoint.alt_type = point.alt_type
# Other actions exist... but none of them *should* be the first
# waypoint for a flight.
waypoint.waypoint_type = {
PointAction.TurningPoint: FlightWaypointType.NAV,
PointAction.FlyOverPoint: FlightWaypointType.NAV,
PointAction.FromParkingArea: FlightWaypointType.TAKEOFF,
PointAction.FromParkingAreaHot: FlightWaypointType.TAKEOFF,
PointAction.FromRunway: FlightWaypointType.TAKEOFF,
}[point.action]
if waypoint.waypoint_type == FlightWaypointType.NAV:
waypoint.name = "NAV"
waypoint.pretty_name = "Nav"
waypoint.description = "Nav"
else:
waypoint.name = "TAKEOFF"
waypoint.pretty_name = "Takeoff"
waypoint.description = "Takeoff"
waypoint.description = f"Takeoff from {from_cp.name}"
return waypoint
class Flight:
def __init__(self, package: Package, unit_type: FlyingType, count: int,
from_cp: ControlPoint, flight_type: FlightType,
start_type: str) -> None:
self.package = package
self.unit_type = unit_type
self.count = count
self.from_cp = from_cp
self.flight_type = flight_type
# TODO: Replace with FlightPlan.
self.targets: List[MissionTarget] = []
self.loadout: Dict[str, str] = {}
self.start_type = start_type
self.use_custom_loadout = False
self.client_count = 0
# Will be replaced with a more appropriate FlightPlan by
# FlightPlanBuilder, but an empty flight plan the flight begins with an
# empty flight plan.
from gen.flights.flightplan import CustomFlightPlan
self.flight_plan: FlightPlan = CustomFlightPlan(
package=package,
flight=self,
custom_waypoints=[]
)
@property
def points(self) -> List[FlightWaypoint]:
return self.flight_plan.waypoints[1:]
def __repr__(self):
return self.flight_type.name + " | " + str(self.count) + "x" + db.unit_type_name(self.unit_type) \
+ " (" + str(len(self.points)) + " wpt)"

1021
gen/flights/flightplan.py Normal file

File diff suppressed because it is too large Load Diff

212
gen/flights/traveltime.py Normal file
View File

@@ -0,0 +1,212 @@
from __future__ import annotations
import logging
import math
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.flights.flight import Flight
if TYPE_CHECKING:
from gen.ato import Package
class GroundSpeed:
@classmethod
def for_flight(cls, flight: Flight, altitude: int) -> int:
if not issubclass(flight.unit_type, FlyingType):
raise TypeError("Flight has non-flying unit")
# TODO: Expose both a cruise speed and target speed.
# The cruise speed can be used for ascent, hold, join, and RTB to save
# on fuel, but mission speed will be fast enough to keep the flight
# safer.
c_sound_sea_level = 661.5
# DCS's max speed is in kph at 0 MSL. Convert to knots.
max_speed = flight.unit_type.max_speed * 0.539957
if max_speed > c_sound_sea_level:
# Aircraft is supersonic. Limit to mach 0.8 to conserve fuel and
# account for heavily loaded jets.
return int(cls.from_mach(0.8, altitude))
# For subsonic aircraft, assume the aircraft can reasonably perform at
# 80% of its maximum, and that it can maintain the same mach at altitude
# as it can at sea level. This probably isn't great assumption, but
# might. be sufficient given the wiggle room. We can come up with
# another heuristic if needed.
mach = max_speed * 0.8 / c_sound_sea_level
return int(cls.from_mach(mach, altitude)) # knots
@staticmethod
def from_mach(mach: float, altitude: int) -> float:
"""Returns the ground speed in knots for the given mach and altitude.
Args:
mach: The mach number to convert to ground speed.
altitude: The altitude in feet.
Returns:
The ground speed corresponding to the given altitude and mach number
in knots.
"""
# https://www.grc.nasa.gov/WWW/K-12/airplane/atmos.html
if altitude <= 36152:
temperature_f = 59 - 0.00356 * altitude
else:
# There's another formula for altitudes over 82k feet, but we better
# not be planning waypoints that high...
temperature_f = -70
temperature_k = (temperature_f + 459.67) * (5 / 9)
# https://www.engineeringtoolbox.com/specific-heat-ratio-d_602.html
# Dependent on temperature, but varies very little (+/-0.001)
# between -40F and 180F.
heat_capacity_ratio = 1.4
# https://www.grc.nasa.gov/WWW/K-12/airplane/sound.html
gas_constant = 286 # m^2/s^2/K
c_sound = math.sqrt(heat_capacity_ratio * gas_constant * temperature_k)
# c_sound is in m/s, convert to knots.
return (c_sound * 1.944) * mach
class TravelTime:
@staticmethod
def between_points(a: Point, b: Point, speed: float) -> timedelta:
error_factor = 1.1
distance = meter_to_nm(a.distance_to_point(b))
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 = timedelta(minutes=5)
def __init__(self, package: Package) -> None:
self.package = package
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)
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) -> Optional[timedelta]:
travel_time = self.travel_time_to_rendezvous_or_target(flight)
if travel_time is None:
from gen.flights.flightplan import CustomFlightPlan
if not isinstance(flight.flight_plan, CustomFlightPlan):
logging.warning(
"Found no rendezvous or target point. Cannot estimate "
f"takeoff time takeoff time for {flight}.")
return None
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:
tot = self.package.time_over_target
return tot - travel_time - self.HOLD_TIME
def earliest_tot(self) -> timedelta:
earliest_tot = max((
self.earliest_tot_for_flight(f) for f in self.package.flights
)) + self.HOLD_TIME
# Trim microseconds. DCS doesn't handle sub-second resolution for tasks,
# and they're not interesting from a mission planning perspective so we
# don't want them in the UI.
#
# Round up so we don't get negative start times.
return timedelta(seconds=math.ceil(earliest_tot.total_seconds()))
def earliest_tot_for_flight(self, flight: Flight) -> timedelta:
"""Estimate fastest time from mission start to the target position.
For BARCAP flights, this is time to race track start. This ensures that
they are on station at the same time any other package members reach
their ingress point.
For other mission types this is the time to the mission target.
Args:
flight: The flight to get the earliest TOT time for.
Returns:
The earliest possible TOT for the given flight in seconds. Returns 0
if an ingress point cannot be found.
"""
time_to_target = self.travel_time_to_target(flight)
if time_to_target is None:
logging.warning(f"Cannot estimate TOT for {flight}")
# Return 0 so this flight's travel time does not affect the rest
# of the package.
return timedelta()
startup = self.estimate_startup(flight)
ground_ops = self.estimate_ground_ops(flight)
return startup + ground_ops + time_to_target
@staticmethod
def estimate_startup(flight: Flight) -> timedelta:
if flight.start_type == "Cold":
if flight.client_count:
return timedelta(minutes=10)
else:
# The AI doesn't seem to have a real startup procedure.
return timedelta(minutes=2)
return timedelta()
@staticmethod
def estimate_ground_ops(flight: Flight) -> timedelta:
if flight.start_type in ("Runway", "In Flight"):
return timedelta()
if flight.from_cp.is_fleet:
return timedelta(minutes=2)
else:
return timedelta(minutes=5)
@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
@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

@@ -0,0 +1,310 @@
from __future__ import annotations
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.weather import Conditions
from theater import ControlPoint, MissionTarget, TheaterGroundObject
from .flight import Flight, FlightWaypoint, FlightWaypointType
@dataclass(frozen=True)
class StrikeTarget:
name: str
target: Union[TheaterGroundObject, Unit]
class WaypointBuilder:
def __init__(self, conditions: Conditions, flight: Flight,
doctrine: Doctrine,
targets: Optional[List[StrikeTarget]] = None) -> None:
self.conditions = conditions
self.flight = flight
self.doctrine = doctrine
self.targets = targets
@property
def is_helo(self) -> bool:
return getattr(self.flight.unit_type, "helicopter", False)
@staticmethod
def takeoff(departure: ControlPoint) -> FlightWaypoint:
"""Create takeoff waypoint for the given arrival airfield or carrier.
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.
"""
position = departure.position
waypoint = FlightWaypoint(
FlightWaypointType.TAKEOFF,
position.x,
position.y,
0
)
waypoint.name = "TAKEOFF"
waypoint.alt_type = "RADIO"
waypoint.description = "Takeoff"
waypoint.pretty_name = "Takeoff"
return waypoint
@staticmethod
def land(arrival: ControlPoint) -> FlightWaypoint:
"""Create descent waypoint for the given arrival airfield or carrier.
Args:
arrival: Arrival airfield or carrier.
"""
position = arrival.position
waypoint = FlightWaypoint(
FlightWaypointType.LANDING_POINT,
position.x,
position.y,
0
)
waypoint.name = "LANDING"
waypoint.alt_type = "RADIO"
waypoint.description = "Land"
waypoint.pretty_name = "Land"
return waypoint
def hold(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.LOITER,
position.x,
position.y,
500 if self.is_helo else self.doctrine.rendezvous_altitude
)
waypoint.pretty_name = "Hold"
waypoint.description = "Wait until push time"
waypoint.name = "HOLD"
return waypoint
def join(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.JOIN,
position.x,
position.y,
500 if self.is_helo else self.doctrine.ingress_altitude
)
waypoint.pretty_name = "Join"
waypoint.description = "Rendezvous with package"
waypoint.name = "JOIN"
return waypoint
def split(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.SPLIT,
position.x,
position.y,
500 if self.is_helo else self.doctrine.ingress_altitude
)
waypoint.pretty_name = "Split"
waypoint.description = "Depart from package"
waypoint.name = "SPLIT"
return waypoint
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) -> 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) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_SEAD, position,
objective)
def ingress_strike(self, position: Point,
objective: MissionTarget) -> FlightWaypoint:
return self._ingress(FlightWaypointType.INGRESS_STRIKE, position,
objective)
def _ingress(self, ingress_type: FlightWaypointType, position: Point,
objective: MissionTarget) -> FlightWaypoint:
waypoint = FlightWaypoint(
ingress_type,
position.x,
position.y,
500 if self.is_helo else self.doctrine.ingress_altitude
)
waypoint.pretty_name = "INGRESS on " + objective.name
waypoint.description = "INGRESS on " + objective.name
waypoint.name = "INGRESS"
# TODO: This seems wrong, but it's what was there before.
waypoint.targets.append(objective)
return waypoint
def egress(self, position: Point, target: MissionTarget) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.EGRESS,
position.x,
position.y,
500 if self.is_helo else self.doctrine.ingress_altitude
)
waypoint.pretty_name = "EGRESS from " + target.name
waypoint.description = "EGRESS from " + target.name
waypoint.name = "EGRESS"
return waypoint
def dead_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}")
def sead_point(self, target: StrikeTarget) -> FlightWaypoint:
return self._target_point(target, f"STRIKE {target.name}")
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.target.position.x,
target.target.position.y,
0
)
waypoint.description = description
waypoint.pretty_name = description
waypoint.name = target.name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True
return waypoint
def strike_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"STRIKE {target.name}", target)
def sead_area(self, target: MissionTarget) -> FlightWaypoint:
return self._target_area(f"SEAD on {target.name}", target)
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,
location.position.y,
0
)
waypoint.description = name
waypoint.pretty_name = name
waypoint.name = name
# The target waypoints are only for the player's benefit. AI tasks for
# the target are set on the ingress point so they begin their attack
# *before* reaching the target.
waypoint.only_for_player = True
return waypoint
def cas(self, position: Point) -> FlightWaypoint:
waypoint = FlightWaypoint(
FlightWaypointType.CAS,
position.x,
position.y,
500 if self.is_helo else 1000
)
waypoint.alt_type = "RADIO"
waypoint.description = "Provide CAS"
waypoint.name = "CAS"
waypoint.pretty_name = "CAS"
return waypoint
@staticmethod
def race_track_start(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a racetrack start waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the racetrack in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL_TRACK,
position.x,
position.y,
altitude
)
waypoint.name = "RACETRACK START"
waypoint.description = "Orbit between this point and the next point"
waypoint.pretty_name = "Race-track start"
return waypoint
@staticmethod
def race_track_end(position: Point, altitude: int) -> FlightWaypoint:
"""Creates a racetrack end waypoint.
Args:
position: Position of the waypoint.
altitude: Altitude of the racetrack in meters.
"""
waypoint = FlightWaypoint(
FlightWaypointType.PATROL,
position.x,
position.y,
altitude
)
waypoint.name = "RACETRACK END"
waypoint.description = "Orbit between this point and the previous point"
waypoint.pretty_name = "Race-track end"
return waypoint
def race_track(self, start: Point, end: Point,
altitude: int) -> Tuple[FlightWaypoint, FlightWaypoint]:
"""Creates two waypoint for a racetrack orbit.
Args:
start: The beginning racetrack waypoint.
end: The ending racetrack waypoint.
altitude: The racetrack altitude.
"""
return (self.race_track_start(start, altitude),
self.race_track_end(end, altitude))
def escort(self, ingress: Point, target: MissionTarget, egress: Point) -> \
Tuple[FlightWaypoint, FlightWaypoint, FlightWaypoint]:
"""Creates the waypoints needed to escort the package.
Args:
ingress: The package ingress point.
target: The mission target.
egress: The package egress point.
"""
# This would preferably be no points at all, and instead the Escort task
# would begin on the join point and end on the split point, however the
# escort task does not appear to work properly (see the longer
# description in gen.aircraft.JoinPointBuilder), so instead we give
# the escort flights a flight plan including the ingress point, target
# area, and egress point.
ingress = self._ingress(FlightWaypointType.INGRESS_ESCORT, ingress,
target)
waypoint = FlightWaypoint(
FlightWaypointType.TARGET_GROUP_LOC,
target.position.x,
target.position.y,
500 if self.is_helo else self.doctrine.ingress_altitude
)
waypoint.name = "TARGET"
waypoint.description = "Escort the package"
waypoint.pretty_name = "Target area"
egress = self.egress(egress, target)
return ingress, waypoint, egress

55
gen/forcedoptionsgen.py Normal file
View File

@@ -0,0 +1,55 @@
import logging
import typing
from enum import IntEnum
from dcs.mission import Mission
from dcs.forcedoptions import ForcedOptions
from .conflictgen import *
class Labels(IntEnum):
Off = 0
Full = 1
Abbreviated = 2
Dot = 3
class ForcedOptionsGenerator:
def __init__(self, mission: Mission, conflict: Conflict, game):
self.mission = mission
self.conflict = conflict
self.game = game
def _set_options_view(self):
if self.game.settings.map_coalition_visibility == ForcedOptions.Views.All:
self.mission.forced_options.options_view = ForcedOptions.Views.All
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.Allies:
self.mission.forced_options.options_view = ForcedOptions.Views.Allies
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyAllies:
self.mission.forced_options.options_view = ForcedOptions.Views.OnlyAllies
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.MyAircraft:
self.mission.forced_options.options_view = ForcedOptions.Views.MyAircraft
elif self.game.settings.map_coalition_visibility == ForcedOptions.Views.OnlyMap:
self.mission.forced_options.options_view = ForcedOptions.Views.OnlyMap
def _set_external_views(self):
if not self.game.settings.external_views_allowed:
self.mission.forced_options.external_views = self.game.settings.external_views_allowed
def _set_labels(self):
if self.game.settings.labels == "Abbreviated":
self.mission.forced_options.labels = int(Labels.Abbreviated)
elif self.game.settings.labels == "Dot Only":
self.mission.forced_options.labels = int(Labels.Dot)
elif self.game.settings.labels == "Off":
self.mission.forced_options.labels = int(Labels.Off)
def generate(self):
self._set_options_view()
self._set_external_views()
self._set_labels()

View File

@@ -0,0 +1,320 @@
import random
from enum import Enum
from typing import Dict, List
from dcs.vehicles import Armor, Artillery, Infantry, Unarmed
from dcs.unittype import VehicleType
import pydcs_extensions.frenchpack.frenchpack as frenchpack
from gen.ground_forces.combat_stance import CombatStance
from theater import ControlPoint
TYPE_TANKS = [
Armor.MBT_T_55,
Armor.MBT_T_72B,
Armor.MBT_T_80U,
Armor.MBT_T_90,
Armor.MBT_Leopard_2,
Armor.MBT_Leopard_1A3,
Armor.MBT_Leclerc,
Armor.MBT_Challenger_II,
Armor.MBT_M1A2_Abrams,
Armor.MBT_M60A3_Patton,
Armor.MBT_Merkava_Mk__4,
Armor.ZTZ_96B,
# WW2
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.MT_M4_Sherman,
Armor.MT_M4A4_Sherman_Firefly,
Armor.StuG_IV,
Armor.CT_Centaur_IV,
Armor.CT_Cromwell_IV,
Armor.HIT_Churchill_VII,
Armor.LT_Mk_VII_Tetrarch,
# Mods
frenchpack.DIM__TOYOTA_BLUE,
frenchpack.DIM__TOYOTA_GREEN,
frenchpack.DIM__TOYOTA_DESERT,
frenchpack.DIM__KAMIKAZE,
frenchpack.AMX_10RCR,
frenchpack.AMX_10RCR_SEPAR,
frenchpack.AMX_30B2,
frenchpack.Leclerc_Serie_XXI,
]
TYPE_ATGM = [
Armor.ATGM_M1045_HMMWV_TOW,
Armor.ATGM_M1134_Stryker,
Armor.IFV_BMP_2,
# WW2 (Tank Destroyers)
Armor.M30_Cargo_Carrier,
Armor.TD_Jagdpanzer_IV,
Armor.TD_Jagdpanther_G1,
Armor.TD_M10_GMC,
# Mods
frenchpack.VBAE_CRAB_MMP,
frenchpack.VAB_MEPHISTO,
frenchpack.TRM_2000_PAMELA,
]
TYPE_IFV = [
Armor.IFV_BMP_3,
Armor.IFV_BMP_2,
Armor.IFV_BMP_1,
Armor.IFV_Marder,
Armor.IFV_MCV_80,
Armor.IFV_LAV_25,
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.IFV_M2A2_Bradley,
Armor.IFV_BMD_1,
Armor.ZBD_04A,
# WW2
Armor.AC_Sd_Kfz_234_2_Puma,
Armor.LAC_M8_Greyhound,
Armor.Daimler_Armoured_Car,
# Mods
frenchpack.ERC_90,
frenchpack.VBAE_CRAB,
frenchpack.VAB_T20_13
]
TYPE_APC = [
Armor.APC_M1043_HMMWV_Armament,
Armor.APC_M1126_Stryker_ICV,
Armor.APC_M113,
Armor.APC_BTR_80,
Armor.APC_MTLB,
Armor.APC_M2A1,
Armor.APC_Cobra,
Armor.APC_Sd_Kfz_251,
Armor.APC_AAV_7,
Armor.TPz_Fuchs,
Armor.ARV_BRDM_2,
Armor.ARV_BTR_RD,
Armor.FDDM_Grad,
# WW2
Armor.APC_M2A1,
Armor.APC_Sd_Kfz_251,
# Mods
frenchpack.VAB__50,
frenchpack.VBL__50,
frenchpack.VBL_AANF1,
]
TYPE_ARTILLERY = [
Artillery.MLRS_9A52_Smerch,
Artillery.SPH_2S1_Gvozdika,
Artillery.SPH_2S3_Akatsia,
Artillery.MLRS_BM_21_Grad,
Artillery.MLRS_9K57_Uragan_BM_27,
Artillery.SPH_M109_Paladin,
Artillery.MLRS_M270,
Artillery.SPH_2S9_Nona,
Artillery.SpGH_Dana,
Artillery.SPH_2S19_Msta,
Artillery.MLRS_FDDM,
# WW2
Artillery.Sturmpanzer_IV_Brummbär,
Artillery.M12_GMC
]
TYPE_LOGI = [
Unarmed.Transport_M818,
Unarmed.Transport_KAMAZ_43101,
Unarmed.Transport_Ural_375,
Unarmed.Transport_GAZ_66,
Unarmed.Transport_GAZ_3307,
Unarmed.Transport_GAZ_3308,
Unarmed.Transport_Ural_4320_31_Armored,
Unarmed.Transport_Ural_4320T,
Unarmed.Blitz_3_6_6700A,
Unarmed.Kübelwagen_82,
Unarmed.Sd_Kfz_7,
Unarmed.Sd_Kfz_2,
Unarmed.Willys_MB,
Unarmed.Land_Rover_109_S3,
Unarmed.Land_Rover_101_FC,
# Mods
frenchpack.VBL,
frenchpack.VAB,
]
TYPE_INFANTRY = [
Infantry.Infantry_Soldier_Insurgents,
Infantry.Soldier_AK,
Infantry.Infantry_M1_Garand,
Infantry.Infantry_Mauser_98,
Infantry.Infantry_SMLE_No_4_Mk_1,
Infantry.Georgian_soldier_with_M4,
Infantry.Infantry_Soldier_Rus,
Infantry.Paratrooper_AKS,
Infantry.Paratrooper_RPG_16,
Infantry.Soldier_M249,
Infantry.Infantry_M4,
Infantry.Soldier_RPG,
]
MAX_COMBAT_GROUP_PER_CP = 10
class CombatGroupRole(Enum):
TANK = 1
APC = 2
IFV = 3
ARTILLERY = 4
SHORAD = 5
LOGI = 6
INFANTRY = 7
ATGM = 8
DISTANCE_FROM_FRONTLINE = {
CombatGroupRole.TANK:3200,
CombatGroupRole.APC:8000,
CombatGroupRole.IFV:3700,
CombatGroupRole.ARTILLERY:18000,
CombatGroupRole.SHORAD:13000,
CombatGroupRole.LOGI:20000,
CombatGroupRole.INFANTRY:3000,
CombatGroupRole.ATGM:6200
}
GROUP_SIZES_BY_COMBAT_STANCE = {
CombatStance.DEFENSIVE: [2, 4, 6],
CombatStance.AGGRESSIVE: [2, 4, 6],
CombatStance.RETREAT: [2, 4, 6, 8],
CombatStance.BREAKTHROUGH: [4, 6, 6, 8],
CombatStance.ELIMINATION: [2, 4, 4, 4, 6],
CombatStance.AMBUSH: [1, 1, 2, 2, 2, 2, 4]
}
class CombatGroup:
def __init__(self, role: CombatGroupRole):
self.units: List[VehicleType] = []
self.role = role
self.assigned_enemy_cp = None
self.start_position = None
def __str__(self):
s = ""
s += "ROLE : " + str(self.role) + "\n"
if len(self.units) > 0:
s += "UNITS " + self.units[0].name + " * " + str(len(self.units))
return s
class GroundPlanner:
def __init__(self, cp:ControlPoint, game):
self.cp = cp
self.game = game
self.connected_enemy_cp = [cp for cp in self.cp.connected_points if cp.captured != self.cp.captured]
self.tank_groups: List[CombatGroup] = []
self.apc_group: List[CombatGroup] = []
self.ifv_group: List[CombatGroup] = []
self.art_group: List[CombatGroup] = []
self.atgm_group: List[CombatGroup] = []
self.logi_groups: List[CombatGroup] = []
self.shorad_groups: List[CombatGroup] = []
self.units_per_cp: Dict[int, List[CombatGroup]] = {}
for cp in self.connected_enemy_cp:
self.units_per_cp[cp.id] = []
self.reserve: List[CombatGroup] = []
def plan_groundwar(self):
if hasattr(self.cp, 'stance'):
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[self.cp.stance]
else:
self.cp.stance = CombatStance.DEFENSIVE
group_size_choice = GROUP_SIZES_BY_COMBAT_STANCE[CombatStance.DEFENSIVE]
# Create combat groups and assign them randomly to each enemy CP
for key in self.cp.base.armor.keys():
role = None
collection = None
if key in TYPE_TANKS:
collection = self.tank_groups
role = CombatGroupRole.TANK
elif key in TYPE_APC:
collection = self.apc_group
role = CombatGroupRole.APC
elif key in TYPE_ARTILLERY:
collection = self.art_group
role = CombatGroupRole.ARTILLERY
elif key in TYPE_IFV:
collection = self.ifv_group
role = CombatGroupRole.IFV
elif key in TYPE_LOGI:
collection = self.logi_groups
role = CombatGroupRole.LOGI
elif key in TYPE_ATGM:
collection = self.atgm_group
role = CombatGroupRole.ATGM
else:
print("Warning unit type not handled by ground generator")
print(key)
continue
available = self.cp.base.armor[key]
while available > 0:
n = random.choice(group_size_choice)
if n > available:
if available >= 2:
n = 2
else:
n = 1
available -= n
group = CombatGroup(role)
if len(self.connected_enemy_cp) > 0:
enemy_cp = random.choice(self.connected_enemy_cp).id
self.units_per_cp[enemy_cp].append(group)
group.assigned_enemy_cp = enemy_cp
else:
self.reserve.append(group)
group.assigned_enemy_cp = "__reserve__"
for i in range(n):
group.units.append(key)
collection.append(group)
print("------------------")
print("Ground Planner : ")
print(self.cp.name)
print("------------------")
for key in self.units_per_cp.keys():
print("For : #" + str(key))
for group in self.units_per_cp[key]:
print(str(group))

View File

@@ -0,0 +1,11 @@
from enum import Enum
class CombatStance(Enum):
DEFENSIVE = 0 # Unit will adopt defensive stance with medium group of units
AGGRESSIVE = 1 # Unit will attempt to make progress with medium sized group of units
RETREAT = 2 # Unit will retreat
BREAKTHROUGH = 3 # Unit will attempt a breakthrough, rushing forward very aggresively with big group of armored units, and even less armored units will move aggresively
ELIMINATION = 4 # Unit will progress aggresively toward anemy units, attempting to eliminate the ennemy force
AMBUSH = 5 # Units will adopt a defensive stance a bit different from 'DEFENSIVE', ATGM & INFANTRY with RPG will be located on frontline with the armored units. (The groups of units will be smaller)

421
gen/groundobjectsgen.py Normal file
View File

@@ -0,0 +1,421 @@
"""Generators for creating the groups for ground objectives.
The classes in this file are responsible for creating the vehicle groups, ship
groups, statics, missile sites, and AA sites for the mission. Each of these
objectives is defined in the Theater by a TheaterGroundObject. These classes
create the pydcs groups and statics for those areas and add them to the mission.
"""
from __future__ import annotations
import logging
import random
from typing import Dict, Iterator, Optional, TYPE_CHECKING
from dcs import Mission
from dcs.country import Country
from dcs.statics import fortification_map, warehouse_map
from dcs.task import (
ActivateBeaconCommand,
ActivateICLSCommand,
EPLRS,
OptAlarmState,
)
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 RadioFrequency, RadioRegistry
from .runways import RunwayData
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,
radio_registry: RadioRegistry, tacan_registry: TacanRegistry):
self.m = mission
self.conflict = conflict
self.game = game
self.radio_registry = radio_registry
self.tacan_registry = tacan_registry
self.icls_alloc = iter(range(1, 21))
self.runways: Dict[str, RunwayData] = {}
def generate_farps(self, number_of_units=1) -> Iterator[StaticGroup]:
if self.conflict.is_vector:
center = self.conflict.center
heading = self.conflict.heading - 90
else:
center, heading = self.conflict.frontline_position(self.conflict.theater, self.conflict.from_cp, self.conflict.to_cp)
heading -= 90
initial_position = center.point_from_heading(heading, FARP_FRONTLINE_DISTANCE)
position = self.conflict.find_ground_position(initial_position, heading)
if not position:
position = initial_position
for i, _ in enumerate(range(0, number_of_units, self.FARP_CAPACITY)):
position = position.point_from_heading(0, i * 275)
yield self.m.farp(
country=self.m.country(self.game.player_country),
name="FARP",
position=position,
)
def generate(self):
for cp in self.game.theater.controlpoints:
if cp.captured:
country_name = self.game.player_country
else:
country_name = self.game.enemy_country
country = self.m.country(country_name)
for ground_object in cp.ground_objects:
if isinstance(ground_object, BuildingGroundObject):
generator = BuildingSiteGenerator(ground_object, country,
self.game, self.m)
elif isinstance(ground_object, CarrierGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, LhaGroundObject):
generator = CarrierGenerator(ground_object, cp, country,
self.game, self.m,
self.radio_registry,
self.tacan_registry,
self.icls_alloc, self.runways)
elif isinstance(ground_object, ShipGroundObject):
generator = ShipObjectGenerator(ground_object, country,
self.game, self.m)
else:
generator = GenericGroundObjectGenerator(ground_object,
country, self.game,
self.m)
generator.generate()

359
gen/kneeboard.py Normal file
View File

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

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

@@ -0,0 +1,29 @@
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
}
def generate_missile_group(game, ground_object, faction_name: str):
"""
This generate a ship group
:return: Nothing, but put the group reference inside the ground object
"""
faction = db.FACTIONS[faction_name]
if len(faction.missiles) > 0:
generators = faction.missiles
if len(generators) > 0:
gen = random.choice(generators)
if gen in MISSILES_MAP.keys():
generator = MISSILES_MAP[gen](game, ground_object, faction)
generator.generate()
return generator.get_generated_group()
else:
logging.info("Unable to generate missile group, generator : " + str(gen) + "does not exists")
return None

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)

32
gen/missiles/v1_group.py Normal file
View File

@@ -0,0 +1,32 @@
import random
from dcs.vehicles import Unarmed, MissilesSS, AirDefence
from gen.sam.group_generator import GroupGenerator
class V1GroupGenerator(GroupGenerator):
def __init__(self, game, ground_object, faction):
super(V1GroupGenerator, self).__init__(game, ground_object)
self.faction = faction
def generate(self):
# Ramps
self.add_unit(MissilesSS.V_1_ramp, "V1#0", self.position.x, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(MissilesSS.V_1_ramp, "V1#1", self.position.x + 50, self.position.y + random.randint(1, 8), self.heading)
self.add_unit(MissilesSS.V_1_ramp, "V1#2", self.position.x + 100, self.position.y + random.randint(1, 8), self.heading)
# Commander
self.add_unit(Unarmed.Kübelwagen_82, "Kubel#0", self.position.x - 35, self.position.y - 20,
self.heading)
# Self defense flak
flak_unit = random.choice([AirDefence.AAA_Flak_Vierling_38, AirDefence.AAA_Flak_38])
self.add_unit(flak_unit, "FLAK#0", self.position.x - 55, self.position.y - 38,
self.heading)
self.add_unit(Unarmed.Blitz_3_6_6700A, "Blitz#0",
self.position.x + 200, self.position.y + 15, 90)

View File

@@ -1,38 +1,84 @@
from game import db
import random
ALPHA_MILITARY = ["Alpha","Bravo","Charlie","Delta","Echo","Foxtrot",
"Golf","Hotel","India","Juliet","Kilo","Lima","Mike",
"November","Oscar","Papa","Quebec","Romeo","Sierra",
"Tango","Uniform","Victor","Whisky","XRay","Yankee",
"Zulu","Zero"]
class NameGenerator:
number = 0
def next_armor_group_name(self):
self.number += 1
return "Armor Unit {}".format(self.number)
ANIMALS = [
"SHARK", "TORTOISE", "BAT", "PANGOLIN", "AARDWOLF",
"MONKEY", "BUFFALO", "DOG", "BOBCAT", "LYNX", "PANTHER", "TIGER",
"LION", "OWL", "BUTTERFLY", "BISON", "DUCK", "COBRA", "MAMBA",
"DOLPHIN", "PHEASANT", "ARMADILLLO", "RACOON", "ZEBRA", "COW", "COYOTE", "FOX",
"LIGHTFOOT", "COTTONMOUTH", "TAURUS", "VIPER", "CASTOR", "GIRAFFE", "SNAKE",
"MONSTER", "ALBATROSS", "HAWK", "DOVE", "MOCKINGBIRD", "GECKO", "ORYX", "GORILLA",
"HARAMBE", "GOOSE", "MAVERICK", "HARE", "JACKAL", "LEOPARD", "CAT", "MUSK", "ORCA",
"OCELOT", "BEAR", "PANDA", "GULL", "PENGUIN", "PYTHON", "RAVEN", "DEER", "MOOSE",
"REINDEER", "SHEEP", "GAZELLE", "INSECT", "VULTURE", "WALLABY", "KANGAROO", "KOALA",
"KIWI", "WHALE", "FISH", "RHINO", "HIPPO", "RAT", "WOODPECKER", "WORM", "BABOON",
"YAK", "SCORPIO", "HORSE", "POODLE", "CENTIPEDE", "CHICKEN", "CHEETAH", "CHAMELEON",
"CATFISH", "CATERPILLAR", "CARACAL", "CAMEL", "CAIMAN", "BARRACUDA", "BANDICOOT",
"ALLIGATOR", "BONGO", "CORAL", "ELEPHANT", "ANTELOPE", "CRAB", "DACHSHUND", "DODO",
"FLAMINGO", "FERRET", "FALCON", "BULLDOG", "DONKEY", "IGUANA", "TAMARIN", "HARRIER",
"GRIZZLY", "GREYHOUND", "GRASSHOPPER", "JAGUAR", "LADYBUG", "KOMODO", "DRAGON", "LIZARD",
"LLAMA", "LOBSTER", "OCTOPUS", "MANATEE", "MAGPIE", "MACAW", "OSTRICH", "OYSTER",
"MOLE", "MULE", "MOTH", "MONGOOSE", "MOLLY", "MEERKAT", "MOUSE", "PEACOCK", "PIKE", "ROBIN",
"RAGDOLL", "PLATYPUS", "PELICAN", "PARROT", "PORCUPINE", "PIRANHA", "PUMA", "PUG", "TAPIR",
"TERMITE", "URCHIN", "SHRIMP", "TURKEY", "TOUCAN", "TETRA", "HUSKY", "STARFISH", "SWAN",
"FROG", "SQUIRREL", "WALRUS", "WARTHOG", "CORGI", "WEASEL", "WOMBAT", "WOLVERINE", "MAMMOTH",
"TOAD", "WOLF", "ZEBU", "SEAL", "SKATE", "JELLYFISH", "MOSQUITO", "LOCUST", "SLUG", "SNAIL",
"HEDGEHOG", "PIGLET", "FENNEC", "BADGER", "ALPACA", "DINGO", "COLT", "SKUNK", "BUNNY", "IMPALA",
"GUANACO", "CAPYBARA", "ELK", "MINK", "PRONGHORN", "CROW", "BUMBLEBEE", "FAWN", "OTTER", "WATERBUCK",
"JERBOA", "KITTEN", "ARGALI", "OX", "MARE", "FINCH", "BASILISK", "GOPHER", "HAMSTER", "CANARY", "WOODCHUCK",
"ANACONDA"
]
def next_cas_group_name(self):
self.number += 1
return "CAS Unit {}".format(self.number)
def __init__(self):
self.number = 0
self.ANIMALS = NameGenerator.ANIMALS.copy()
def next_escort_group_name(self):
self.number += 1
return "Escort Unit {}".format(self.number)
def reset(self):
self.number = 0
self.ANIMALS = NameGenerator.ANIMALS.copy()
def next_intercept_group_name(self):
def next_unit_name(self, country, parent_base_id, unit_type):
self.number += 1
return "Intercept Unit {}".format(self.number)
def next_ground_group_name(self):
self.number += 1
return "AA Unit {}".format(self.number)
return "unit|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
def next_transport_group_name(self):
def next_infantry_name(self, country, parent_base_id, unit_type):
self.number += 1
return "Transport Unit {}".format(self.number)
return "infantry|{}|{}|{}|{}|".format(country.id, self.number, parent_base_id, db.unit_type_name(unit_type))
def next_awacs_group_name(self):
self.number += 1
return "AWACS Unit {}".format(self.number)
def next_basedefense_name(self):
return "basedefense_aa|0|0|"
def next_passenger_group_name(self):
def next_awacs_name(self, country):
self.number += 1
return "Infantry Unit {}".format(self.number)
return "awacs|{}|{}|0|".format(country.id, self.number)
def next_tanker_name(self, country, unit_type):
self.number += 1
return "tanker|{}|{}|0|{}".format(country.id, self.number, db.unit_type_name(unit_type))
def next_carrier_name(self, country):
self.number += 1
return "carrier|{}|{}|0|".format(country.id, self.number)
def random_objective_name(self):
if len(self.ANIMALS) == 0:
return random.choice(ALPHA_MILITARY).upper() + "#" + str(random.randint(0, 100))
else:
animal = random.choice(self.ANIMALS)
self.ANIMALS.remove(animal)
return animal
namegen = NameGenerator()

245
gen/radios.py Normal file
View File

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

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